
一个JSP打天下47KB万能表单引擎非科班野生程序员深耕政务信息化20年这套自研Java Web框架支撑过省级社保、跨省医保结算等核心民生系统。政务审批系统的核心是工作流但工作流的前端——表单——比工作流本身更复杂。这篇拆解一个47KB的JSP文件怎么扛起所有流程的所有表单全是政务场景踩坑后的实用解法。最后感谢豆包、智谱、OpenCode决策是我做的代码是我搓的文字是他们总结的。背景政务OA有几十个流程每个流程的表单不一样。请假申请、用章申请、预算审批、采购审批、车辆调度……每个表单字段不同、验证规则不同、不同环节可编辑的字段不同。一般做法是每个流程写一个JSP。问题是几十个流程就是几十个JSP维护成本高新增流程必须开发——业务人员画完BPMN还得等开发写表单同一流程不同环节显示不同——申请环节能填审批环节只读有些字段某些环节隐藏——每个环节一套JSP我的做法是所有流程的所有任务只走一个JSP文件——onlyFrom.jsp。核心思路表单定义不在JSP里在数据库里。onlyFrom.jsp启动时从数据库读取元数据动态生成HTML。每个表单有自己的字段定义、控件类型、代码表、默认值、HTML模板——全存在关系表里。请求流程任务 ↓ onlyFrom.jsp 启动 ↓ Java 代码段JSP顶部从数据库读取 ├── formds —— 表单定义标题、宽度、高度、类型、HTML模板 ├── fieldds —— 字段定义名称、类型、列数、代码表、默认值 ├── tableds —— 表单与数据表的绑定关系 ├── ellist —— 元素默认值列表 └── gridformsql —— 网格子表单的SQL ↓ 动态生成HTML JavaScript ↓ search() 调用 allbytaskid 一次加载所有数据 ↓ 表单渲染完成元数据驱动的表单生成Java 代码段从数据库加载元数据// onlyFrom.jsp 顶部StringsqlIdrequest.getParameter(id);formServiceImpl objnewformServiceImpl();DataStoredsnewDataStore();ds.setParameter(id,sqlId);DBUtil.BeginTrans(false);DataStoreformdsobj.selectform(ds);// 表单定义DataStorefielddsobj.selectfield(ds);// 字段定义DataStoretabledsobj.selecttable(ds);// 数据表绑定Listeldaoellistobj.getdefualt(ds,userlist,deptid);// 默认值Stringgridformsqlobj.getGridSql(ds);// 网格子表SQLDBUtil.EndTrans();6次数据库查询一次事务拿到渲染一个表单需要的全部元数据。动态生成代码表数据源字段类型coltpye决定了控件类型和数据的来源%for (int i 0; i fieldds.getRowset().getPrimary().size(); i) { if (5.equals(fieldds.getRowset().getrow(i).getItemStringValue(coltpye)) || 6.equals(fieldds.getRowset().getrow(i).getItemStringValue(coltpye))) {% temp String(%fieldds.getRowset().getrow(i).getItemStringValue(commens)%); temp dojo.fromJson(temp); var %fieldds.getRowset().getrow(i).getItemStringValue(name)% new browise.ds.DataStore(%fieldds.getRowset().getrow(i).getItemStringValue(name).toUpperCase()%, temp); dataCenter.addDataStore(%fieldds.getRowset().getrow(i).getItemStringValue(name)%); %} }%字段类型5单选、6多选的数据来自commens字段的JSON。字段类型7下拉框的数据来自代码表%for (int i 0; i fieldds.getRowset().getPrimary().size(); i) { if (7.equals(fieldds.getRowset().getrow(i).getItemStringValue(coltpye)) || (fieldds.getRowset().getrow(i).getItemStringValue(codename) ! null !.equals(fieldds.getRowset().getrow(i).getItemStringValue(codename)))) {% dataCenter.addDataStore(browise.Action.getCodeList( %fieldds.getRowset().getrow(i).getItemStringValue(codename).toUpperCase()%)); %} }%coltpye对照值控件类型数据来源1TextBox无2Textarea无3DateTextBox无4DateTextBox无5RadioButtonGroupcommensJSON6CheckBoxcommensJSON7ComboBox代码表codenameHTML模板数据库存储 vs 外部文件表单HTML有两种来源% String type1 formds.getRowset().getrow(0).getItemStringValue(type); if(3.equals(type1)) { // 类型3从外部文件读取 String html FileUtil.readJspFile( formds.getRowset().getrow(0).getItemStringValue(filepath)); out.println(html); } else { // 其他类型从数据库字段读取 String html String.valueOf(formds.getRowset().getrow(0).getItemValue(html_)); out.println(html); } %类型3适合复杂表单——HTML太长放数据库不方便维护用外部文件。简单表单直接存数据库字段。script_列表单插件系统这是万能表单最巧妙的设计。每个表单定义有一个script_列可以存任意JavaScript代码。万能表单渲染时原样输出这段代码%String sp String.valueOf(formds.getRowset().getrow(0).getItemValue(script_)); out.println(sp);%三个钩子函数万能表单在关键生命周期点通过try/catch调用外部函数// 数据加载完成后try{if(searchothers)searchothers();}catch(E){}// 保存之前try{if(saveothers)saveothers();}catch(E){}// 表单数据绑定完成后try{if(setothers)setothers();}catch(E){}try/catch是关键——如果表单没有定义这些函数不会报错安静跳过。这意味着每个表单可以在不修改万能表单模板的情况下注入自己的行为。比如一个采购审批表单需要在加载时自动算总价、保存前校验预算——这些逻辑写在script_字段里就行。字段级权限控制同一个表单不同环节可编辑的字段不同。申请环节所有字段可填科长审批时金额字段只读处长审批时只看意见框。权限加载allbytaskid接口返回froleDataStore里面是当前环节只读的字段列表。万能表单收到后逐个设置functionsetrolebydata(value){if(value.getRowSet().getRowCount()0)return;value.getRowSet().forEach(function(row){try{browise.byId(row.getItemValue(teId)).setDisabled(true);}catch(E){}});}字段权限的配置在otherSet.jsp管理界面里——管理员选择一个流程步骤勾选哪些字段只读存到t_from_readonly表。运行时根据当前任务ID查这张表。流程进度条表单顶部有一行文字显示流程走到哪了当前环节标红申请 —→ 科长审批 —→ [处长审批] —→ 局长审批方括号里的标红显示。实现很直接——后端返回字符串申请—→科长审批—→[处长审批]—→局长审批前端解析方括号functionsetmx(value){varsub_string1value.substring(0,value.indexOf([));varsub_string2value.substring(value.indexOf([)1,value.indexOf(]));varsub_string3value.substring(value.indexOf(])1);vartitlespan stylefont-size:14px;sub_string1/spanspan stylecolor:red;font-size:14px;sub_string2/spanspan stylefont-size:14px;sub_string3/span;document.getElementById(title_sw_01).innerHTMLtitle;// 按钮文字根据环节变化if(sub_string2.indexOf(复核)0){browise.byId(tj001).setLabel(复核);}if(sub_string2.indexOf(审批)0){browise.byId(tj001).setLabel(审批);}}细节按钮文字跟着环节变——申请环节显示提交复核环节显示复核审批环节显示审批。政务用户习惯看审批两个字不习惯看提交。孤儿流程自动清理用户打开了申请表单填了一半关掉了浏览器。此时流程已经启动了有了一个实例但第一个任务没完成。如果不清理数据库里会积累大量只开了个头就没下文的流程实例。window.onunloaddeletetask;%if(candelete){%isdeletetrue;%}%// 如果不是第一环节不能自动删除%if(!isfirst){%isdeletefalse;%}%functiondeletetask(){if(isdelete){vartaskid%taskid%;vardcnewbrowise.ds.DataCenter();varURLbrowise.WEB_APP_NAME/BusinessAction?BusinessflowActiondeleteinsjsessionidsessionidtaskidtaskid;vardata{url:URL,sync:false,load:function(dc1){}};browise.Action.requestData(data,dc,true);}}三重判断window.onunload触发 →isdelete为 true前端允许删除 →isfirst为 true第一个环节才删→candelete为 true后端允许删除。四个条件全满足才发删除请求。中间环节的审批关掉浏览器不会触发删除。工作时间校验政务系统有个不成文的规矩正式公文不应该在非工作时间提交。不是技术限制是合规要求。// 判断当前时间是否为工作时间不是则提示给经办人functionQueryTime(){vardcnewbrowise.ds.DataCenter();varURLwebRootDir/BusinessAction?BusinessExternalActionQueryTime;vardata{url:URL,sync:false,load:function(dc1){varmessagedc1.getDataStore(message);if(dc1.getCode()0){MessageBox.alert({title:提示,message:message.getParameter(message)});}}}browise.Action.requestData(data,dc,true);}注意注释里的日期——2022.06.17团队里一位年轻同事加的功能。不做技术限制只是提醒——弹个窗告诉你当前是非工作时间确认后照样能提交。务实。一键意见functionyjchoose(){varcyyjbrowise.byId(cyyj).getValue();browise.byId(task_suggestion).setValue(cyyj);if(cyyj同意){browise.byId(spztGroup).setValue(1);// 通过}else{browise.byId(spztGroup).setValue(0);// 不通过}}下拉框选同意自动填意见框自动选通过。选不同意自动填自动选不通过。政务审批90%都是点同意这个快捷操作省了审批人大量时间。万能表单的完整生命周期用户点击待办任务 ↓ cform.jsp包装层include onlyFrom.jsp ↓ onlyFrom.jsp 启动 ├── Java段从数据库加载表单元数据formds/fieldds/tableds ├── 生成代码表DataStoreradio/checkbox/combobox ├── 设置 window.onunload deletetask孤儿清理 ├── 输出 script_ 列的JavaScript表单插件 ↓ dojo.addOnLoad → search() ├── 调用 allbytaskid一次拿6组数据 ├── setmx() —— 显示流程进度条 ├── setrolebydata() —— 设置字段只读 ├── sethqyj() —— 加载会签意见 ├── setNext() —— 加载下一步选项 ├── 绑定审批历史 ├── try { searchothers() } —— 表单插件钩子 └── try { QueryTime() } —— 工作时间提醒 ↓ 用户操作提交/退回/作废/办结/会签/转办/暂存/打印 ↓ 关闭窗口 → deletetask()如果是第一环节且允许删除清理孤儿实例为什么这样做一个JSP扛起所有表单核心收益是新增流程不需要写JSP——业务人员在管理界面配置表单定义、字段、布局系统自动渲染维护成本降一个量级——改一个字段不需要改JSP改数据库配置就行插件系统保证了灵活性——通用逻辑在万能表单里特殊逻辑在script_列里互不干扰全流程统一体验——进度条、审批历史、常用意见、字段权限——所有表单行为一致代价是47KB的JSP文件确实太大了。1546行代码Java和JavaScript混在一起维护起来不轻松。但比起维护几十个JSP文件一个47KB的万能模板反而更可控。决策原则把变化的东西从代码移到数据。表单字段会变、布局会变、验证规则会变——这些东西不应该硬编码在JSP里。存在数据库里运行时动态生成新增一个流程就是新增几行配置数据。script_列的插件系统是补充——通用的走元数据特殊的走代码两条路都能到。你的项目里动态表单是怎么做的是写死JSP还是元数据驱动欢迎评论区聊聊。系列导航回到总纲政务工作流实战二BPMN画不出来的七个设计决策上一篇综述下一篇数据库配拦截器不用改BPMN作者许彰午| 非科班野生程序员深耕政务信息化20年标签#Java #Activiti #工作流 #动态表单 #元数据驱动 #插件系统 #政务信息化