
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); ListMapString, String origin new ArrayList(); while (rs.next()) { MapString, 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 ; } // 第三步复制一份按业务规则排序 ListMapString, 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); MapString, 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 ComparatorMapString, 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是两个独立的 Listorigin保持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,3getDetailRowValue(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 里 CASTJava 里用 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先在测试环境用三条数据跑一遍故意让金额相同、故意留一个空值、故意把一行删掉再加一行把这几类边界都覆盖到。这三分钟的自测比上线后被业务方叫去排查要划算得多。