流程查询、审批历史与人工干预¶
本章面向**已经会基本流程脚本(审批人、路径条件)**的开发者,解决三类问题:
- 查询——取流程信息、当前/上一节点、审批历史、当前待办经办人;
- 覆盖——改最后一条审批意见、判断当前用户是不是经办人;
- 干预——脚本里直接
doBack/doTerminate/doSkipNode/doSubmit,类似后台"流程监控"按钮的能力。风险提示:本章后半段(「流程回退」~「视图伪提交待办」)属于**高权限、部分不可逆**操作,可能改变流程状态、清理待办、删除任务记录。务必先在测试环境验证,并加权限校验、操作日志、
doUpdate落库这三道保险。语法层面(.equals/.length()/java.util.X)请先翻 getting-started.md#graalvm-差异。
场景速查¶
| 场景 | 落点 | 关键 API | 风险 |
|---|---|---|---|
| 取流程信息 / 当前节点 / 上一节点状态 | 值脚本 / 计算脚本 | getFlowProcess().doView/doViewByDocument、SQL 查 t_flowstatert |
只读 |
取流程提交基本信息(_attitude/_flowType/_targetNode) |
操作前置 / 值脚本 | getParameter |
只读 |
| 审批历史查询(多展示) | 计算 / 视图列 | SQL 查 t_actorhis |
只读 |
| 覆盖最后一条审批意见 | 操作后置 | updateByDSName 改 T_ACTORHIS/T_RELATIONHIS |
中(直接改系统表) |
| 判断当前用户是否经办人 | 校验 / 隐藏 / 过滤 | SQL 查 t_actor |
只读 |
| 流程回退(上/指定/首节点) | 操作前置 / 后置 | flowProcess.doBack |
高、不可逆 |
| 强制终止 + 清理待办/通知 | 操作前置 / 后置 | flowProcess.doTerminate |
高、不可逆 |
| 干预:跳过 / 回退 / 终止 / 干预提交 | 操作前置 / 后置 | doSkipNode/doBack/doTerminate/doSubmit |
高、部分不可逆 |
| 视图伪提交待办 | 视图操作按钮 | getParameter("_selects") + doUpdate |
中(绕过正常流转) |
| 节点审批 / 过期时限 | WORKFLOW:NODE_TIME_LIMIT |
getItemValueAsDate、getToday、getDay |
低 |
涉及的系统表:
t_flow(流程定义)、t_flowstatert(流程运行时状态)、t_flow_node(节点定义)、t_actor(当前待处理执行人,state=0待处理)、t_actorhis(审批历史)、t_relationhis(路径历史)、t_task(待办任务)。
取流程信息、当前节点与上一节点状态¶
业务目标:在表单/视图里展示"当前在哪个节点、上一节点叫什么、流程整体跑没跑完",或据此分支后续逻辑。三种信息同源(都从 getCurrentDocument() + t_flowstatert 出发),合并为一节。
写在哪儿:
- 域 → 路径 → 脚本类型:表单 → 表单控件 → 值脚本(valueScript,Label FORM:FIELD_VALUE),或表单/视图 → 计算 → 计算脚本;
- 属性名示例:valueScript;
- Label:FORM:FIELD_VALUE。
- 想把结果落到字段时也可以放在"操作前置/后置"里 + doc.findItem(x).setValue(...)。
触发时机与上下文:表单打开、字段重算、视图列渲染时触发;可用环境变量 getCurrentDocument()、getWebUser()。
返回值契约:值脚本返回字符串/对象,赋给当前字段;若用作内部逻辑,可返回 JS 对象(在计算脚本里序列化为 JSON 串)。
示例代码(取当前节点名称 + 上一节点名称,组合为一个字符串):
(function () {
var doc = getCurrentDocument();
var docId = doc.getId();
// 1) 当前节点:直接用文档 stateLabel
var currentStateLabel = doc.getStateLabel();
// 2) 上一节点:查审批历史里"非当前节点"的最新一条
var sql = "select domainid,state_label,actor_name,attitude,processtime ";
sql += "from t_actorhis ";
sql += "where flowstatert_id in (select id from t_flowstatert where docid = '" + docId + "') ";
sql += "and state_label != '" + currentStateLabel + "' ";
sql += "order by processtime desc limit 1";
var prev = findBySQL(sql);
var prevLabel = "";
var prevActor = "";
if (prev != null) {
prevLabel = prev.getItemValueAsString("state_label");
prevActor = prev.getItemValueAsString("actor_name");
}
// 拼一行展示文本
return "当前节点:" + currentStateLabel +
";上一节点:" + (prevLabel || "(无)") +
(prevActor ? "(" + prevActor + ")" : "");
})()
如需整张流程对象(含开始/结束时间、所有节点),改用 getFlowProcess().doViewByDocument(doc):
(function () {
var doc = getCurrentDocument();
var flowProcess = getFlowProcess();
var flow = flowProcess.doViewByDocument(doc);
if (flow == null) {
return null;
}
return {
id: flow.getId(),
name: flow.getName(),
state: flow.getState(),
currentNode: flow.getCurrentNode() != null ? flow.getCurrentNode().getName() : "",
startTime: flow.getStartTime(),
endTime: flow.getEndTime()
};
})()
代码说明:
- getCurrentDocument() / doc.getStateLabel() / doc.getId() → iscript-guid/api → doc
- findBySQL(sql) / queryBySQL(sql)(返回 java.util.List,遍历用 .iterator() 或 .size()+.get(i)) → iscript-guid/api → database
- getFlowProcess()、flowProcess.doViewByDocument(doc)、flow.getCurrentNode()、flow.getNodes() → iscript-guid/api → flow
- 系统表 t_flowstatert / t_actorhis / t_flow_node 的字段含义见章首"系统表"。
变体与扩展:
- 取所有节点列表:flow.getNodes() → nodes.size()+nodes.get(i) 遍历,注意这是 java.util.List,不能**用 .length(详见 GraalVM 差异「Java List .size() vs JS 数组」)。
- **取当前节点的审批人:在 t_actor 上 where flowstatert_id=... and state=0 查。
- 落到字段:把 return ... 换成 doc.findItem("当前节点").setValue(currentStateLabel); 并在末尾 getDocumentProcess().doUpdate(doc); 落库(参见「流程回退」)。
常见坑:
- 第一个节点没有上一节点:prev 会是 null,记得判空,否则 prev.getItemValueAsString 会 NPE。
- getFlowProcess().doViewByDocument(doc) 可能返回 null(文档未起流程时);用前先判空。
- 字符串拼接 SQL:本章所有示例为了可读性直接拼字符串,生产代码请用参数化或对值做转义,特别是当 stateLabel 来自用户输入时——SQL 注入风险详见 getting-started.md#graalvm-差异。
- Java List vs JS 数组:flow.getNodes()、queryBySQL() 返回的是 java.util.List,长度用 .size()、取元素用 .get(i)、遍历用 .iterator();不要写成 .length。
取流程提交基本信息¶
业务目标:在提交瞬间拿到 _attitude(审批意见)、_flowType(操作类型)、_targetNode(目标节点 id),用于日志、字段联动、按操作类型分支。
写在哪儿:表单/视图 → 操作按钮 → 操作前置 / 操作后置(如 ACTIVITY:BEFORE / ACTIVITY:AFTER,或表单"保存/提交前后置")。属性名以具体按钮配置为准。
触发时机与上下文:用户点提交/回退/保存按钮触发;可用 getParameter("_attitude")、getParameter("_flowType")、getParameter("_targetNode")、getWebUser()、getCurrentDocument()。
返回值契约:
- 操作前置:失败返回非空 String(阻断),成功返回 ""(继续);
- 操作后置:通常无返回值,做副作用(写日志、联动字段)。
示例代码(操作后置:根据 _attitude/_flowType 写"最后操作时间"和"处理结果"):
(function () {
var doc = getCurrentDocument();
var user = getWebUser();
var attitude = getParameter("_attitude");
var flowType = getParameter("_flowType");
// _flowType 常见值:"80"=保存/提交/暂存,"81"=回退
if (flowType === "80") {
doc.findItem("最后操作时间").setValue(format(new Date(), "yyyy-MM-dd HH:mm:ss"));
}
// 按 _attitude 关键字联动处理结果字段
var result = "";
if (attitude != null) {
if (attitude.indexOf("同意") >= 0 || attitude.indexOf("通过") >= 0) {
result = "通过";
} else if (attitude.indexOf("不同意") >= 0 || attitude.indexOf("驳回") >= 0) {
result = "不通过";
}
}
if (result !== "") {
doc.findItem("处理结果").setValue(result);
}
return ""; // 操作后置无返回值或返回空串
})()
代码说明:
- getParameter(name) 取平台注入参数 → iscript-guid/functions → system-functions
- getCurrentDocument() / doc.findItem(x).setValue(v) → iscript-guid/functions → curdoc-functions、iscript-guid/api → doc
- getWebUser() → iscript-guid/api → curruser
- format(date, pattern) → iscript-guid/functions → date-functions
变体与扩展:
- 批量取参数:getParamsTable() 拿到 ParamsTable 后用 .getParameterAsString("_attitude")。
- 取目标节点对象:拿到 _targetNode 后 flow.getNodeById(targetNodeId) → node.getName()。
- 写日志表:用 updateByDSName($datasource, "insert into tlk_日志表 (...) values (...)")。
常见坑:
- _attitude 可能为 null:用户没填意见时是 null,调 attitude.indexOf 会 NPE;先 attitude != null 判空。
- _flowType 是字符串字面量:比较用 flowType === "80",不要写 flowType == 80(== 会做类型转换但严格相等更安全,详见 GraalVM 差异)。
- 提交时机 vs 状态更新时机:操作**前置**触发时,流程状态尚未更新;要拿"提交后状态"必须在操作**后置**或 t_actorhis 里查。
审批历史查询(列表 / 时间线 / 统计)¶
业务目标:把审批历史以"列表 HTML / 时间线 JSON / 统计数字"形式展示在表单字段或视图列里。
写在哪儿:表单 → 表单控件 → 值脚本(Label FORM:FIELD_VALUE),或视图 → 视图列 → 列值脚本(Label VIEW:COLUMN_VALUE)。
触发时机与上下文:表单/视图渲染时;可用 getCurrentDocument()。
返回值契约:值脚本返回字符串(HTML 或纯文本);列值脚本同理。
示例代码(值脚本:返回一段审批历史 HTML 表格):
(function () {
var doc = getCurrentDocument();
var docId = doc.getId();
// 先取 flowstatert id
var fsSQL = "select domainid,id from t_flowstatert where docid = '" + docId + "'";
var fs = findBySQL(fsSQL);
if (fs == null) {
return "暂无审批历史";
}
var fsId = fs.getId();
// 查历史
var sql = "select domainid,actor_name,state_label,attitude,processtime ";
sql += "from t_actorhis ";
sql += "where flowstatert_id = '" + fsId + "' ";
sql += "order by processtime";
var rows = queryBySQL(sql);
var html = "<table border='1'>" +
"<tr><th>节点</th><th>审批人</th><th>意见</th><th>时间</th></tr>";
if (rows != null && rows.size() > 0) {
for (var it = rows.iterator(); it.hasNext();) {
var h = it.next();
var attitude = h.getItemValueAsString("attitude");
html += "<tr>";
html += "<td>" + h.getItemValueAsString("state_label") + "</td>";
html += "<td>" + h.getItemValueAsString("actor_name") + "</td>";
html += "<td>" + (attitude != null ? attitude : "") + "</td>";
html += "<td>" + format(h.getItemValueAsDate("processtime"), "yyyy-MM-dd HH:mm:ss") + "</td>";
html += "</tr>";
}
}
html += "</table>";
return html;
})()
代码说明:
- queryBySQL(sql) 返回 java.util.List,遍历用 .iterator() → iscript-guid/api → database
- format(date, pattern) → iscript-guid/functions → date-functions
- t_actorhis 字段:actor_name(审批人姓名)、state_label(节点名)、attitude(意见,可能为 null)、processtime(处理时间)。
变体与扩展:
| 变体 | 关键改动 |
|---|---|
| 最后一条审批历史 | SQL 末尾改 order by processtime desc limit 1,用 findBySQL 取单条 |
| 指定节点历史 | SQL 加 and state_label = '节点名' |
| 时间线 JSON | 遍历 push 到 JS 数组,return JSON.stringify(timeline);相邻两条算时间差:diff = (cur.getTime() - prev.getTime()) / 3600000 |
| 统计(同意/反对条数) | 遍历时 if (attitude.indexOf("同意") >= 0) agreeCount++; |
常见坑:
- attitude 字段可能为 null:渲染时三元判空,否则页面上出现 "null"。
- queryBySQL 返回 null vs 空 List:没数据时可能返回 null,先用 rows != null && rows.size() > 0 双重判断。
- 视图列里直接拼 HTML:确认视图配置允许 HTML 渲染(部分版本需开启"显示为 HTML"选项),否则会被当文本转义。
- rows.size() 不要写成 rows.length(Java List)—— GraalVM 差异「Java List .size() vs JS 数组」。
覆盖最后一条审批意见¶
业务目标:在流程干预/管理员修正场景,把"最后一条审批意见"(如自动跳过时缺省的"自动通过")改成更有意义的文字。直接 UPDATE T_ACTORHIS 与 T_RELATIONHIS 两张系统表。
写在哪儿:表单/视图 → 操作按钮 → 操作后置(如 ACTIVITY:AFTER)。属性名以按钮配置为准。
触发时机与上下文:操作提交完成后;可用 getCurrentDocument()、getWebUser()。
返回值契约:操作后置无强制返回,做副作用。
示例代码:
(function () {
var doc = getCurrentDocument();
var docId = doc.getId();
var newAttitude = "管理员修正:补充审批意见";
setLastAttitude(docId, newAttitude);
})()
// 把最后一条审批意见覆盖为新值;同时改 T_RELATIONHIS 与 T_ACTORHIS
function setLastAttitude(docid, dValue) {
var dName = sys_function.DATASOURCENAME;
// 1) T_RELATIONHIS
var updateRel = " UPDATE T_RELATIONHIS SET ATTITUDE = '" + dValue + "' ";
updateRel += " WHERE DOCID = '" + docid + "' ";
updateRel += " AND PROCESSTIME = (SELECT MAX(PROCESSTIME) FROM T_RELATIONHIS WHERE DOCID = '" + docid + "')";
updateByDSName(dName, updateRel);
// 2) T_ACTORHIS(ATTITUDE 在 SQL Server 下是关键字,用 [ATTITUDE] 括起来)
var updateAtt = " UPDATE T_ACTORHIS SET [ATTITUDE] = '" + dValue + "' ";
updateAtt += " WHERE DOC_ID = '" + docid + "' ";
updateAtt += " AND FLOWSTATERT_ID = (SELECT ID FROM T_FLOWSTATERT WHERE DOCID = '" + docid + "') ";
updateAtt += " AND PROCESSTIME = (SELECT MAX(PROCESSTIME) FROM T_ACTORHIS WHERE DOC_ID = '" + docid + "' ";
updateAtt += " AND FLOWSTATERT_ID = (SELECT ID FROM T_FLOWSTATERT WHERE DOCID = '" + docid + "'))";
updateByDSName(dName, updateAtt);
}
代码说明:
- updateByDSName(dsName, sql) → iscript-guid/api → database
- sys_function.DATASOURCENAME 取默认数据源名(全局对象,平台注入)。
- 两张系统表:T_RELATIONHIS(关系历史)、T_ACTORHIS(执行人历史);都用 MAX(PROCESSTIME) 锁定"最后一条"。
变体与扩展:
- 按字段动态取意见:var v = doc.getItemValueAsString("新审批意见"); if (v == null || v.trim().length === 0) return "意见不能为空";
- 批量覆盖多个文档:外层 for 循环 docIds,逐个调 setLastAttitude。
常见坑:
- 直接改系统表:会绕过流程引擎的一致性校验,不可逆,先备份再执行;操作前最好 select 一遍确认目标行。
- 原历史脚本里有个 SQL 错误:SELECT MAX(PROCESSTIME) T_RELATIONHIS WHERE ... 漏了 FROM 关键字——上面示例已修正为 SELECT MAX(PROCESSTIME) FROM T_RELATIONHIS WHERE ...。
- ATTITUDE 是关键字:SQL Server 下需用 [ATTITUDE](方括号);MySQL/Oracle 下不需要。
- 意见含单引号:拼接 SQL 会断裂,必须 dValue.replace(/'/g, "''") 转义,或改用参数化。
- 空串判断用 .length 不是 .length()(JS 字符串)—— GraalVM 差异「字符串长度:用 .length 属性,禁用 `.le」。
判断当前用户是否经办人¶
业务目标:根据"我是不是这条流程的当前经办人"控制按钮显示/字段编辑权/分支逻辑。
写在哪儿:表单 → 表单控件 → 隐藏脚本 / 只读脚本 / 校验脚本(hiddenScript/readonlyScript/validateScript);或视图 → 操作按钮 → 操作前置 / 隐藏(ACTIVITY:HIDDEN/ACTIVITY:BEFORE)。
触发时机与上下文:表单/视图渲染或按钮点击时;可用 getCurrentDocument()、getWebUser()。
返回值契约:布尔脚本——true 表示隐藏/只读生效;校验脚本——非空串阻断、"" 通过。
示例代码(按钮"隐藏脚本":当前用户不是经办人时**隐藏**按钮,返回 true 即隐藏):
(function () {
var doc = getCurrentDocument();
var docId = doc.getId();
var uid = getWebUser().getId();
// 取 flowstatert id
var fsSQL = "select domainid,id from t_flowstatert where docid = '" + docId + "'";
var fs = findBySQL(fsSQL);
if (fs == null) {
return true; // 没起流程,直接隐藏
}
var fsId = fs.getId();
// t_actor 里 state=0 表示当前待处理经办人
var sql = "select domainid ";
sql += "from t_actor ";
sql += "where flowstatert_id = '" + fsId + "' ";
sql += "and actor_id = '" + uid + "' ";
sql += "and state = 0";
var count = countBySQL(sql);
// count > 0 表示是经办人 → 不隐藏(return false);否则隐藏(return true)
return !(count > 0);
})()
代码说明:
- countBySQL(sql) 返回数字 → iscript-guid/api → database
- getWebUser().getId() → iscript-guid/api → curruser
- t_actor.state:0=待处理、1=已处理、2=已跳过/已终止。
- 布尔契约统一:true = 隐藏/只读生效(详见 getting-started.md「返回值契约一览」)。
变体与扩展:
| 变体 | 改法 |
|---|---|
| 是否**当前节点**经办人 | 关联 t_flow_node 用 node_name = stateLabel 取 node_id,加 and node_id = '...' |
| 是否**已处理过**的经办人 | state = 1 |
| 是否**任意节点**经办人 | 去掉 state 条件 |
| 返回经办人列表 | 改用 queryBySQL + .iterator() 遍历 push 到 JS 数组 |
常见坑:
- state 必须显式限定:不写 state 时会同时命中"已处理"的人,把按钮给错对象。
- 多重身份:协办人也在 t_actor 里,但 state/type 字段不同;区分协办/主办需要按字段过滤(具体字段名以部署版本为准)。
- count > 0 写法:countBySQL 返回数值,直接 return count > 0 即可,不要再 === true。
- 管理员/超管不一定是经办人:要给管理员开绿灯,单独加 if (roleName === "管理员") return false; 分支。
流程回退(doBack)¶
业务目标:脚本里把流程从当前节点回退到**上一节点 / 指定节点 / 首节点**。常用于"管理员纠正""退回申请人补材料"。
写在哪儿:表单/视图 → 操作按钮 → 操作前置 / 操作后置(ACTIVITY:BEFORE/ACTIVITY:AFTER)。
触发时机与上下文:用户点操作按钮时;可用 getCurrentDocument()、getWebUser()、createParamsTable()。
返回值契约:前置非空串阻断、"" 通过;后置做副作用。回退本身由 flowProcess.doBack(...) 完成。
示例代码(回退到指定节点,并更新审批历史/字段/落库):
(function () {
var doc = getCurrentDocument();
var docId = doc.getId();
var targetNodeId = "目标节点ID"; // 替换为实际节点 id
var flowProcess = getFlowProcess();
var flow = flowProcess.doViewByDocument(doc);
if (flow == null) {
return "流程不存在";
}
var targetNode = flow.getNodeById(targetNodeId);
if (targetNode == null) {
return "目标节点不存在";
}
// 1) 记录回退字段(供表单展示)
doc.findItem("回退操作").setValue("是");
doc.findItem("回退时间").setValue(format(new Date(), "yyyy-MM-dd HH:mm:ss"));
doc.findItem("回退人").setValue(getWebUser().getName());
doc.findItem("回退目标节点").setValue(targetNode.getName());
// 2) 执行回退
var params = createParamsTable();
params.setParameter("_attitude", doc.getItemValueAsString("回退原因"));
flowProcess.doBack(flow, targetNode, params, getWebUser());
// 3) 落库(字段修改必须 doUpdate,否则不生效)
var docProcess = getDocumentProcess();
docProcess.doUpdate(doc);
return "回退成功";
})()
代码说明:
- getFlowProcess()、flowProcess.doViewByDocument(doc)、flow.getNodeById(id)、flow.getNodes()、flowProcess.doBack(flow, node, params, user) → iscript-guid/api → flow
- createParamsTable() 创建参数表 → iscript-guid/functions → system-functions
- getDocumentProcess().doUpdate(doc) 把字段修改落库 → iscript-guid/api → doc
变体与扩展:
| 变体 | 关键改动 |
|---|---|
| 回退到上一节点 | 用 flow.getNodes() 遍历找到当前节点下标 currentIndex,取 nodes.get(currentIndex - 1) |
| 回退到第一个节点 | nodes.get(0) |
| 条件回退 | 按 doc.getItemValueAsString("条件字段") 选 targetNodeId |
| 回退 + 通知 | doBack 后查 t_actor(flowstatert_id=fsId and node_id=targetNodeId and state=0)拿审批人 id,调 sendMessage |
常见坑:
- doBack 不可逆:执行后流程状态立刻改变,无法"撤回";务必先 select 确认目标节点正确。
- 字段修改不落库:doc.findItem(...).setValue(...) 只改内存对象,必须 docProcess.doUpdate(doc) 才会写表——这是干预类脚本最常见的坑。
- 目标节点 id 必须存在:flow.getNodeById(targetNodeId) 返回 null 时不要继续,否则 doBack NPE。
- 权限:建议先用 getWebUser().getRole() 判断是否"管理员/流程管理员",否则任何人都能脚本回退。详见 GraalVM 差异。
- doBack 顺序:先 setValue → 再 doBack → 最后 doUpdate;颠倒可能导致字段被流程引擎覆盖。
强制终止流程(doTerminate + 清理待办 + 通知)¶
业务目标:业务取消/异常时强制结束流程,并清理待办、通知相关人员、更新文档状态。
写在哪儿:表单/视图 → 操作按钮 → 操作前置 / 操作后置。
触发时机与上下文:用户点操作按钮时;可用 getCurrentDocument()、getWebUser()、createParamsTable()、sys_function.DATASOURCENAME。
返回值契约:同「流程回退」。
示例代码(终止 + 改文档状态 + 清理待办 + 通知作者/审批人):
(function () {
var doc = getCurrentDocument();
var docId = doc.getId();
var reason = doc.getItemValueAsString("终止原因") || "管理员终止";
// 权限校验
var user = getWebUser();
var role = user.getRole();
var roleName = role != null ? role.getName() : "";
if (roleName !== "管理员" && roleName !== "流程管理员") {
return "您没有终止流程的权限";
}
var flowProcess = getFlowProcess();
var flow = flowProcess.doViewByDocument(doc);
if (flow == null) {
return "流程不存在";
}
// 1) 终止
var params = createParamsTable();
params.setParameter("_attitude", reason);
flowProcess.doTerminate(flow, params, user);
// 2) 更新文档字段并落库
doc.findItem("流程状态").setValue("已终止");
doc.findItem("终止时间").setValue(format(new Date(), "yyyy-MM-dd HH:mm:ss"));
doc.findItem("终止人").setValue(user.getName());
getDocumentProcess().doUpdate(doc);
// 3) 清理待办
var dName = sys_function.DATASOURCENAME;
var delTask = " DELETE FROM T_TASK WHERE DOC_ID = '" + docId + "' AND STATE = 0";
updateByDSName(dName, delTask);
// 4) 取待处理审批人,发通知
var fsSQL = "select domainid,id from t_flowstatert where docid = '" + docId + "'";
var fs = findBySQL(fsSQL);
if (fs != null) {
var fsId = fs.getId();
var actorSQL = "select domainid,actor_id from t_actor where flowstatert_id = '" + fsId + "' and state = 0";
var actors = queryBySQL(actorSQL);
if (actors != null && actors.size() > 0) {
for (var it = actors.iterator(); it.hasNext();) {
var actorId = it.next().getItemValueAsString("actor_id");
sendMessage(actorId, "流程已被终止,无需继续处理。原因:" + reason);
}
}
}
// 5) 通知文档作者
var authorId = doc.getAuthor();
if (authorId != null && authorId.trim().length > 0) {
sendMessage(authorId, "您的流程已被终止。原因:" + reason);
}
return "流程终止成功";
})()
代码说明:
- flowProcess.doTerminate(flow, params, user) → iscript-guid/api → flow
- sendMessage(userId, content) → iscript-guid/functions → message-functions
- updateByDSName、findBySQL、queryBySQL → iscript-guid/api → database
- 待办表 T_TASK、执行人表 T_ACTOR、状态运行时表 T_FLOWSTATERT 见章首。
变体与扩展:
- 更新审批历史意见:在 doTerminate 后用「覆盖最后一条审批意见」的 setLastAttitude(docId, "流程终止:" + reason)。
- 更新流程状态字段:UPDATE T_FLOWSTATERT SET STATE = '已终止', STATE_LABEL = '已终止' WHERE ID = '...'。
- 记录监控日志:insert into tlk_流程监控日志 (...) values (...)。
常见坑:
- doTerminate 不可逆:终止后流程无法恢复;务必加权限校验、操作前确认。
- sendMessage 参数:第一参数是用户 id(不是登录名),第二参数是内容;批量发送时**每个用户单独发**,不要拼接 id。
- 清理待办:doTerminate 自身会处理一部分,但若发现待办仍残留,再用 DELETE FROM T_TASK 补刀——执行前**先 select 看一眼**。
- 顺序:先 doTerminate → 再 doUpdate 字段;如果先改字段再终止,可能被引擎覆盖。
- getAuthor() 可能返回 null(旧数据/匿名提交),用前判空—— GraalVM 差异。
流程干预操作速查(跳过 / 回退 / 终止 / 干预提交)¶
业务目标:把 simple-intervention-script(跳过/回退/终止)与 intervention-submit-monitor(干预提交)合并讲解——都是绕过正常流转、由脚本直接驱动流程引擎,**类似后台"流程监控"按钮**的能力。
写在哪儿:表单/视图 → 操作按钮 → 操作前置 / 操作后置。
触发时机与上下文:用户点操作按钮时;可用 getCurrentDocument()、getWebUser()、createParamsTable()。
返回值契约:前置非空串阻断、"" 通过;后置做副作用。流程状态由 flowProcess.doXxx(...) 改变。
干预 API 速查表¶
| 操作 | API | 关键参数 | 不可逆性 | 典型场景 |
|---|---|---|---|---|
| 跳过节点 | flowProcess.doSkipNode(flow, node, params, user) |
params.setParameter("_skipNode", "true") |
中(流转走但能再回退) | 某条件不需要某人审批 |
| 回退节点 | flowProcess.doBack(flow, node, params, user) |
params.setParameter("_attitude", "回退原因") |
高 | 退回申请人补材料 |
| 终止流程 | flowProcess.doTerminate(flow, params, user) |
params.setParameter("_attitude", "终止原因") |
极高(不可恢复) | 业务取消 |
| 干预提交 | flowProcess.doSubmit(flow, node, params, user) |
params.setParameter("_attitude", "管理员干预提交") |
中(直接跳到目标节点) | 流程加速、纠错 |
高层封装:
FLOW命名空间提供FLOW.interveneFlow(flowId, currentNodeId, nextNodeIds, attitude, user, doc)等单行 API,可在不取flowProcess/flow对象时直接调用。
公共片段:干预前的权限校验¶
// 所有干预脚本建议在开头跑一遍
function checkIntervenePermission() {
var user = getWebUser();
var role = user.getRole();
var roleName = role != null ? role.getName() : "";
return roleName === "管理员" || roleName === "流程管理员" || roleName === "系统管理员";
}
示例 1:跳过当前节点¶
(function () {
if (!checkIntervenePermission()) {
return "您没有流程干预权限";
}
var doc = getCurrentDocument();
var flow = getFlowProcess().doViewByDocument(doc);
if (flow == null) return "流程不存在";
var currentNode = flow.getCurrentNode();
if (currentNode == null) return "当前节点为空";
var params = createParamsTable();
params.setParameter("_skipNode", "true");
params.setParameter("_attitude", "自动跳过节点");
getFlowProcess().doSkipNode(flow, currentNode, params, getWebUser());
return "已跳过";
})()
示例 2:干预提交到指定节点(不经过中间审批)¶
(function () {
if (!checkIntervenePermission()) {
return "您没有流程干预权限";
}
var doc = getCurrentDocument();
var targetNodeId = "目标节点ID";
var flow = getFlowProcess().doViewByDocument(doc);
if (flow == null) return "流程不存在";
var targetNode = flow.getNodeById(targetNodeId);
if (targetNode == null) return "目标节点不存在";
var params = createParamsTable();
params.setParameter("_attitude", "管理员干预提交到节点:" + targetNode.getName());
getFlowProcess().doSubmit(flow, targetNode, params, getWebUser());
// 落库 + 监控日志
doc.findItem("干预操作").setValue("是");
doc.findItem("干预人").setValue(getWebUser().getName());
doc.findItem("干预时间").setValue(format(new Date(), "yyyy-MM-dd HH:mm:ss"));
getDocumentProcess().doUpdate(doc);
return "干预提交成功";
})()
示例 3:按金额阈值终止(自动干预)¶
(function () {
var doc = getCurrentDocument();
var amount = doc.getItemValueAsDouble("金额");
if (amount <= 100000) {
return ""; // 不触发
}
var flow = getFlowProcess().doViewByDocument(doc);
if (flow == null) return "";
var params = createParamsTable();
params.setParameter("_attitude", "金额超过 10 万上限,自动终止");
getFlowProcess().doTerminate(flow, params, getWebUser());
doc.findItem("流程状态").setValue("已终止");
getDocumentProcess().doUpdate(doc);
return "";
})()
代码说明:
- 全部 flowProcess.doSkipNode/doBack/doTerminate/doSubmit → iscript-guid/api → flow
- createParamsTable() / params.setParameter → iscript-guid/functions → system-functions
- getDocumentProcess().doUpdate(doc) 落库 → iscript-guid/api → doc
变体与扩展:
- 干预 + 更新审批历史意见:参考「覆盖最后一条审批意见」的 setLastAttitude(docId, "干预操作:跳过节点")。
- 干预 + 监控日志:updateByDSName(dName, "insert into tlk_流程监控日志 (...) values (...)")。
- 批量干预:for 循环 docIds,每个 docProcess.doView(docId) 后再干预——但**事务边界**要小心,逐条 try/catch。
- 回退 + 通知:参「流程回退」的"回退 + sendMessage"组合。
常见坑(本章重点):
- 干预类操作风险高、部分不可逆:doTerminate 终止后**无法恢复**;doBack/doSubmit 改变流程状态后也无法简单撤销。务必先在测试环境验证,并保留操作日志。
- 字段修改必须 doUpdate 落库:doc.findItem(...).setValue(...) 只改内存;干预脚本里改了字段必须 getDocumentProcess().doUpdate(doc),否则下次打开字段又变回原值。
- 权限校验不可省:干预脚本通常绑在操作按钮上,任何看到按钮的人都能触发;不校验角色=把"流程监控"权限下发给所有人。
- 顺序:setValue → doXxx(流程引擎动作)→ doUpdate;颠倒会被引擎覆盖。
- sendMessage 参数要对:第一参数是用户 id,不是登录名/姓名;第二参数是字符串内容;批量逐个发,不要拼 id。
- Java List 与 JS 数组混用:flow.getNodes() 返回 java.util.List,遍历用 .iterator() 或 .size()+.get(i);getParameterAsArray("_selects") 才是 JS 数组(用 .length)—— GraalVM 差异「Java List .size() vs JS 数组」。
- Packages.cn.myapps...:现存示例里 sys_function.DATASOURCENAME 是平台注入的全局对象,可以直接用;不要自己写 Packages.cn.myapps.xxx 引用内部类,改用 Java.type('java.util.X')。
视图伪提交待办¶
业务目标:在视图里选中若干待办,点一个按钮"伪提交"——更新文档/审批历史/执行人状态,但**不真的让流程往下走**。用于批量签收、特殊业务模拟提交。
写在哪儿:视图 → 操作按钮 → 操作前置 / 操作后置(ACTIVITY:BEFORE/ACTIVITY:AFTER)。属性名以按钮配置为准。
触发时机与上下文:用户在视图里勾选行 + 点按钮时;可用 getParameter("_selects")(选中行的待办 id,分号分隔)。
返回值契约:前置非空串阻断、"" 通过;后置返回字符串提示。
示例代码(批量伪提交,统计成功/失败条数):
(function () {
var selects = getParameter("_selects");
if (selects == null || selects.trim().length === 0) {
return "请选择待办任务";
}
var taskIds = selects.split(";"); // JS 数组,用 .length
var successCount = 0;
var failCount = 0;
var user = getWebUser();
var dName = sys_function.DATASOURCENAME;
var docProcess = getDocumentProcess();
for (var i = 0; i < taskIds.length; i++) {
var taskId = taskIds[i];
if (taskId == null || taskId.trim().length === 0) continue;
// 通过待办 id 取文档与 flowstatert id
var taskSQL = "select domainid,doc_id,flowstatert_id from t_task where id = '" + taskId + "'";
var task = findBySQL(taskSQL);
if (task == null) { failCount++; continue; }
var docId = task.getItemValueAsString("doc_id");
var fsId = task.getItemValueAsString("flowstatert_id");
var doc = docProcess.doView(docId);
if (doc == null) { failCount++; continue; }
// 1) 更新审批历史最后一条意见
var updHis = " UPDATE T_ACTORHIS SET [ATTITUDE] = '伪提交' ";
updHis += " WHERE DOC_ID = '" + docId + "' ";
updHis += " AND FLOWSTATERT_ID = '" + fsId + "' ";
updHis += " AND PROCESSTIME = (SELECT MAX(PROCESSTIME) FROM T_ACTORHIS WHERE DOC_ID = '" + docId + "' AND FLOWSTATERT_ID = '" + fsId + "')";
updateByDSName(dName, updHis);
// 2) 把当前用户的待处理执行人置为已处理
var updActor = " UPDATE T_ACTOR SET STATE = 1 ";
updActor += " WHERE FLOWSTATERT_ID = '" + fsId + "' ";
updActor += " AND ACTOR_ID = '" + user.getId() + "' ";
updActor += " AND STATE = 0";
updateByDSName(dName, updActor);
// 3) 更新文档字段(按需)
doc.findItem("处理状态").setValue("已伪提交");
doc.findItem("处理人").setValue(user.getName());
doc.findItem("处理时间").setValue(format(new Date(), "yyyy-MM-dd HH:mm:ss"));
docProcess.doUpdate(doc);
successCount++;
}
return "成功:" + successCount + ",失败:" + failCount;
})()
代码说明:
- getParameter("_selects") 取视图选中行的 id 串 → iscript-guid/functions → system-functions
- getDocumentProcess().doView(docId) / doUpdate(doc) → iscript-guid/api → doc
- updateByDSName → iscript-guid/api → database
- 待办表 t_task、执行人表 t_actor、历史表 t_actorhis 见章首。
变体与扩展:
- 单条伪提交:taskIds[0] 即可。
- 伪提交 + 通知:完成后 sendMessage(doc.getAuthor(), "您的文档已被伪提交")。
- 权限校验:开头加 if (!checkIntervenePermission()) return "无权限";(参考「流程干预操作速查」)。
常见坑:
- _selects 是分号字符串不是数组:必须 .split(";") 后用 JS 数组(.length);不要 selects.size()。
- STATE 字段类型:SQL Server 下 STATE 是关键字,必要时加方括号 [STATE]。
- 数据一致性:伪提交绕过流程引擎,需要自己保证 t_actor/t_actorhis/文档字段**同步更新**,否则下次打开状态会乱。
- 不要忘 doUpdate:改了 doc.findItem 必须落库。
- 批量循环里异常吞掉:建议 try { ... } catch (e) { failCount++; } 包住单条逻辑(GraalVM 支持 try/catch)。
节点审批 / 过期时限脚本(NODE_TIME_LIMIT)¶
业务目标:按表单字段动态算出"本节点审批应该在多久后超时",超时后平台自动提交到下一节点。
写在哪儿:流程 → 节点 → 审批时限脚本(属性 timeLimitScript,Label WORKFLOW:NODE_TIME_LIMIT)。挂载点说明见 iscript-guid/basic/where-to-use/flow/flow-node.md。
触发时机与上下文:计算节点时限时;可用 getCurrentDocument()、getItemValueAsDate("字段名")、getToday()、getDay(...)。
返回值契约:返回时长(日/时/分等产品约定,常以"日"为单位);或返回一个日期对象作为截止时间(按节点配置而定)。
示例代码(按"日期"字段算审批时限,缺省回退到今天):
(function () {
var starttime = getItemValueAsDate("日期");
if (starttime != null) {
return getDay(starttime); // 取该日期对应的"日"数作为时限
}
return getDay(getToday()); // 没填日期则按今天
})()
返回具体截止日期的写法:
(function () {
var starttime = getItemValueAsDate("结束日期");
if (starttime != null) {
return starttime; // 直接返回 Date 作为截止时刻
}
return getToday();
})()
代码说明:
- getItemValueAsDate(name) → iscript-guid/functions → doc-functions
- getToday() / getDay(date) / format(date, pattern) → iscript-guid/functions → date-functions
- 挂载点位置与触发时机 → iscript-guid/basic/where-to-use/flow/flow-node.md。
变体与扩展:
| 变体 | 改法 |
|---|---|
| 按角色给不同时限 | if (roleName === "管理员") return 1; else return 3; |
| 排除周末 | 用工作日历相关函数(如平台提供 getWorkingDays(...)),或自行遍历加 5/减 2 |
| 与"过期时限"配合 | 节点上另配"过期时限脚本",到达时限后触发自动提交/通知 |
常见坑:
- starttime != null 必须判空:用户没填日期时 getItemValueAsDate 返回 null,传给 getDay(null) 会报错。
- 返回类型要看节点配置:有的版本期待数字(天数),有的期待日期对象;先在该节点的"审批时限设置"里看说明。
- getDay(date) 的语义:返回的是该日期的"日号数"(1~31),不是"周几"也不是"毫秒数";按业务确认是否真的是你想要的——具体行为以 iscript-guid/functions → date-functions 为准。
章末常见坑汇总¶
| 类别 | 坑 | 解法 |
|---|---|---|
| 干预类 | doTerminate/doBack 不可逆 |
测试环境验证 + 权限校验 + 操作日志 |
| 干预类 | 改了字段不生效 | 必须在 doXxx 之后调 getDocumentProcess().doUpdate(doc) |
| 干预类 | 权限下放 | 干预脚本绑按钮后任何人都可触发,开头必须 getWebUser().getRole() 校验 |
| 干预类 | sendMessage 参数错 |
第一参数用户 id(非姓名)、第二参数字符串;批量逐个发 |
| 查询类 | queryBySQL 返回 null |
rows != null && rows.size() > 0 双重判断 |
| 查询类 | t_actor.state 漏判 |
经办人 state=0、已处理 state=1、跳过/终止 state=2 |
| 查询类 | 第一节点无上一节点 | SQL 返回 null,用前判空 |
| 数据类 | SQL 字符串拼接注入 | 用户输入的值要转义单引号或用参数化 |
| 数据类 | SQL Server 关键字 | ATTITUDE、STATE 用 [...] 括起来 |
| GraalVM | .equals / .length() |
改 === / !== 与 .length —— 见 getting-started.md#graalvm-差异 |
| GraalVM | Java List .length / JS 数组 .size() |
List 用 .size()+.get(i)+.iterator();JS 数组用 .length+下标 |
| GraalVM | 裸 java.util.X / importClass |
改 Java.type('java.util.X') |