跳转至

流程查询、审批历史与人工干预

本章面向**已经会基本流程脚本(审批人、路径条件)**的开发者,解决三类问题:

  1. 查询——取流程信息、当前/上一节点、审批历史、当前待办经办人;
  2. 覆盖——改最后一条审批意见、判断当前用户是不是经办人;
  3. 干预——脚本里直接 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 只读
覆盖最后一条审批意见 操作后置 updateByDSNameT_ACTORHIS/T_RELATIONHIS 中(直接改系统表)
判断当前用户是否经办人 校验 / 隐藏 / 过滤 SQL 查 t_actor 只读
流程回退(上/指定/首节点) 操作前置 / 后置 flowProcess.doBack 高、不可逆
强制终止 + 清理待办/通知 操作前置 / 后置 flowProcess.doTerminate 高、不可逆
干预:跳过 / 回退 / 终止 / 干预提交 操作前置 / 后置 doSkipNode/doBack/doTerminate/doSubmit 高、部分不可逆
视图伪提交待办 视图操作按钮 getParameter("_selects") + doUpdate 中(绕过正常流转)
节点审批 / 过期时限 WORKFLOW:NODE_TIME_LIMIT getItemValueAsDategetTodaygetDay

涉及的系统表: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_actorwhere 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-functionsiscript-guid/api → doc - getWebUser()iscript-guid/api → curruser - format(date, pattern)iscript-guid/functions → date-functions

变体与扩展: - 批量取参数getParamsTable() 拿到 ParamsTable 后用 .getParameterAsString("_attitude")。 - 取目标节点对象:拿到 _targetNodeflow.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_ACTORHIST_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.state0=待处理、1=已处理、2=已跳过/已终止。 - 布尔契约统一:true = 隐藏/只读生效(详见 getting-started.md「返回值契约一览」)。

变体与扩展

变体 改法
是否**当前节点**经办人 关联 t_flow_nodenode_name = stateLabelnode_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_actorflowstatert_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 - updateByDSNamefindBySQLqueryBySQLiscript-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/doSubmitiscript-guid/api → flow - createParamsTable() / params.setParameteriscript-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),否则下次打开字段又变回原值。 - 权限校验不可省:干预脚本通常绑在操作按钮上,任何看到按钮的人都能触发;不校验角色=把"流程监控"权限下发给所有人。 - 顺序setValuedoXxx(流程引擎动作)→ 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 - updateByDSNameiscript-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 关键字 ATTITUDESTATE[...] 括起来
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')