跳转至

文件附件与二维码

一句话定位:本章处理"附件字段值"这条隐式 JSON 数据线——上传控件存的是 [{"name":"x.pdf","size":1024,"path":"..."}] 形式的 JSON 数组字符串,要先解析才能拿到数量/文件名/大小;再延伸到 ZIP 打包下载、计算脚本图片自适应展示、二维码内容生成与扫码签到回调;最后用一张表收束上传控件的同构权限脚本族(下载/删除/重命名/预览/在线编辑/新版本/版本比较/版本还原/水印)。附件 JSON 用 org.json.JSONArray 解析,jsonArray.length() 是 Java 方法(合法),与 JS 字符串的 .length 属性是两码事——动手前先扫 GraalVM 差异

场景清单

场景 落点 / Label 主要素材
解析附件 JSON(数量/名/大小) 值脚本 FORM:FIELD_VALUE / 校验 FORM:FIELD_VALIDATE get-form-attachment-count
附件打包 ZIP 下载 视图操作 / 操作后置 / 计算 file-download
计算脚本图片自适应高宽 计算脚本 FORM:FIELD_VALUE(返回 HTML) calc-script-image-display-resize
二维码控件内容 FORM:FIELD_QR_CONTENT form-controls
二维码扫码签到回调 + 重复校验 二维码回调脚本 qrcode-callback
上传权限脚本族速查(下载/删除/重命名/预览/在线编辑/版本/水印) FORM:FIELD_UPLOAD_* 全族 iscript-usage/form

附件字段 JSON 形态(字段名按业务约定,常见为"附件"/"图片"/"文件"):

[
  {"name":"合同.pdf", "path":"/uploads/xxx", "size":102400, "type":"application/pdf"},
  {"name":"附件.doc", "path":"/uploads/yyy", "size":204800, "type":"application/msword"}
]

注意:字段未上传附件时可能返回 null"""[]"——解析前必须判空。


解析附件 JSON(数量/名/大小)

本章标杆场景:8 字段完整展开,后续场景沿用同构写法。

业务目标:把"附件"字段的 JSON 字符串解析出来,在表单上展示"共 N 个文件 / 总大小 X MB / 文件清单",或把数量写回另一个数值字段供后续流程使用。校验场景("至少上传 1 个、不超过 5 个")已在 → 附件上传数量校验 讲过,此处聚焦"解析 + 展示/统计"。

写在哪儿: - 值脚本 / 计算脚本(.form,属性 valueScript / JsonTemplate valuescript,Label FORM:FIELD_VALUE)—— 在派生字段(如"附件数量"、"附件清单"计算文本)里返回数量或拼接好的展示串。 - 校验脚本(.form,Label FORM:FIELD_VALIDATE)—— 校验数量时复用同一段解析逻辑(详见 表单与导入校验)。

挂载点位置见 where-to-use/form/form-controls

触发时机与上下文:表单打开、刷新计算时。可用环境变量:WebUser(→ curruser API)、CurrentDocument(→ doc API)、RelateDocumentParentDocument

返回值契约:值脚本——Number(数量)或 String(拼接展示文案),赋给当前字段。计算脚本(HTML 类型控件)可返回 HTML 串直接渲染。

示例代码

值脚本——返回附件数量:

(function () {
  var jsonText = getItemValueAsString("附件");
  var number = 0;
  if (jsonText != null && jsonText.trim().length > 0) {
    // JSONArray 是真实 Java 对象,length() 是方法(合法),不要改成 .length 属性
    var JSONArray = Java.type('org.json.JSONArray');
    var jsonArray = new JSONArray(jsonText);
    number = jsonArray.length();
  }
  return number;
})()

值脚本——返回"附件清单 + 总大小"展示串:

(function () {
  var jsonText = getItemValueAsString("附件");
  if (jsonText == null || jsonText.trim().length === 0) {
    return "暂无附件";
  }
  var JSONArray = Java.type('org.json.JSONArray');
  var jsonArray = new JSONArray(jsonText);
  var names = [];
  var totalBytes = 0;
  for (var i = 0; i < jsonArray.length(); i++) {   // Java 方法 length()
    var fileObj = jsonArray.getJSONObject(i);
    names.push(fileObj.getString("name"));
    totalBytes += fileObj.optLong("size", 0);       // optLong 兜底 0,避免缺字段异常
  }
  var totalMB = (totalBytes / 1024 / 1024).toFixed(2);
  return "共 " + jsonArray.length() + " 个,"
       + totalMB + " MB:" + names.join("、");
})()

操作后置——把数量写回"附件数量"字段(保存后持久化):

(function () {
  var doc = getCurrentDocument();
  var jsonText = doc.getItemValueAsString("附件");
  var number = 0;
  if (jsonText != null && jsonText.trim().length > 0) {
    var JSONArray = Java.type('org.json.JSONArray');
    number = new JSONArray(jsonText).length();
  }
  doc.findItem("附件数量").setValue(number);
})()

代码说明

  • getItemValueAsString("附件") —— 取附件字段的 JSON 字符串值;空时返回 null"",二者都要判。
  • Java.type('org.json.JSONArray') —— GraalVM 推荐的 Java 类引用方式;旧示例里的 new Packages.org.json.JSONArray(...) 仍可兼容运行(→ GraalVM 差异 → 取 Java 类)。
  • jsonArray.length() —— Java 方法,返回数组长度。这是 GraalVM 下少有的合法 .length():因为 jsonArray 是真实 Java 对象,不是 JS 数组、也不是 JS 字符串。**不要**改成 .length 属性,详见 GraalVM 差异 → 字符串长度
  • jsonArray.getJSONObject(i).getString("name") / .optLong("size", 0) —— 标准 org.json.JSONObject API;.optXxx(key, default) 在字段缺失时返回默认值,比 .getXxx(key) 安全。
  • doc.findItem("字段名").setValue(value) —— 把派生值写回主表字段。

变体与扩展

  • 过滤特定类型:遍历时按 fileObj.getString("name") 的扩展名筛选(如只统计 .pdf)。
  • 多附件字段汇总:把多个附件字段的 JSON 各自解析后累加。
  • HTML 渲染清单:计算脚本里把 names 拼成 <ul><li>...</li></ul> 直接渲染。
  • 校验数量上限:复用同一段解析,把"非空串=失败提示"的校验契约套上去(见 → 附件上传数量校验)。

常见坑

  • 混淆 .length.length():JS 字符串 jsonText.trim().length 是**属性**(无括号);jsonArray.length()Java 方法(带括号)。两者在同一脚本里出现时最易写错,详见 GraalVM 差异
  • 空值未判直接解析new JSONArray(null)new JSONArray("") 会抛异常,必须先 if (jsonText != null && jsonText.trim().length > 0)
  • 字段名匹配:附件字段名("附件"/"图片"/"文件")须与表单实际字段一致;不同表单上传控件对应的字段名不同。
  • getLong("size") 缺字段抛异常:附件 JSON 中若个别对象缺 size 键,.getLong("size") 直接抛异常,建议用 .optLong("size", 0)

附件打包 ZIP 下载

业务目标:把表单中多个附件打包成 ZIP 文件并提供下载链接,常用于"一键下载全部附件"、批量归档、批量导出。

写在哪儿:通常挂在视图操作按钮的后置脚本或表单工具栏按钮的执行后脚本(.activity,属性 afteractionscript,Label ACTIVITY:AFTER);也可放在计算脚本里返回 URL 串供前端跳转。挂载点位置见 where-to-use/view/view-actions

触发时机与上下文:操作按钮点击后触发。可用 getCurrentDocument()(→ doc API)、getWebUser()getItemValueAsString("字段名")。视图操作上下文还可用 getParameterAsArray("_selects") 取选中记录。

返回值契约String——ZIP 文件的相对下载路径(如 /uploads/zipFile/xxx.zip),前端按此路径触发跳转下载。

示例代码

(function () {
  var fileString = getItemValueAsString("附件");
  if (fileString == null || fileString.trim().length === 0) {
    return ""; // 无附件,交由前端处理
  }

  // —— Java 工具类:源素材用 Packages.xxx 形式,平台兼容写法,保留并加注 ——
  var ZipUtil = new Packages.cn.myapps.util.file.ZipUtil();
  var FileUtil = new Packages.cn.myapps.util.file.FileUtil();
  var JSONArray = new Packages.net.sf.json.JSONArray();
  var fileArr = JSONArray.fromObject(fileString);

  // 5.1+ 版本取存储根路径
  var rootPath = Packages.cn.myapps.common.Environment.getInstance().getStorageRootPath();
  var destFolder = rootPath + "/uploads/zipFile";
  var copyFolder = destFolder + "/copyFile";

  // 确保临时目录存在
  var copyDir = new Packages.java.io.File(copyFolder);
  if (!copyDir.exists()) {
    copyDir.mkdirs();
  }

  // 复制文件到临时目录,处理重名
  var files = [];
  var index = 1;
  for (var it = fileArr.iterator(); it.hasNext();) {
    var file = it.next();
    var path = rootPath + file.getString("path");
    var name = file.getString("name");
    var fileType = name.substring(name.lastIndexOf("."));
    var newPath = copyFolder + "/" + name;
    if (new Packages.java.io.File(newPath).exists()) {
      newPath = copyFolder + "/" + name.replace(fileType, index + fileType);
      index++;
    }
    FileUtil.copyFile(newPath, path);
    files.push(newPath);
  }

  // 压缩 + 清理临时副本
  var zipName = "附件_" + format(new Date(), "yyyyMMddHHmmss");
  ZipUtil.compressFiles(zipName, files, destFolder);
  for (var i = 0; i < files.length; i++) {   // files 是 JS 数组,用 .length
    FileUtil.deleteFiles(files[i]);
  }

  return "/uploads/zipFile/" + zipName + ".zip";
})()

代码说明

  • Packages.cn.myapps.util.file.ZipUtil / FileUtil —— 平台内部 Java 工具类,源素材均以 Packages.xxx 形式引用,按 GraalVM 差异 → 取 Java 类 的"兼容保留"规则沿用;新代码不强制改写为 Java.type,因为平台内部类用 Packages 更直观。
  • JSONArray.fromObject(jsonString) —— net.sf.json.JSONArray 的解析方法(与第 1 节的 org.json.JSONArray 构造函数是**两套不同的 JSON 库**,不要混用)。
  • Environment.getInstance().getStorageRootPath() —— 5.1+ 版本取存储根路径;旧版本(4.4 用 cn.myapps.util.property.PropertyUtil、5.0 用 cn.myapps.common.util.PropertyUtil)按部署版本切换。
  • ZipUtil.compressFiles(zipName, files, destFolder) —— 参数:ZIP 名(不含扩展名)、待压缩文件路径数组(JS 数组即可)、目标目录。
  • format(date, pattern) —— 日期格式化,给 ZIP 名加时间戳避免重名。

变体与扩展

  • 按文档名命名 ZIPvar zipName = getCurrentDocument().getItemValueAsString("文档名称") || "附件";
  • 过滤特定类型再打包:遍历时按扩展名白名单筛选。
  • 单文件直链下载:不压缩,直接 return fileArr.getJSONObject(0).getString("path");

常见坑

  • files 是 JS 数组用 .lengthfileArr 是 Java List/JSONArray.iterator().size()——两种长度写法混用是最常见的踩坑点,详见 GraalVM 差异 → Java List .size() vs JS 数组 .length
  • 根路径版本不匹配:4.⅘.0/5.1 三种 PropertyUtil / Environment 写法分别对应不同平台版本,错用会找不到类。
  • 目录权限:确保应用运行账号对 destFolder/copyFolder 有写权限。
  • 磁盘空间:临时副本会翻倍占用空间,压缩完成后必须清理(脚本末尾的 deleteFiles 循环不能漏)。
  • 大量文件耗时:压缩成百上千个文件可能超时,建议改为后台任务或限制单次打包数量。

计算脚本图片自适应高宽

业务目标:在表单的计算脚本控件里展示一张或多张图片,限制最大宽高并保持原始比例,避免图片溢出撑爆布局。

写在哪儿:表单域 → 表单 → 计算脚本控件(控件类型设为 HTML/计算文本)(.form,属性 valueScript / JsonTemplate valuescript,Label FORM:FIELD_VALUE)。挂载点位置见 where-to-use/form/form-controls

触发时机与上下文:表单打开、刷新计算时。可用 getCurrentDocument()getWebUser()

返回值契约String——HTML 片段,平台按 HTML 渲染。

示例代码

单张图片 + 限制最大宽高(推荐写法):

(function () {
  var doc = getCurrentDocument();
  var imageUrl = doc.getItemValueAsString("图片字段");
  if (imageUrl == null || imageUrl.trim().length === 0) {
    return "<span style='color:#999;'>未设置图片</span>";
  }
  var maxWidth = 500;
  var maxHeight = 400;
  return "<img src='" + imageUrl + "' "
       + "style='max-width:" + maxWidth + "px;"
       + " max-height:" + maxHeight + "px;"
       + " width:auto; height:auto; display:block;'"
       + " onerror=\"this.style.display='none'\">";
})()

多张图片(附件字段是 JSON 数组)+ Flex 布局:

(function () {
  var doc = getCurrentDocument();
  var jsonText = doc.getItemValueAsString("图片");
  if (jsonText == null || jsonText.trim().length === 0) {
    return "<span style='color:#999;'>暂无图片</span>";
  }
  var JSONArray = Java.type('org.json.JSONArray');
  var jsonArray = new JSONArray(jsonText);
  var maxWidth = 300, maxHeight = 300;
  var html = "<div style='display:flex; flex-wrap:wrap; gap:10px;'>";
  for (var i = 0; i < jsonArray.length(); i++) {   // Java 方法
    var imgObj = jsonArray.getJSONObject(i);
    var url = imgObj.getString("path");
    html += "<img src='" + url + "' "
          + "style='max-width:" + maxWidth + "px;"
          + " max-height:" + maxHeight + "px;"
          + " width:auto; height:auto;"
          + " border:1px solid #ccc;'>";
  }
  html += "</div>";
  return html;
})()

代码说明

  • CSS 关键三件套:max-width + max-height + width:auto; height:auto; —— 限制上限的同时保持原始比例。
  • object-fit: contain; —— 进一步保证图片完整显示不裁剪。
  • getCurrentDocument().getItemValueAsString("字段名") —— 从当前文档取图片 URL 字段。
  • Java.type('org.json.JSONArray') —— 解析附件 JSON 数组(同第 1 节),遍历用 jsonArray.length()(Java 方法)。

变体与扩展

  • 点击放大:外层包 <div onclick='window.open(url, "_blank")'>
  • 响应式:用 CSS 媒体查询 @media (max-width:768px){...} 适配移动端。
  • 缩略图列表 + 轮播:遍历数组时把第一张 display:block,其余 display:none,前端再加切换按钮。
  • 懒加载<img loading='lazy' ...>

常见坑

  • 图片 URL 转义:URL 含 & 时直接拼到 HTML 里会破坏属性解析,应先 imageUrl.replace(/&/g, "&amp;")
  • onerror 引号嵌套:HTML 里嵌 JS 字符串时引号要交替使用(外双内单或反之)。
  • 计算脚本不落库:返回的 HTML 只渲染不入库,不能依赖此脚本做数据持久化。
  • 附件 JSON 解析方法名:本节用 org.json.JSONArray 的构造函数,第 2 节用 net.sf.json.JSONArray.fromObject——是两套不同 JSON 库,API 不可混用。

二维码控件内容

业务目标:在表单上动态生成二维码内容(URL 或文本),扫码后跳转或显示信息;内容可按当前文档/用户动态变化(如生成"签到码"、"作品编号链接")。

写在哪儿:表单域 → 表单 → 二维码控件 → 内容脚本(.form,属性"二维码内容脚本";Label FORM:FIELD_QR_CONTENT)。挂载点位置见 where-to-use/form/form-controls

触发时机与上下文:表单打开时。可用环境变量:WebUserCurrentDocumentRelateDocumentParentDocument

返回值契约String——二维码内容。若是 URL,扫码后浏览器会跳转到此 URL;若是普通文本,扫码后显示该文本。

示例代码

固定 URL:

(function () {
  return "https://www.example.com/";
})()

按当前文档 ID 拼动态签到链接:

(function () {
  var doc = getCurrentDocument();
  var docId = doc.getId();
  // 拼成带 docId 参数的签到链接,配合下一节"扫码回调"完成签到
  return "https://app.example.com/qrcode/checkin?docId=" + docId;
})()

按当前用户 + 时间生成一次性签到码:

(function () {
  var user = getWebUser();
  var code = user.getId() + "|" + format(new Date(), "yyyyMMddHHmm");
  return code;
})()

代码说明

变体与扩展

  • 生成业务编号二维码getItemValueAsString("流水号") 直接返回。
  • 配扫码回调:内容脚本生成扫码入口、回调脚本(见下一节)处理扫码后的业务逻辑——两者配合实现完整签到闭环。
  • 加密签名 URL:对参数做签名(如 MD5)防止用户改 URL 伪造签到。

常见坑

  • 二维码内容为空:脚本忘了 return,或返回 undefined,二维码渲染为空。
  • URL 未转义:拼到 URL 里的参数含 &/=/中文 时要 encodeURIComponent
  • 字符串比较用 ===:根据返回值切换二维码内容时不要用 .equals(→ GraalVM 差异)。

二维码扫码签到回调 + 重复校验

业务目标:用户用手机扫描表单上的二维码后,平台回调本脚本——校验当前用户是否在"签到人员"名单中、是否已签到过,然后写库并返回提示消息。典型用于会议签到、活动签到、培训打卡。

写在哪儿:表单域 → 表单 → 二维码控件 → 回调脚本(.form,属性依产品约定为"回调脚本",Label 平台未公开固定枚举;与 第 4 节 的"内容脚本"成对配置)。挂载点位置见 where-to-use/form/form-controls

触发时机与上下文:用户扫码并命中回调时触发。可用 getCurrentDocument()(→ doc API)、getWebUser()(→ curruser API)。

返回值契约JsMessage 对象——平台按消息类型(成功/提示/警告/错误)弹窗显示。也可返回 String,平台按普通提示处理。

示例代码

(function () {
  var user = getWebUser();
  var userId = user.getId();
  var doc = getCurrentDocument();
  var docId = doc.getId();

  // 已签到名单(形如 "u1;u2;u3;"),按 ; 拆成 JS 数组
  var signedRaw = doc.getItemValueAsString("签到");
  var signedList = (signedRaw == null || signedRaw === "") ? [] : signedRaw.split(";");

  // 1. 重复校验:当前用户已在"签到"字段中
  for (var i = 0; i < signedList.length; i++) {   // JS 数组用 .length
    if (signedList[i] === userId) {
      return new JsMessage(2, "您已签到,无需重复签到!");
    }
  }

  // 2. 资格校验:当前用户是否在"签到人员"名单中
  var allowedRaw = doc.getItemValueAsString("签到人员");
  var allowedList = (allowedRaw == null || allowedRaw === "") ? [] : allowedRaw.split(";");
  var isAllowed = false;
  for (var j = 0; j < allowedList.length; j++) {
    if (allowedList[j] === userId) {
      isAllowed = true;
      break;
    }
  }

  if (!isAllowed) {
    return new JsMessage(4, "您不在本次签到名单中,签到失败!");
  }

  // 3. 写库:把当前用户追加到"签到"字段
  var newSigned = (signedRaw == null ? "" : signedRaw) + userId + ";";
  var sql = "update tlk_会议签到 set item_签到 = '" + newSigned.replace(/'/g, "''")
          + "' where id = '" + docId + "'";
  updateByDSName("business", sql);

  // 同步内存中的文档对象,避免后续脚本读到旧值
  doc.findItem("签到").setValue(newSigned);

  return new JsMessage(1, "签到成功!");
})()

代码说明

  • JsMessage(type, text) —— 平台消息对象,type1=成功、2=提示、3=警告、4=错误(与源素材约定一致)。也可改用 MSGBOX 工厂的 createSuccess(text) / createInfo(text) / createWarning(text) / createAlert(text),语义更清晰。
  • String.split(";") —— JS 字符串方法,返回 JS 数组,长度用 .length(属性,无括号)。
  • updateByDSName(dsName, sql) —— 跨数据源执行 update/insert/delete;表名前缀 tlk_<表单名>,业务字段列名 item_<字段名>
  • doc.findItem("字段名").setValue(value) —— 同步更新内存中的文档字段,避免回调内后续脚本读到旧值。
  • 字符串相等用 ===(→ GraalVM 差异)。

变体与扩展

  • 签到时间落库:在表中加 item_签到时间 列,update 时一起写入 format(new Date(), "yyyy-MM-dd HH:mm:ss")
  • 签到人数统计:签到成功后 queryBySQL("select count(...) ...") 统计并回写到主表"已签到人数"字段。
  • 迟到判断:取当前时间与会议开始时间比较,超过则返回 JsMessage(3, "您已迟到")
  • 不写 SQL 改用文档 APIdoc.findItem("签到").setValue(newSigned); getDocumentProcess().doUpdate(doc); 替代裸 SQL,避免表名/字段名硬编码。

常见坑

  • split 返回的是 JS 数组——长度用 .length(属性),**不要**写 .size().length()
  • SQL 注入:把字段值原样拼进 SQL 极易被注入,生产环境务必先 replace(/'/g, "''") 或改用文档 API(doUpdate)。
  • 表前缀与字段列名:平台业务表为 tlk_<表单名>、字段列为 item_<字段名>;写错 SQL 静默失败不报错。
  • 数据源名大小写updateByDSName 第一个参数须与平台数据源配置名完全一致。
  • JsMessage 类型枚举:½/¾ 的语义在不同版本/素材中描述略有差异;上线前用 println 打印一次确认。
  • 重复签到竞态:高并发下两个请求同时读到未含本用户的"签到"字段,会出现覆盖——可加锁或在 SQL 里 where item_签到 not like '%<userId>;%' 兜底。

上传权限脚本族速查

上传控件的"下载/删除/重命名/预览/在线编辑/新版本/版本比较/版本还原/水印"9 个挂载点**属性同构**(都是布尔权限或字符串水印)、写法一致,没必要逐条展开 8 字段模板,合并为一张速查表。素材:agent-skills-usage/skills/iscript-usage/form.md

业务目标:对上传控件(附件/图片)的每个操作按钮**单独**控制权限——谁能下载、谁能删除、谁能在线编辑、谁能还原旧版本;以及对预览/下载行为打水印。

写在哪儿:表单域 → 表单 → 上传控件 → 各操作脚本(.form,Label FORM:FIELD_UPLOAD_*,属性名见下表)。挂载点位置见 where-to-use/form/form-controls

触发时机与上下文:用户点击上传控件对应的操作按钮时(或控件渲染时取权限)。可用 getCurrentDocument()getWebUser()

返回值契约:前 8 个权限脚本——Booleantrue = 允许该操作、false = 禁用该按钮(隐藏或置灰)。水印脚本——String:水印文案。

速查表

Label 属性名 返回 典型用法
FORM:FIELD_UPLOAD_DOWNLOAD downloadscript Boolean 仅"下载员"角色允许下载
FORM:FIELD_UPLOAD_REMOVE deletescript Boolean 保存后不可删,按 getCurrentDocument().isFirstNode() 判断
FORM:FIELD_UPLOAD_RENAME renamescript Boolean 仅上传者本人允许重命名
FORM:FIELD_UPLOAD_PREVIEW previewscript Boolean 公开链接分享时禁用预览
FORM:FIELD_UPLOAD_ONLINEEDIT editscript Boolean 仅"编辑员"角色允许在线编辑
FORM:FIELD_UPLOAD_VERSION_RENEW newversionscript Boolean 修改文档时强制上传新版本
FORM:FIELD_UPLOAD_VERSION_COMPARE versioncomparescript Boolean 法务审批场景允许版本对比
FORM:FIELD_UPLOAD_VERSION_REVERT restorescript Boolean 仅管理员允许还原旧版本
FORM:FIELD_UPLOAD_WATERMARK watermarkscript String 预览/下载时打当前用户姓名水印,溯源防泄密

最短示例(前 8 个布尔权限通用模板):

// 仅"管理员"角色允许该操作
(function () {
  var roles = getWebUser().getRoles();   // Java Collection
  if (roles == null || roles.size() === 0) {
    return false;
  }
  for (var it = roles.iterator(); it.hasNext();) {
    var r = it.next();
    if (r.getName() === "管理员") {
      return true;
    }
  }
  return false;
})()

水印脚本示例FORM:FIELD_UPLOAD_WATERMARK):

(function () {
  // 拼上用户名 + 时间,作为附件预览/下载水印
  return getWebUser().getName() + " " + format(new Date(), "yyyy-MM-dd HH:mm");
})()

代码说明

变体与扩展

  • 按字段状态联动var st = getCurrentDocument().getItemValueAsString("状态"); return st === "草稿"; —— 仅草稿状态可删/改名。
  • 按流程节点getCurrentDocument().isFirstNode() —— 仅首节点(起草)允许删/重命名。
  • 水印按角色隐藏:管理员返回 ""(不打水印),其他角色返回用户名。

常见坑

  • getRoles() 是 Java Collection——用 .size() 取长度,不要写 .length;遍历用 .iterator(),不要写 roles[i]
  • 属性名拼写:上传控件属性多为**全小写脚本字段名**(downloadscriptdeletescript),不要写成 camelCase。
  • 布尔语义方向true = 允许操作(与"隐藏脚本 true=隐藏"的方向**相反**,容易记混)——上传权限族是"允许"语义,隐藏/只读族是"生效"语义。
  • 水印脚本返回空串"" 视为不打水印,不要返回 null

相关场景

  • → 附件上传数量校验 —— 校验脚本里复用本章第 1 节的 JSON 解析模式。
  • → 字段只读/隐藏 —— 与本章第 6 节上传权限族同属"权限脚本"大家族,但契约方向不同(隐藏脚本 true=隐藏、上传权限 true=允许),切换场景时务必注意。
  • → 附录 A 工具函数 —— JSON 创建/解析的更多模式(createOptions().toJSON()Packages.net.sf.json.JSONObject 等)。