表单与导入校验¶
一句话定位:校验类脚本的核心契约是**「失败返回提示 String,成功返回
""(空串)」**——既不是null,也不是true/false。本章所有挂载点(表单字段、子表、附件、流程路径、视图操作、Excel 导入列、查重)都遵守这一契约,差异只在「写在哪儿」和「触发时机」。
场景清单¶
| 场景 | 落点 / Label | 主要素材 |
|---|---|---|
| 字段非空/格式校验 | 校验脚本 FORM:FIELD_VALIDATE |
form-controls |
| 子表(网格视图)非空校验 | 校验脚本 / 保存前 | grid-view-non-empty-check |
| 附件上传数量校验 | 校验脚本 FORM:FIELD_VALIDATE |
get-form-attachment-count |
| 流程送出前校验(备注必填) | 路径送出校验 WORKFLOW:RELATION_PASS_VALIDATE |
flow-path |
| 视图操作执行前校验(数量上限) | 操作前置 VIEW:COLUMN_OPERATE_BEFORE |
view-actions |
| Excel 导入列校验 + 数据清洗 | EXCELIMPORT:COLUMN_VALIDATE / COLUMN_VALUE |
excel-import |
| 记录是否存在 / 查重 | 校验脚本 FORM:FIELD_VALIDATE |
check-record-exists-or-count |
校验失败统一弹窗:只要返回的是**非空字符串**,平台就会以提示框形式展示并**阻断后续动作**(保存/提交/送出/操作)。返回
""视为通过。详见 前言 → 默认包装与返回值契约通则。
字段非空/格式校验¶
本章标杆场景:8 字段完整展开,后续场景沿用同构写法。
业务目标:在保存/提交表单前,校验某字段不为空、或符合指定格式(手机号、邮箱、身份证等),不通过则给出可读提示并阻止保存。
写在哪儿:表单域 → 表单 → 表单控件 → 校验(.form,属性 validateScript / JsonTemplate 校验相关键,Label FORM:FIELD_VALIDATE)。挂载点位置见 iscript-guid/basic → form-controls。
触发时机与上下文:保存/提交校验时触发。可用环境变量:WebUser、CurrentDocument、RelateDocument、ParentDocument。
返回值契约:String——非空串 = 校验失败的提示文案(平台弹窗显示);""(空串)= 校验通过。不是 null,不是 true/false。
示例代码:
// 校验"职务"字段不能为空
(function () {
var rt = getItemValueAsString("职务");
if (rt == null || rt === "") {
return "'职务'不能为空";
}
return "";
})()
// 校验手机号格式(非空 + 11 位数字)
(function () {
var mobile = getItemValueAsString("手机号");
if (mobile == null || mobile.trim() === "") {
return "请填写手机号";
}
// JS 正则:11 位数字
if (!/^1\d{10}$/.test(mobile)) {
return "手机号格式不正确(应为 11 位数字)";
}
return "";
})()
代码说明:
getItemValueAsString("字段名")—— 按字段名取当前文档的字符串值;空字段返回null或"",需同时判断。getCurrentDocument()—— 取当前文档对象(值/校验脚本上下文里隐式可用,getItemValueAsString即作用于该文档)。getWebUser()—— 取当前登录用户,常用于"按角色放宽校验"。- 字符串相等用
===/!==;判空用v == null || v === ""(== null同时覆盖null与undefined)。详见 GraalVM 差异。
变体与扩展:
- 数值范围:
var n = Number(getItemValueAsString("金额")); if (!(n > 0)) return "金额必须大于 0"; - 日期:用
getItemValueAsDate取 Date 对象后再比较;勿对字符串直接做日期比较。 - 条件必填:根据他字段值决定是否必填(如"类型=报销"时"发票号"必填)。
- 多字段一次性校验:把多个
if串联,任意一条失败立即return提示;不要拼成"字段1、字段2 不能为空"的长串,体验差。
常见坑:
- 用
"".equals(v)判空——GraalVM 下字符串字面量**没有**.equals方法,须改v === ""。详见 GraalVM 差异。 - 用
v.length()取长度——JS 字符串的length是**属性**,不是方法。 - 成功路径忘了
return "",导致返回undefined,行为依平台版本而异(旧版可能弹"undefined")。 - 把校验脚本当值脚本写(返回布尔)——校验脚本只看是否为非空串。
子表(网格视图)非空校验¶
业务目标:保存前确保子表(网格视图)至少有一条有效明细,常用于报销明细、订单明细、出库清单等场景。
写在哪儿:通常写在保存前/提交前的操作前置脚本(.activity,Label ACTIVITY:BEFORE);也可挂在主表或子表字段的校验脚本(.form,Label FORM:FIELD_VALIDATE)。两者返回值契约一致(非空串=失败提示)。
触发时机与上下文:保存或提交前触发。可用环境变量:WebUser、CurrentDocument。
返回值契约:String——非空串 = 失败提示;"" 或不返回 = 通过。
示例代码:
// 校验子表至少有 1 条未删除的明细
(function () {
var doc = getCurrentDocument();
var subs = doc.getSubDocuments(); // 取所有子文档(含已删除)
// 收集未删除行
var list = createObject("java.util.ArrayList");
for (var iter = subs.iterator(); iter.hasNext();) {
var subDoc = iter.next();
if (!subDoc.isDelete()) { // true 表示该行被标记删除
list.add(subDoc);
}
}
if (list.size() === 0) {
return "单据体没有数据,不允许保存";
}
return "";
})()
代码说明:
getCurrentDocument()—— 取主表文档。doc.getSubDocuments()—— 取所有子文档(网格视图行),包含已标记删除的行,需自行用subDoc.isDelete()过滤。createObject("java.util.ArrayList")—— 平台助手函数,创建 JavaArrayList(比Java.type('java.util.ArrayList')更短,二者皆可)。- 遍历 Java
Collection用.iterator()+.hasNext()+.next();取长度用.size(),不是.length。详见 GraalVM 差异。
变体与扩展:
- 指定子表:在循环里加
if (subDoc.getFormname().indexOf("子表名") >= 0 && !subDoc.isDelete())仅统计某个子表。 - 行数下限/上限:
if (list.size() < 2) return "至少 2 行"; if (list.size() > 100) return "超过 100 行"; - 特定列非空:在循环内对每行
subDoc.getItemValueAsString("关键字段")再判空,全空才报错。 - 金额合计校验:在循环内累加某列,校验合计是否超过上限。
常见坑:
- 忘记排除已删除行:用户在前端删了一行又点了保存,
getSubDocuments()仍会返回该行(带删除标记),不过滤就会误判"有数据"。 - 变量名覆盖:循环里再用
var doc = iter.next()会遮蔽外层doc,建议起名subDoc。 - 把 List 当数组:
list.length、list[i]都错——JavaList用list.size()、list.get(i)。 ==比较:if (list.size() == 0)在 GraalVM 下能跑(弱相等),但全书约定写===。
附件上传数量校验¶
业务目标:保存前校验附件字段上传的文件数量(至少 N 个、不超过 M 个、必须为正整数等),常用于合同、报销单据。
写在哪儿:表单域 → 表单控件 → 校验(.form,属性 validateScript,Label FORM:FIELD_VALIDATE)。
触发时机与上下文:保存/提交前触发。可用环境变量:WebUser、CurrentDocument、RelateDocument、ParentDocument。
返回值契约:String——非空串 = 失败提示;"" = 通过。
示例代码:
// 校验附件数量:至少 1 个,不超过 5 个
(function () {
var jsonText = getItemValueAsString("附件");
var number = 0;
if (jsonText != null && jsonText.trim().length > 0) {
// 附件字段存的是 JSON 数组字符串;JSONArray 是真实 Java 对象
var JSONArray = Java.type('org.json.JSONArray');
var jsonArray = new JSONArray(jsonText);
number = jsonArray.length(); // 例外:Java 方法,不是 JS 属性
}
if (number === 0) {
return "请至少上传 1 个附件";
}
if (number > 5) {
return "附件数量不能超过 5 个(当前 " + number + " 个)";
}
return "";
})()
代码说明:
getItemValueAsString("附件")—— 附件字段返回 JSON 数组字符串(如[{"name":"x.pdf","size":1024,...}]),空时为null或""。Java.type('org.json.JSONArray')—— GraalVM 推荐写法。源示例里的new Packages.org.json.JSONArray(...)仍可兼容,新代码优先Java.type。jsonArray.length()—— GraalVM 例外:JSONArray是真实 Java 类型,length()是 Java 方法,不要改成.length。详见 GraalVM 差异。- JS 字符串的
jsonText.trim().length是属性,不带括号——与上面的 Java 方法对照记忆。
变体与扩展:
- 总大小限制:循环
for (var i = 0; i < jsonArray.length(); i++)取jsonArray.getJSONObject(i).getLong("size")累加,超阈值报错。 - 类型限制:取
fileObj.getString("name")后看扩展名是否在白名单。 - 统计字段写回:在值脚本里把
number写到"附件数量"字段(doc.findItem("附件数量").setValue(number))。
常见坑:
- 空 JSON 解析异常:
jsonText为""或null时直接new JSONArray(...)会抛错,必须先判空再解析。 - 混用
.length与.length():JS 字符串用属性、JSONArray 用方法,写反任一都会错。 - 用
Packages.org.json.JSONArray:兼容但属旧写法,新代码用Java.type('org.json.JSONArray')。
流程送出前校验(备注必填)¶
业务目标:流程提交经过某条路径时,强制要求填写备注/意见/某字段,否则不允许送出。
写在哪儿:流程域 → 流程 → 关联线信息 → 路径送出校验脚本 → 值脚本(.flow 路径属性 validateScript,Label WORKFLOW:RELATION_PASS_VALIDATE)。挂载点见 iscript-guid/basic → flow-path。
触发时机与上下文:流程提交前触发(类似表单控件校验,但作用于路径)。可用环境变量:WebUser、CurrentDocument。
返回值契约:String——非空串 = 失败提示;"" = 通过。
示例代码:
// 路径送出前校验"备注"必填
(function () {
var doc = getCurrentDocument();
var value = doc.getItemValueAsString("备注");
if (value == null || value === "") {
return "备注内容不能为空";
}
return "";
})()
代码说明:
getCurrentDocument()—— 流程上下文中的当前文档。doc.getItemValueAsString("字段名")——iscript-guid/api→ currdoc 中 Document 的字段取值方法。- 与表单字段校验完全同构,差异仅在挂载点(路径而非字段)和触发时机(送出而非保存)。
变体与扩展:
- 按路径分支不同校验:不同路径挂不同
validateScript,例如驳回路径要求填写驳回理由。 - 审批意见校验:结合
getParameter("_attitude")判断当前操作类型再决定是否必填。 - 金额阈值:
if (Number(doc.getItemValueAsString("金额")) > 10000 && value === "") return "金额超 1 万必须填备注";
常见坑:
- 与路径条件混淆:
WORKFLOW:RELATION_CONDITION返回Boolean(是否走该路径),RELATION_PASS_VALIDATE返回String(失败提示/空串),两个 Label 不要写串。 - 与"路径执行脚本"混淆:
RELATION_PASS_ACTION是经过时执行副作用(写状态、改字段),不返回提示;要做"拦截送出"必须用RELATION_PASS_VALIDATE。 - 源示例里
if (value == null || value == "")用==——GraalVM 下能跑,但全书统一写===。
视图操作执行前校验(数量上限)¶
业务目标:在视图里点行内操作或顶部按钮时,执行默认动作前对勾选记录做业务校验(数量上下限、字段值范围等),不通过则弹提示并阻止后续动作。
写在哪儿:视图域 → 视图 → 操作(或行内操作列)→ 动作执行前脚本(.view,操作属性 beforeScript,Label VIEW:COLUMN_OPERATE_BEFORE)。挂载点见 iscript-guid/basic → view-actions。
触发时机与上下文:点击视图按钮、在执行默认操作前触发。可用环境变量:WebUser、CurrentDocument、Params(含 _selects 勾选记录 id 分号串)。
返回值契约:String / Message / 空——返回非空串或 Message 对象时,平台以弹窗形式展示并**不执行**后续默认动作与脚本;返回空("" / 不返回)表示放行。
示例代码:
// 勾选记录的"数量"字段都不能小于 100,否则阻断
(function () {
var selectId = getParameterAsText("_selects"); // 形如 "id1;id2;id3"
if (selectId == null || selectId === "") {
return "请先勾选记录";
}
// 拼成 SQL 的 IN 列表:('id1','id2','id3')
var sel = "('" + selectId.replace(/;/g, "','") + "')";
var sql = "select * from tlk_材料信息表 where id in " + sel;
var datas = queryBySQL(sql);
if (datas != null && datas.size() > 0) {
for (var iter = datas.iterator(); iter.hasNext();) {
var row = iter.next();
var number = row.getItemValueAsString("数量");
if (Number(number) < 100) {
return "勾选数据中存在数量小于 100 的值";
}
}
}
return "";
})()
代码说明:
getParameterAsText("_selects")—— 取勾选记录 id 的分号分隔串;若需要数组形式用getParameterAsArray(注意:返回的是 JS 数组,长度用.length)。queryBySQL(sql)—— 按表名前缀tlk_查询,返回Collection(用.iterator()遍历、.size()取长度)。详见 GraalVM 差异 → Java List vs JS 数组。- 查到的是
Document/数据行对象,字段取值用row.getItemValueAsString("字段名")。 createConfirm("...")—— 如果要弹"确认/取消"对话框(而非直接阻断),返回createConfirm(...)即可。
变体与扩展:
- 勾选数量上下限:用
getParameterAsArray("_selects").length判断勾了几条。 - 批量删除前确认:
return createConfirm("确认删除选中的 " + n + " 条记录?"); - 按状态过滤:在 SQL 里
and item_状态 = '已审',查不到记录则阻断。
常见坑:
_selects形态混淆:getParameterAsText返回分号串(要.replace转义),getParameterAsArray返回 JS 数组(用.length、下标访问)。不要把数组当字符串处理。- SQL 注入:直接把
_selects拼进 SQL 是源素材的做法,但生产环境若 id 可被前端伪造需注意;高安全场景建议改用in (?)参数绑定或先校验 id 形态。 selectId.length(属性)不是.length()——JS 字符串。- 判空方向:源示例里
if (selectId != null || selectId.length > 0)的||应为&&,复制时注意修正。
Excel 导入列校验 + 数据清洗¶
一个场景、两个挂载点:导入每行时既可"清洗值"(COLUMN_VALUE),也可"校验值"(COLUMN_VALIDATE)。两者常配合使用:先清洗成规范值,再校验。
业务目标:Excel 导入时对某列做"负数置零""金额上限""空值报错"等处理,避免脏数据进库。
写在哪儿:
- 数据清洗:Excel 导入域 → Excel 导入 → 列信息 → 值脚本(
.excelconfig列属性valueScript,LabelEXCELIMPORT:COLUMN_VALUE;运行/调试时归一到FORM:FIELD_VALUE)。 - 列校验:Excel 导入域 → Excel 导入 → 列信息 → 检验脚本(
.excelconfig列属性validateRule,LabelEXCELIMPORT:COLUMN_VALIDATE)。
挂载点见 iscript-guid/basic → excel-import。
触发时机与上下文:导入 Excel 时每条记录触发一次。可用环境变量:CurrentDocument(导入行映射成的临时文档)。
返回值契约:
COLUMN_VALUE:返回写入字段的值(String/Number/Date 等),无强制空串约定。COLUMN_VALIDATE:String——非空串 = 错误提示并终止导入该行;""= 通过。
示例代码:
// === COLUMN_VALUE:金额为负或缺失时置 0 ===
(function () {
var doc = getCurrentDocument();
var value = doc.getItemValue("金额"); // 原始值(可能是数字/字符串)
if (value == null || Number(value) < 0) {
return 0;
}
return value;
})()
// === COLUMN_VALIDATE:名称必填,空则终止导入 ===
(function () {
var doc = getCurrentDocument();
var value = doc.getItemValueAsString("名称");
if (value == null || value === "") {
return "名称不能为空";
}
return "";
})()
代码说明:
getCurrentDocument()—— 当前导入行被映射为临时文档,doc.getItemValue("字段名")取原始值(类型依列),doc.getItemValueAsString("字段名")取字符串。- 调试监听:
EXCELIMPORT:COLUMN_VALUE在调试器里会归一到FORM:FIELD_VALUE(见iscript-usage→ excelimport),行为一致。 - 校验失败返回非空串时,平台会把该行作为错误行展示给导入者。
变体与扩展:
- 日期格式归一化:把
"2024/1/1"、"2024-1-1"统一转成"yyyy-MM-dd"再返回(COLUMN_VALUE)。 - 字典码 → 名称:导入的是代码(如"01"),查询字典表转成名称"已审"再入库。
- 金额超阈值告警:
if (Number(value) > 100000) return "单行金额超过 10 万,请确认"; - 跨字段校验:在同一文档上取多列值联合判断。
常见坑:
- 返回类型:COLUMN_VALUE 返回的是要**入库的值**,不是提示串;把"提示"错返回到 COLUMN_VALUE 会污染数据。
- Label 归并:调试时看到
FORM:FIELD_VALUE是正常的(不是配置错了)。 - 空串 vs null:
value === ""才是真空串;value == null仅判断 null/undefined。判"未填写"建议两者都判:v == null || v === ""。 - 不要在校验里写副作用(如
doUpdate),导入事务由平台控制。
记录是否存在 / 查重¶
业务目标:保存前根据业务主键(编码、身份证、合同号等)查重,确保唯一性;或检查关联记录是否存在。
写在哪儿:表单域 → 表单控件 → 校验(.form,属性 validateScript,Label FORM:FIELD_VALIDATE);也可放保存前操作前置。
触发时机与上下文:保存/提交前触发。可用环境变量:WebUser、CurrentDocument。
返回值契约:String——非空串 = 失败提示;"" = 通过。
示例代码:
// 新增/编辑时按"合同号"查重,编辑时排除自身
(function () {
var doc = getCurrentDocument();
var code = doc.getItemValueAsString("合同号");
if (code == null || code === "") {
return ""; // 非空校验交给另一个脚本,这里放行
}
// 拼SQL:item_字段名 是平台字段列名约定;tlk_ 是表名前缀
var selfId = doc.getId();
var sql = "select id from tlk_合同表 " +
"where item_合同号 = '" + code + "' " +
"and id <> '" + selfId + "'";
var count = countBySQL(sql);
if (count > 0) {
return "合同号已存在,请检查后再提交";
}
return "";
})()
代码说明:
countBySQL(sql)—— 按 SQL 统计记录数,比queryBySQL更轻;只需判断"有没有"时优先用它。也可用findBySQL(sql)(返回单条文档或null)。- 平台约定:表单字段在数据库中的列名为
item_字段名;表名为tlk_表单名;多租户/多域时建议加domainid限定(getWebUser().getDomainid())。 getCurrentDocument().getId()—— 取当前文档 id,编辑场景下用于排除自身。
变体与扩展:
- 跨表查重:
select id from tlk_另一张表 where item_某字段 = '...'。 - 多条件查重:
where item_字段1 = '...' and item_字段2 = '...' and domainid = '...'。 - 限本域:
and domainid = '" + getWebUser().getDomainid() + "'",避免跨域误判。 - 统计计数:返回具体数量给前端(值脚本/计算脚本场景):
return "已存在 " + count + " 条相同记录"; - 存在即更新(操作后置场景,不是校验):用
updateByDSName执行update语句。
常见坑:
- 编辑时把自己算成重复:必须
and id <> '<当前文档id>',否则一保存就报"已存在"。 - 表名前缀/字段列名:写
from 合同表、where 合同号都查不到——平台表名是tlk_表单名、字段列名是item_字段名。 - SQL 注入:直接拼用户输入有风险,关键场景应做格式校验(如只允许字母数字)再拼。
- 跨域数据:不加
domainid限定时,可能把别的租户/域的同名记录误判为重复。 count > 0用>:countBySQL返回数值,直接比较即可;不要写=== true。- 把校验脚本当查询脚本:在 校验脚本里
doUpdate/sendMail会破坏保存事务,副作用应放操作后置。
本章"返回值契约"速查¶
| 场景 | Label | 失败 | 成功 |
|---|---|---|---|
| 字段校验 | FORM:FIELD_VALIDATE |
非空提示 String | "" |
| 子表非空 | 校验/操作前置 | 非空提示 String | "" |
| 附件数量 | FORM:FIELD_VALIDATE |
非空提示 String | "" |
| 流程送出 | WORKFLOW:RELATION_PASS_VALIDATE |
非空提示 String | "" |
| 视图操作前 | VIEW:COLUMN_OPERATE_BEFORE |
非空串 / Message | "" / 不返回 |
| Excel 校验 | EXCELIMPORT:COLUMN_VALIDATE |
非空提示 String | "" |
| 记录查重 | FORM:FIELD_VALIDATE |
非空提示 String | "" |
一句话记住:校验脚本永远
return ""表示通过;想阻断就return "可读提示"。其它返回形态(null、true、false、undefined)都不是契约保证的"通过"语义,不要依赖。
相关章节¶
- 字段值/选项/默认值填充:表单数据填充、联动与动态选项
- 字段隐藏/只读/打印隐藏:权限与可见性控制
- SQL 查询与批量操作:数据查询、统计与文档批量操作
- 路径条件/经过动作(与送出校验对比):流程路由条件与子流程