随时会签:审批人临时拉人商量
非科班野生程序员,深耕政务信息化20年,这套自研Java Web框架支撑过省级社保、跨省医保结算等核心民生系统。上一篇系列文写了"外挂会签"——不用Activiti的multiInstance,用独立子流程+关联表实现会签。这篇是前传和补充:从前后端完整链路看"处长审批到一半临时想问纪检科意见"这种BPMN画不出来的需求怎么落地。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。
背景
Activiti的multiInstance(多实例)可以实现并行审批。但有几个问题:
- 必须预先设计在BPMN里——流程定义时就确定好哪些环节需要会签
- 参与者写死——运行时不能动态改变谁参与
- 不能临时发起——审批人走着走着突然想拉人商量,BPMN里没画这个节点就做不了
政务场景的真实情况是:处长审批到一半,觉得这个事项需要征求纪检科意见。这个决定是运行时做的,不是设计时能预见的。
核心思路
不用Activiti的multiInstance。在万能表单里放一个"会签"按钮,点击后:
- 启动一个独立的Activiti流程实例(
key=hq) - 在
T_HJS_s关联表里记录父子流程关系 - 会签发起人选择参与者,每个参与者独立审批
- 主流程等待所有人签完才能继续
主流程 T_HJS_s 表 会签子流程 ┌──────────────┐ ┌───────────────┐ ┌────────────┐ │ 科长审批 │ │ proc_inst_id_ │ │ 参与者A │ │ 处长审批 →───┤──pre_proc │ pre_proc_inst │←───────── 子流程实例A │ │ _inst_id_ │ │ │ │ │ 点击"会签" │ │ │←───────── 子流程实例B│ 参与者B │ │ 局长审批 │ └───────────────┘ └────────────┘ └──────────────┘ 不修改BPMN 关联父子 独立流程定义前端:一个按钮触发
onlyFrom.jsp 中的会签按钮
functionhq(){vards=browise.byId("...").getBinding().getDataStore();varmatter_name=ds.getRowSet().getRow(0).getItemValue("matter_name");if(!matter_name||matter_name=="null"){matter_name="会签";}varURL=webRootDir+"/BusinessAction?Business=hq&Action=starthq"+"&key=hq&userid=<%=userid%>"+"&preinsid=<%=insid%>&matter_name="+matter_name;vardialog=DialogUtil.showDialog({title:'发起会签',url:encodeURI(URL),height:document.body.clientHeight*0.8,width:document.body.clientWidth*0.8,isClose:true,resizable:false,dialogData:{},modal:true,isExpand:false,onComplete:function(){gethqinfo();// 会签对话框关闭后,刷新会签状态}});}点击按钮 → 打开一个对话框 → 对话框里是会签发起人选择参与者的界面。
会签发起人界面(onlyFrom_hq_sq.jsp)
发起人看到的是:
- 主流程的表单数据(只读)
- 一棵用户树(选择会签参与者)
- 提交按钮
// onlyFrom_hq_sq.jsp 的提交functionsave(){varselectedNodes=browise.byId('persistTree').getSelectedNodes();if(selectedNodes.length<1){MessageBox.alert({title:"提示信息",message:'请选择任务接收人!'});return;}// 遍历选中的参与者,为每个人启动一个会签任务for(vari=0;i<selectedNodes.length;i++){varuserid=selectedNodes[i].getData().psn_id;// ... 收集参与者列表}// 调用 completeS,后端为每个参与者启动子流程}会签参与者界面(onlyFrom_hq.jsp)
每个会签参与者在待办列表里看到一个会签任务。打开后看到的是:
- 主流程的表单数据(只读)
- 通过/不通过选择
- 意见填写框
// onlyFrom_hq.jsp —— 加载主流程表单数据functionseach(){// 关键:insid 是子流程实例ID,preinsid 是主流程实例IDvarURL=webRootDir+"/BusinessAction?Business=flow&Action=binddataG"+"&taskid=<%=taskid%>&insid=<%=preinsid%>&formid=<%=sqlId1%>";vardata={url:URL,sync:false,load:function(dc1){vards=dc1.getSingleDataStore();varrow=ds.getRowSet().getRow(0);if(!row){row=ds.getRowSet().addRow();}// 绑定数据到表单varfrom01=browise.byId("...");from01.getBinding().bind(row);// 所有字段设为只读dojo.forEach(from01.getDescendants(),function(widget){try{widget.setDisabled(true);}catch(E){}});}};browise.Action.requestData(data,dc,true);}关键点:参与者看的是主流程的表单数据(通过preinsid反查),但所有字段只读。参与者只需要选择通过/不通过、填写意见。
提交时:
functionsave(){vardc=newbrowise.ds.DataCenter();dc.setParameter("taskid","<%=insid%>");dc.setParameter("insid","<%=insid%>");// 子流程实例IDdc.setParameter("preinsid","<%=preinsid%>");// 主流程实例IDdc.setParameter("pass",browise.byId("PASS").getValue());dc.setParameter("cause",browise.byId("CAUSE").getValue());varURL=webRootDir+"/BusinessAction?Business=hq&Action=completeP";browise.Action.requestData(data,dc,true);}后端:独立流程 + 关联表
starthq() —— 启动会签
publicDataCenterstarthq(DataCentercenter,...)throwsException{Stringkey=request.getParameter("key");// "hq"Stringuserid=request.getParameter("userid");Stringpreinsid=request.getParameter("preinsid");// 主流程实例IDStringmatter_name=request.getParameter("matter_name");DBUtil.BeginTrans(null,false);// 1. 启动一个全新的Activiti流程实例HashMap<String,String>tasks=processService.start(key,userid);// 2. 查主流程的表单keyStringformkey1=processService.getTaskFormInsId(preinsid);// 3. 在 T_HJS_s 关联表里保存父子关系DataCenterdc=newDataCenter();DataStoreds=newDataStore("ds");ds.setParameter("ins","T_HJS_s");ds.setParameter("up","T_HJS_s");Rowrow=ds.getRowset().add();row.setItemValue("proc_inst_id_",tasks.get("insid"));// 会签子流程row.setItemValue("pre_proc_inst_id_",preinsid);// 主流程row.setItemValue("matter_name",matter_name+"会签");row.set_t(1);CommonSQLcontrolsqlcl=(CommonSQLcontrol)BeanFactory.getBean("CommonSQL");sqlcl.CommonSaveofbyseach(dc,null,null);DBUtil.EndTrans();// 4. 跳转到会签发起人界面response.sendRedirect(request.getContextPath()+"/pages/workflow/hq/cform_hq_sq.jsp?"+"&id="+formkey+"&taskid="+tasks.get("id")+"&insid="+tasks.get("insid")+"&preinsid="+preinsid+"&id1="+formkey1);returnnull;}流程:
processService.start("hq", userid)启动一个全新的流程实例(key是hq)- 在
T_HJS_s表里插入一行,proc_inst_id_是子流程,pre_proc_inst_id_是主流程 - 跳转到发起人选择参与者的界面
completeP() —— 会签参与者提交
publicDataCentercompleteP(DataCentercenter,...)throwsException{DBUtil.BeginTrans(null,false);Stringinsid=String.valueOf(center.getParameters("insid"));// 子流程Stringpreinsid=String.valueOf(center.getParameters("preinsid"));// 主流程Stringpass=String.valueOf(center.getParameters("pass"));Stringcause=String.valueOf(center.getParameters("cause"));// 1. 保存会签审批信息到 T_HJ_s 表DataStoreds=newDataStore("ds");ds.setParameter("ins","T_HJ_s");Rowrow=ds.getRowset().add();row.setItemValue("proc_inst_id_",insid);row.setItemValue("pre_proc_inst_id_",preinsid);row.setItemValue("user_id",userid);row.setItemValue("pass",pass);row.setItemValue("cause",cause);row.setItemValue("DEPT_ID",dept_id);row.set_t(1);sqlcl.CommonSaveofbyseach(dc,null,null);// 2. 推进子流程processService.passProcessNo(center,request,response);// 3. 检查子流程是否全部完成HashMap<String,String>map=processService.currTask(insid);if(map==null){// 子流程没有活动任务了 → 会签全部完成 → 通知发起人Stringuserid1=processService.getFirstTaskAssignee(taskid);Stringtitle=processService.getInsTitle(insid);MyMsgdao=newMyMsg();dao.setPid(insid);dao.setUserid(userid1);dao.setMsg("事项:"+title+"已办结!");insertmsg(dao);}DBUtil.EndTrans();returnnewdc;}会签参与者提交后:
- 审批结果(通过/不通过/意见)存到
T_HJ_s表 - 子流程推进一步(
passProcessNo) - 检查这个子流程是否结束了——如果结束了,给发起人发一条站内消息
gethqinfo() —— 查询会签状态
主流程的万能表单调这个方法,看会签是否完成:
// flowControl.javahq dao=newhq();dao.setSearchMethod("selectC");// 查剩余未完成数dao.setPreProcInstId(insid);List<hq>list=(List<hq>)dao.search();intcount=Integer.parseInt(list.get(0).getTaskId());booleancanNext=true;if(count>0)canNext=false;// 还有人没签完dao.setSearchMethod("selectAll");// 查所有会签记录list=(List<hq>)dao.search();for(inti=0;i<list.size();i++){// 填充:部门、姓名、结果(通过/不通过/未签批/放弃)、意见、时间if("0".equals(list.get(i).getPass())){canNext=false;// 有人不通过,也不能继续}}if(canNext){newdc.setCode(0);// 可以继续走主流程}else{newdc.setCode(-1);// 会签未完成,主流程等待}四种会签状态:
| 状态 | 含义 | 判断条件 |
|---|---|---|
| 未签批 | 还没处理 | enddate为空 |
| 放弃 | 放弃会签 | procInstId为 null |
| 通过 | 签了同意 | pass = "1" |
| 不通过 | 签了不同意 | pass = "0" |
前端:会签未完成时禁用提交按钮
// onlyFrom.jsp 中 gethqinfo() 的回调functiongethqinfo(){varURL=webRootDir+"/BusinessAction?Business=flow&Action=gethqinfo&insid=<%=insid%>";vardata={url:URL,sync:false,load:function(dc1){vards_hqjij=dc1.getSingleDataStore();if(ds_hqjij.getRecordCount()<1){browise.byId("task_hqyij").hide();// 没有会签记录,隐藏会签区域}else{browise.byId("task_hqyij").getBinding().setDataStore(ds_hqjij);browise.byId("task_hqyij").show();}if(dc1.getCode()=='0'){browise.byId("tj001").setDisabled(false);// 会签完成,可以提交}else{browise.byId("tj001").setDisabled(true);// 会签未完成,禁用提交}}};browise.Action.requestData(data,dc,true);}审批人点了"会签"按钮后,提交按钮立即变灰。只有所有人签完(或有人放弃),提交按钮才恢复。这是前端做的互锁,后端的canNext判断是兜底。
完整流程时序
1. 处长审批中,点击"会签"按钮 ↓ 2. 前端 hq() → 打开会签对话框 ↓ 3. 后端 starthq() → 启动独立流程实例 → 存 T_HJS_s 关联表 ↓ 4. 会签发起人界面 → 选择参与者(用户树上勾选)→ 提交 ↓ 5. 后端 completeS() → 为每个参与者分配子流程任务 ↓ 6. 每个参与者的待办列表出现会签任务 ↓ 7. 参与者打开会签任务 → 看到主流程表单(只读)→ 选通过/不通过 + 填意见 → 提交 ↓ 8. 后端 completeP() → 保存审批结果到 T_HJ_s → 推进子流程 ↓ 9. 所有参与者完成 → 子流程结束 → 通知发起人 ↓ 10. 主流程的 gethqinfo() 返回 code=0 → 提交按钮恢复 → 处长继续审批为什么这样做
为什么是独立流程而不是multiInstance?
- 不需要预定义——
multiInstance要在BPMN里画节点,我们的会签是运行时临时发起的 - 参与者动态指定——发起人当场选人,不是预定义的
- 加签减签改数据库——在
T_HJS_s表加减记录就行 - 任何节点都能发起——不管主流程走到哪一步,都能点"会签"
为什么用关联表而不是流程变量?
Activiti的流程变量绑定在流程实例上。会签的子流程和主流程是不同的流程实例,流程变量不共享。用数据库表关联最简单直接——pre_proc_inst_id_一关联,查询方便,不会丢失。
决策原则
会签是运行时的动作,不是设计时的约束。
BPMN定义的是流程的骨架——先A再B再C。但A到B之间"需不需要拉人商量",这个决定不应该在设计时就定死。放在运行时,让审批人自己决定,才是政务场景需要的灵活性。
你的项目里会签是怎么做的?是multiInstance还是其他方案?有没有遇到过"临时想拉人商量"的需求?欢迎评论区聊聊。
系列导航:
- 回到总纲:政务工作流实战(二):BPMN画不出来的七个设计决策
- 上一篇:“相对部门”:政务OA最绕的审批路由
- 下一篇:流程串流程:父子关联与数据回填
作者:许彰午| 非科班野生程序员,深耕政务信息化20年
标签:#Java #Activiti #工作流 #会签 #外挂子流程 #政务信息化