1. 明细表的“显示顺序”到底由谁决定
做泛微OA流程开发的人,基本都会遇到一个绕不过去的需求:流程里的明细表,用户录入时是东一行西一行,提交之后领导打开一看,顺序乱七八糟,于是要求“提交后按金额从大到小排”“按物料编码升序排”“按费用发生日期排”。看起来只是一个小小的显示问题,但真动手做的时候,很多人第一次都是懵的——因为在泛微的表单设计器里,你找不到一个叫“明细表排序规则”的开关。
我先把结论摆出来:泛微OA流程表单里的明细表,在流转页面和归档查看页面上呈现的行顺序,本质上就是数据库里明细记录的物理主键顺序,也就是id的升序。用户录入的第一行id最小,最后一行id最大,页面上就按这个顺序从上往下铺开。你在表单上给明细行配了什么“序号”字段,那只是一个普通字段而已,它填的是几,跟这一行排第几,没有任何绑定关系。
所以“让明细表按规则排列”这件事,绕不开一个核心认知:行的位置你改不了,但行里装的内容你可以换。这句话是后面所有方案的地基。想明白这一点,整个实现思路就通了——我们不重排行,我们重排“内容”,让按id排好的第一行,装上按规则应该排第一的那份数据。用户在页面上看到的就是排好序的结果,浑然不觉底层动过手脚。
这个思路听起来有点“偷梁换柱”,但它在泛微这套体系里是最稳的。原因很实在:明细行的id被太多东西引用了。附件表、签字意见、子表明细、外部接口的回写记录,全都是靠这个id去关联的。你要是图省事,把明细行删掉再按新顺序重新插一遍,表面上顺序对了,附件全丢了,子表关联全断了,后面业务部门来找你的时候,问题就不是排序这么简单了。
提示:如果你接手的是一个已经在跑的流程,动手前先在测试环境复制一份流程和表单,别在生产上直接改。明细表重排属于“改数据”而不是“改配置”,出问题很难回滚。
1.1 从数据库里把明细表的真实结构看清楚
动手之前,先把物理表摸清楚,这一步不能省。泛微流程表单的明细表命名规则非常规整,主表叫formtable_main_数字,明细表在主表名后面挂_dt1、_dt2、_dt3,数字对应表单上明细表的顺序编号。比如主表是formtable_main_128,那它的第一个明细表就是formtable_main_128_dt1。
明细表里的字段分两类。一类是系统字段,通常有id(自增主键)、mainid(指向主表formtable_main_128的id)。另一类是你自己建的 业务字段,字段名就是你建字段时填的英文名,比如wlmc(物料名称)、sl(数量)、dj(单价)、je(金额)。
我一般会先跑一条desc或者查一下表结构,把字段名和类型都列出来:
-- 以 MySQL 为例,其他库换成对应的元数据查询 SHOW COLUMNS FROM formtable_main_128_dt1;这一步最容易被跳过的原因是,很多人以为自己记得字段名。实际结果是,表单上显示“金额”,字段名可能是je,也可能是money,还可能是hsje。字段名写错一个字母,Action 跑完不报错,但值全写串了,这种事故排查起来非常痛苦。我吃过一次亏,dj和dj1两个字段长得太像,一晚上没睡好。
顺便确认一下主表的关联方式。主表formtable_main_128上有一个requestid字段,存的是流程请求 ID。Action 里拿到的是requestid,要用它反查主表id,再拿主表id去查明细表mainid,这条链路要捋顺。
1.2 为什么不能直接给明细表加个 order by
有朋友会问,既然是查出来展示的,那我在查询的时候加个order by je desc不就完了?这个想法很自然,但在泛微的标准流程表单里行不通。流程表单的明细渲染走的是平台内置的渲染逻辑,SQL 是底层拼的,order by固定挂在主键上,你在表单设计器里没有入口去改它。
有些版本的表单属性里确实能看到一些和明细表相关的显示设置,但那些基本是控制列宽、行高、是否允许增删行的,跟行序没关系。建模模块里的表格视图倒是能配排序,但那是建模,不是流程表单,两者不是一回事。所以别在这条路上耗时间,我见过有人翻了半天文档想找那个“排序设置”,最后发现根本不存在。
真正能落地的路子只有几条,下面挨个拆。
2. 四条实现路线的取舍与选型逻辑
排序这个需求,看起来只有一个目标,但落地方式差别很大,选错了路线后期维护成本会翻倍。我把实际项目里用过的四种方式摆出来,各自的适用边界说清楚,你对着自己的场景挑。
选型的判断维度其实就三个:排序要不要持久化、明细行有没有附件和子表关联、明细数据量大不大。这三个问题问完,答案基本就定了。
2.1 前端 JS 重排:见效快,但只解决“当下”
前端方案的逻辑很直接:在用户点提交按钮之前,用 JavaScript 把明细表里所有行的值读出来,在浏览器内存里排好序,再按顺序一行一行回填到表单上。因为回填发生在提交之前,最终写进数据库的顺序就是排好序的顺序。
这个方案最大的优点是快,不用碰后端,不用重新部署,改一个表单的 JS 就能上线,验证起来也直观。缺点同样明显:它依赖“用户点提交”这个动作。如果流程后续有退回、转办、会签、加签,用户重新打开表单再提交一次,JS 会不会再跑一遍?跑了会不会把已经排好的又打乱?这些都要考虑。而且回填动作会触发字段联动和计算公式重算,明细表上如果挂了复杂的计算逻辑,很容易算出诡异的数。
我的看法是,前端方案适合“一次性、低风险、明细表结构简单”的场景,比如一个纯文本的清单,没有附件、没有子表、没有公式。稍微复杂一点的,别用它。
2.2 后端 Action 重写行内容:稳,是主力方案
这是我最推荐的方案,也是实际项目里用得最多的。思路是在流程节点的“提交后”Action 里,用 JDBC 把明细数据查出来,按规则排好,再按id升序把内容回写到原来的行上。
它稳在哪儿?第一,数据已经在库里了,读的是最终态,不存在“用户改了一半”的问题。第二,不管流程被退回几次、转办几次,每次提交都会重新跑一遍,而排序本身是幂等的,跑一百次结果都一样。第三,附件、子表关联全都不受影响,因为你只改业务字段,id和mainid一个都没动。
代价是需要写 Java 代码、需要注册 Action、需要重新部署。对纯实施人员来说门槛略高,但如果团队里有开发资源,这就是标准答案。
2.3 加排序号字段 + 展示层排序:改造成本最低
如果需求方的真实诉求不是“改变存储顺序”,而是“我想看到一个有序的列表”,那第三条路更划算:在明细表上加一个隐藏的排序列,提交时把序号算好写进去,然后在展示环节按这个序号排。
展示环节可以是流程归档后的自定义页面,可以是建模里配的一个报表,也可以是导出 Excel 时用脚本排。这种方式的好处是完全不动原始数据,风险最低,随时可以回退。坏处是流程流转页面里看到的还是老顺序,如果领导就是在流转页面上看,这个方案满足不了。
2.4 自定义页面单独呈现:适合“只读汇总”场景
有些流程的明细表数据量很大,几十上百行,在表单里排不排其实意义不大,业务方真正需要的是一个独立的查询页面。这时候干脆跳出表单,做一个自定义的查询页或者报表,SQL 里想怎么order by就怎么order by,还能加分页、加筛选、加导出。
这条路适合归档后的统计分析场景。缺点是它解决不了“流程在流转中就要看到排序结果”的问题。
2.5 四条路线的横向对比
| 方案 | 持久化 | 对附件/子表的影响 | 实施门槛 | 适用场景 |
|---|---|---|---|---|
| 前端 JS 重排 | 是(提交前生效) | 无(不删行) | 低 | 明细简单、无公式、一次性 |
| 后端 Action 重写 | 是(最彻底) | 无(只 update 内容) | 中 | 绝大多数流程场景,首选 |
| 排序号字段 + 展示层 | 否(原数据不变) | 无 | 低 | 只读统计、导出、报表 |
| 自定义页面查询 | 否 | 无 | 中 | 大数据量归档查询 |
表格里能看出来,后端 Action 是唯一一个“既彻底又安全”的方案。下面重点讲它怎么落地。
3. 后端 Action 的完整落地过程
这一节是重头戏,我把从准备工作到代码写完、再到注册上线的全过程拆开讲。中间会带上我实际踩过的一些细节,这些在官方文档里基本找不到。
先说清楚整体流程:拿到requestid→ 查主表拿mainid→ 按mainid查明细所有行(按id升序,这是原始位置)→ 复制一份按业务规则排序 → 把排序后的内容依次回写到原始位置上 → 提交事务。
3.1 前置准备:把物理表和字段摸清楚
前面提过要desc表结构,这里再补两个容易漏的点。
第一,确认明细表的行数上限。泛微表单明细表在表单设计时有个“最多行数”的设置,实际库里可能有历史遗留的超限数据。写代码时别假设行数,用List动态装。
第二,确认哪些字段是业务字段、哪些是系统字段。id、mainid绝对不能进回写列表。另外,如果你的明细表上配置了“附件上传”类型的字段,那个字段存的是附件关联 ID,也属于不能乱动的范畴——虽然重写它不会立刻出问题,但会让附件挂错行。稳妥做法是只回写那些纯业务字段,附件字段、关联字段统统跳过。
我一般的做法是先跑一条查询看看真实数据长什么样:
SELECT id, mainid, wlmc, ggxh, sl, dj, je FROM formtable_main_128_dt1 WHERE mainid = 10086 ORDER BY id;把结果贴到表格里对一眼,确认字段名和你想的一致,再往下写代码。这一步花两分钟,能省掉后面两小时的排查。
3.2 排序规则怎么写才不出错
排序规则是需求的核心,也是最容易出问题的地方。我列几个高频的坑。
坑一:字符串排序和数字排序不是一回事。如果金额字段在数据库里是varchar类型(泛微建字段时如果选了“单行文本”就是字符串),order by je desc出来的结果是"9"排在"10"前面,因为字符串按字典序比。所以排序前必须转成数值类型,SQL 里用CAST(je AS DECIMAL(18,2)),Java 里用BigDecimal兜底。
坑二:空值的位置。有的行金额没填,NULL或者空字符串。NULL在order by desc里默认排最前(不同数据库行为不同),空字符串会当成 0 参与比较之后又和 0 混在一起。稳妥的做法是统一用NVL(je, '0')或者 Java 里判空转BigDecimal.ZERO。
坑三:多字段排序的优先级。需求经常是“先按金额降序,金额相同的按日期升序,还相同的按录入顺序”。这个在 Java 里用Comparator链式写最清楚,比在 SQL 里堆order by a desc, b asc, c asc好维护,因为 Java 里能加自定义比较逻辑。
坑四:中文字段的排序。如果按物料名称这类中文字段排,数据库的字符集和排序规则会影响结果,中文一般按拼音或者按编码排,跟用户直觉可能不一致。这种需求最好提前跟业务方确认清楚“按什么排”,别自己拍脑袋。
坑五:排序字段本身就是“序号”。如果明细表里有个用户手填的序号列,你拿它当排序依据,那结果就是用户填什么顺序就是什么顺序,等于没排。要排就得用有业务含义的字段。
3.3 Java 实现的完整代码
下面的代码是一个可以直接套用的模板。用的是泛微 e-cology 的Action接口和RecordSet,数据库访问走平台自带的连接池,不用自己配数据源。
package com.example.workflow; import weaver.conn.RecordSet; import weaver.general.Util; import weaver.interfaces.workflow.action.Action; import weaver.soa.workflow.request.RequestInfo; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 流程提交后,按规则重排明细表内容 * 注意:只重写业务字段,不动 id / mainid / 附件关联字段 */ public class DetailSortAction implements Action { /** 主表物理表名 */ private static final String MAIN_TABLE = "formtable_main_128"; /** 明细表物理表名 */ private static final String DT_TABLE = "formtable_main_128_dt1"; /** 参与排序并回写的业务字段,按实际字段名填 */ private static final String[] BIZ_COLUMNS = {"wlmc", "ggxh", "sl", "dj", "je"}; /** 主排序字段 */ private static final String SORT_COLUMN = "je"; /** 次排序字段,为空则不做二级排序 */ private static final String SORT_COLUMN_2 = "rq"; @Override public String execute(RequestInfo request) { String requestId = Util.null2String(request.getRequestid()); if ("".equals(requestId)) { return ""; } RecordSet rs = new RecordSet(); String mainId = ""; try { // 第一步:requestid -> 主表 id rs.executeQuery("select id from " + MAIN_TABLE + " where requestid = ?", requestId); if (rs.next()) { mainId = Util.null2String(rs.getString("id")); } if ("".equals(mainId)) { return ""; } // 第二步:按 id 升序读出所有明细行,这个顺序就是页面上的原始位置 StringBuilder colStr = new StringBuilder("id"); for (String c : BIZ_COLUMNS) { colStr.append(",").append(c); } String sql = "select " + colStr + " from " + DT_TABLE + " where mainid = ? order by id"; rs.executeQuery(sql, mainId); List<Map<String, String>> origin = new ArrayList<>(); while (rs.next()) { Map<String, String> row = new HashMap<>(); row.put("id", Util.null2String(rs.getString("id"))); for (String c : BIZ_COLUMNS) { row.put(c, Util.null2String(rs.getString(c))); } origin.add(row); } if (origin.size() <= 1) { return ""; } // 第三步:复制一份,按业务规则排序 List<Map<String, String>> sorted = new ArrayList<>(origin); sorted.sort(buildComparator()); // 第四步:把排序后的内容,按原始 id 顺序回写 StringBuilder setStr = new StringBuilder(); for (int i = 0; i < BIZ_COLUMNS.length; i++) { if (i > 0) { setStr.append(","); } setStr.append(BIZ_COLUMNS[i]).append("=?"); } String updateSql = "update " + DT_TABLE + " set " + setStr + " where id=?"; for (int i = 0; i < origin.size(); i++) { String targetId = origin.get(i).get("id"); Map<String, String> src = sorted.get(i); Object[] params = new Object[BIZ_COLUMNS.length + 1]; for (int j = 0; j < BIZ_COLUMNS.length; j++) { params[j] = src.get(BIZ_COLUMNS[j]); } params[BIZ_COLUMNS.length] = targetId; rs.executeUpdate(updateSql, params); } } catch (Exception e) { // 记录日志,但不要抛出异常阻断流程 weaver.general.BaseBean log = new weaver.general.BaseBean(); log.writeLog("DetailSortAction error, requestId=" + requestId + ", mainId=" + mainId + ", msg=" + e.getMessage()); return ""; } return ""; } /** 构造比较器:主字段降序,次字段升序,最后按原 id 升序保证稳定 */ private Comparator<Map<String, String>> buildComparator() { return (a, b) -> { int r = toDecimal(b.get(SORT_COLUMN)).compareTo(toDecimal(a.get(SORT_COLUMN))); if (r != 0) { return r; } if (SORT_COLUMN_2 != null && !"".equals(SORT_COLUMN_2)) { r = Util.null2String(a.get(SORT_COLUMN_2)) .compareTo(Util.null2String(b.get(SORT_COLUMN_2))); if (r != 0) { return r; } } return toDecimal(a.get("id")).compareTo(toDecimal(b.get("id"))); }; } /** 字符串转数值,空值兜底为 0,避免字典序排序 */ private BigDecimal toDecimal(String v) { if (v == null || "".equals(v.trim())) { return BigDecimal.ZERO; } try { return new BigDecimal(v.trim()); } catch (NumberFormatException e) { return BigDecimal.ZERO; } } }代码里几个地方值得单独拎出来说。
origin和sorted是两个独立的 List,origin保持id升序,代表“坑位”;sorted是排好序的“内容”。回写的时候是origin.get(i)的id装sorted.get(i)的内容,这个映射关系是整个方案的核心,写错一个下标就全乱。
buildComparator里最后那一行toDecimal(a.get("id")).compareTo(toDecimal(b.get("id")))是稳定性保障。当主排序字段和次排序字段都相等时,靠id决定先后,这样每次跑出来的结果完全一致,不会出现“同样金额的两行今天在上面明天在下面”的鬼现象。
异常处理里return ""而不是抛出去,是因为泛微的 Action 返回非空字符串会阻断流程,赔了夫人又折兵。日志一定要打,不然出了问题连线索都没有。
注意:老版本的
RecordSet可能不支持executeUpdate(sql, Object...)这种变参写法。如果编译报错,把参数改成手动拼接,但一定要自己做转义,或者改用平台提供的PreparedStatement封装,别直接字符串拼接用户输入,有注入风险。
3.4 注册到流程节点与事务保护
代码写完,还要挂到流程上。路径是在流程设置里找到对应节点,在“节点操作”或“高级设置”里配置“提交后 Action”,把类的全限定名填进去。注册完记得清缓存重启,不然新类加载不上,你会以为代码没生效。
这里有个必须说清楚的点:Action 在什么时机执行。挂在“提交后”,意味着表单数据已经落库、流程状态已经推进,此时查明细表能查到完整数据。如果挂错了地方,比如挂在“提交前”,那查出来的还是上一版数据,排序自然不对。
关于事务,泛微的 Action 一般跑在流程自己的事务里,如果 Action 里抛了未捕获的异常,流程会卡住。所以代码里必须 try-catch 兜住。反过来说,如果你的批量 update 中途失败了一半,已经成功的那部分不会自动回滚,这时候数据就是半新半旧的状态。要彻底解决,得自己在 Action 里开事务手动控制,但对大多数场景来说没必要,因为排序操作本身幂等,重跑一次就对了。
还有个小细节:如果流程配置了“自动提交”,也就是节点上没有人参与、系统自动流转,这种节点的 Action 同样会执行,不用特殊处理。
4. 前端 JS 排序的应急方案
有些场景后端动不了,比如客户环境不让改代码、不让重新部署,只能改表单前端。这时候就得用 JS 顶上。我把这个方法也写清楚,但还是要强调一句:能用后端就用后端,前端是应急手段。
4.1 用 WfForm 接口读行、排序、回填
泛微 e-cology 9 的表单前端有一套WfForm对象,提供了操作明细表的能力。核心三个接口:getDetailAllRowIndexStr("detail_1")拿到所有行的索引串,比如"0,1,2,3";getDetailRowValue(index, "fieldname")读某一行的某个字段值;changeFieldValue("fieldname_index", {value: "..."})往某个单元格写值。
思路跟后端一样,读出来、排好、写回去:
function sortDetailByAmount() { var detailName = "detail_1"; var indexStr = WfForm.getDetailAllRowIndexStr(detailName); if (!indexStr) { return; } var indexes = indexStr.split(",").filter(function (v) { return v !== ""; }); if (indexes.length <= 1) { return; } // 1. 把每行的数据读出来 var rows = indexes.map(function (idx) { return { idx: idx, je: parseFloat(WfForm.getDetailRowValue(idx, "je")) || 0, wlmc: WfForm.getDetailRowValue(idx, "wlmc"), sl: WfForm.getDetailRowValue(idx, "sl"), dj: WfForm.getDetailRowValue(idx, "dj") }; }); // 2. 按金额降序,金额相同按原索引升序保证稳定 rows.sort(function (a, b) { if (b.je !== a.je) { return b.je - a.je; } return parseInt(a.idx) - parseInt(b.idx); }); // 3. 按原索引顺序回填排序后的内容 indexes.forEach(function (targetIdx, i) { var src = rows[i]; WfForm.changeFieldValue("wlmc_" + targetIdx, { value: src.wlmc }); WfForm.changeFieldValue("sl_" + targetIdx, { value: src.sl }); WfForm.changeFieldValue("dj_" + targetIdx, { value: src.dj }); WfForm.changeFieldValue("je_" + targetIdx, { value: src.je }); }); }这段逻辑本身不复杂,麻烦的是什么时候触发。泛微提供了WfForm.registerCheckEvent(WfForm.OPER_SUBMIT, function(callback) {...})这种提交前拦截的接口,把排序逻辑塞进去,排完再callback()放行。不同小版本这个方法名可能略有差异,具体以现场前端源码为准,打开表单页面看一眼WfForm的挂载方法就知道。
4.2 序号列同步与几个必须避开的坑
前端方案有四个坑,我逐个说。
坑一:changeFieldValue会触发联动和公式。这是最大的隐患。你把金额从第一行挪到第三行,如果金额字段配了“金额 = 数量 × 单价”的计算公式,或者配了汇总到主表的联动,那么每次changeFieldValue都会触发一次重算和联动。多次触发之后,主表的合计值可能被算歪。解决办法是先禁用联动(如果有对应接口),或者干脆避开带公式的字段,只排那些纯文本字段。
坑二:明细表的行数是动态的。用户在提交前可能刚加了一行还没填,也可能删了一行。getDetailAllRowIndexStr返回的索引可能是"0,2,3"这种不连续的串,代码里必须按返回的串遍历,不能自己for (i = 0; i < count; i++),否则会读到不存在的行,报错。
坑三:排序后页面上的“序号”列不会自动更新。泛微明细表的序号列是渲染时按位置生成的,你在 DOM 上换内容,序号列的视觉位置不变(它本来就是按位置显示的,所以是对的),但如果你的明细表里有一个用户可见的“序号”业务字段,那个字段的值不会跟着变,需要你在回填时一并写进去。
坑四:重复提交。用户点了提交,网络慢,页面转圈,用户又点了一次。排序跑了两遍,因为幂等所以结果不错,但如果排序逻辑里带了累加、编号重排这种非幂等操作,就会出问题。所以排序规则里尽可能只用比较,不做计算。
5. 高频问题排查速查表
这一节是实战沉淀,我把这几年在这个需求上遇到过的问题整理成表,遇到现象直接对号入座。
5.1 十个典型现象与原因对照
| 现象 | 大概率原因 | 处理办法 |
|---|---|---|
| 提交后顺序完全没变 | Action 没注册成功,或挂在了提交前节点 | 查流程日志确认 Action 是否执行,检查注册位置 |
| 顺序变了但数字排得不对,10 排在 9 前面 | 字段是字符类型,走了字典序 | SQL 里 CAST,Java 里用 BigDecimal |
| 空值行跑到了最前面 | NULL 或空串参与排序被当成最小值 | 统一兜底为 0 再比较 |
| 金额相同的两行每次顺序都不一样 | 比较器不稳定,缺少兜底排序键 | 最后一律用 id 升序兜底 |
| 附件全跑到别的行上了 | 回写时动了附件关联字段,或删除重建了行 | 只 update 业务字段,绝不动 id 和附件字段 |
| 子表明细和主明细对不上了 | 明细2 通过字段引用明细1 的行 id,被一起重写了 | 关联字段排除在回写列表外 |
| 部分行没排到 | 循环里用了固定行数,实际行数更多 | 用 List.size() 动态控制 |
| 主表合计值变大了或变小了 | 前端方案触发多次联动计算 | 换后端方案,或禁用联动后再排 |
| 流程被退回后顺序又乱了 | 退回后重新提交,Action 又跑了一遍,但排序依据字段被改过 | 确认排序字段是稳定业务字段 |
| 几百行的明细提交特别慢 | 逐条 update,每条一次数据库往返 | 改批量提交或合并 SQL |
表格里的前三条,基本占了实际问题的七成。尤其是字符类型排序那条,我见过不止一个项目因为金额字段建成了文本类型,排出来的顺序让人哭笑不得。
5.2 大明细表的性能处理
明细行数少的时候,逐条 update 完全没问题,几十行也就几十毫秒。但如果是物料清单、工程预算这种动辄几百上千行的明细,逐条 update 会明显拖慢流程提交速度,用户会感觉到“点提交之后卡住了”。
我的经验数据是:100 行以内,逐条 update 耗时在几百毫秒级别,感知不明显;500 行以上就建议优化了,1000 行以上必须优化。
优化的路子有三条。第一条是批量提交,把多条 update 攒起来一次性发给数据库,泛微的RecordSet通常提供批量执行的方法,或者你直接拿到底层Connection开addBatch。第二条是在数据库层面用一条 SQL 搞定,MySQL 8 可以用窗口函数配合临时表,把映射关系算好再一次更新:
-- MySQL 8.0+,示意写法,字段和表名按现场替换 UPDATE formtable_main_128_dt1 a JOIN ( SELECT t.id AS target_id, s.wlmc, s.sl, s.dj, s.je FROM ( SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM formtable_main_128_dt1 WHERE mainid = 10086 ) t JOIN ( SELECT ROW_NUMBER() OVER (ORDER BY CAST(je AS DECIMAL(18,2)) DESC, id) AS rn, wlmc, sl, dj, je FROM formtable_main_128_dt1 WHERE mainid = 10086 ) s ON t.rn = s.rn ) b ON a.id = b.target_id SET a.wlmc = b.wlmc, a.sl = b.sl, a.dj = b.dj, a.je = b.je;这条 SQL 的思路是把“按 id 排的行号”和“按规则排的行号”对齐,然后一次性 join 更新。写法上依赖窗口函数,SQL Server 和 Oracle 也有对应实现,但语法不同,跨库移植要改。
第三条是干脆把排序挪到异步。流程提交后启动一个后台任务去排,用户提交时不等结果。缺点是用户提交完立刻打开看到的是没排的,过几秒刷新才对。这种方式适合数据量特别大、又不要求实时看到结果的场景。
提示:不管你用哪种性能优化,改完一定要拿接近真实数据量的明细去压一遍。用五行的测试数据验证通过的代码,扔到五百行的生产环境里,未必扛得住。
5.3 几个只有实际做过才知道的细节
再说几个碎但很要命的点。
排序的时间点选择。如果你希望“每次提交都重排”,Action 挂在每个节点上。如果只希望“最终归档时排一次”,那就只挂在最后一个节点。前者的问题是用户中途打开表单看到顺序变了会困惑,后者的问题是流程一旦归档就改不了了。我一般选前者,因为用户对“提交之后就变了”是有预期的,反而对“走到一半突然变了”没预期。
和“必填校验”的关系。排序会在提交后重写值,如果某一行的必填字段本来就是空的,重排之后还是空的,不影响校验。但如果你在前端方案里做重排,重排完可能触发某些字段的必填提示,因为changeFieldValue有时会触发一次校验。这个在测试时要覆盖。
和“复制流程”的关系。泛微支持从已有流程复制新建,复制出来的新流程,表单物理表是新的,表名里的数字变了。Action 里硬编码的表名必须跟着改,否则代码跑起来查的是老表,静默失败,什么也不报。这个坑我在一个项目上踩过,排查了半天以为是权限问题。
和“数据导入”的关系。如果这个流程支持 Excel 批量导入明细,导入进来的顺序是导入文件的顺序。排序 Action 跑一遍之后会按规则重排,这是符合预期的,但要告诉业务方一声,不然他们会觉得导入的顺序被弄乱了。
和“打印模板”的关系。打印模板读取明细时也是按id顺序,所以排序结果会同步体现在打印件上,这是好事,但要注意打印模板里如果有“共计 X 行”这种固定文案,行数不会因为排序变化,不用管。
最后分享一个我自己一直在用的小习惯:写完排序 Action,先在测试环境用三条数据跑一遍,故意让金额相同、故意留一个空值、故意把一行删掉再加一行,把这几类边界都覆盖到。这三分钟的自测,比上线后被业务方叫去排查要划算得多。