文章目录

上一篇写了怎么把钉钉群文件区的 xlsx 稳定拉到本地。但拉下来只是开始——真正的难题是怎么把文件内容写进业务表单。这篇写这条链路的下半场:一个跑了数月的差异同步引擎,核心要回答三个问题:

  1. 源表(xlsx)和业务表单(宜搭表单,数百条记录)怎么逐行配对,尤其是同一批数据在两边有多条重复行时;
  2. HR 有两条修改路径(改群里的 xlsx / 直接在业务页面改数据),两边冲突时听谁的;
  3. 自动化怎么不出事故——批量误操作、解析错位、通道假成功,各需要什么保险。

一、先说为什么要做差异同步

最朴素的方案是"清库重灌":删光表内记录 → 按 xlsx 全量重建。它有完整的备份/对账闭环,但每次跑要十多分钟,而且有个致命问题——页面上的手工维护会被整批抹掉。我需要的是每天定时跑、增量只写差异、且不覆盖页面改动的方案:

清库重灌差异同步
耗时~15 分钟~1 分钟
页面手工改动全部覆盖保护并报告
适合场景兜底/修复每日定时主力

最终架构是两者并存:差异同步做主力,清库重灌(带备份)做异常兜底,--force 参数做"确认以源表为准"的人工逃生门。

二、行配对:匹配键与同键多行

差异比对的第一步是给两边(xlsx 行 / 表单记录)各算一个匹配键。键的设计原则:业务上唯一、跨渠道稳定。以人员花名册为例,我用了两级键:

⚠ 同键多行是常态,不是脏数据:同一人在一年里入职两次(离职再入职),就是两条"同键"记录;编制表里同一岗位分两行编制,也是同键多行。严禁按人去重——我们曾因此误清了有效数据。正确姿势是把同键的多行当作一个组,组内一一配对。

组内配对:差异最小化

更麻烦的是,业务系统对同键多行的返回顺序不保证稳定(分页、排序都是平台内部行为)。如果直接按顺序 zip 配对,两个"顺序错位"的行会配成一对,凭空造出成对的假差异。解法是差异最小化配对:当组内行数 ≤ 6 时,枚举所有排列,取"配对后逐行字段差异数总和最小"的组合:

bestPairing — 同键多行的差异最小配对
function bestPairing(srcRows, tblRows) {
  let best = null, bestCost = Infinity;
  permute([...srcRows.keys()], 0);          // n ≤ 6 时全排列
  function permute(arr, l) {
    if (l === arr.length) {
      let cost = 0;
      for (let i = 0; i < arr.length; i++)
        cost += fieldDiffs(tblRows[arr[i]], srcRows[i]).length;
      if (cost < bestCost) { bestCost = cost; best = arr.slice(); }
      return;
    }
    for (let i = l; i < arr.length; i++) { ... } // 交换递归
  }
  return best;
}

实践中再配一道辅助:拉取表单时带上确定性排序参数(按匹配键同构的字段排序),把"顺序漂移"压到只剩同键组内,配对算法就是最后一道防线。超过 6 行的大组按原顺序兜底(真出现 6 行以上同键时,说明数据本身需要人工看一眼了)。

三、冲突仲裁:页面改动保护(指纹快照)

这是整个引擎里我认为最有价值的部分。问题场景:自动同步每天跑,但 HR 也可能直接在业务页面上修数据(比如某人当天紧急离职)。下次同步时,这条行的表内值与源表不一致——是源表改了,还是页面改了?听错一边,要么覆盖掉 HR 的紧急修改,要么把源表的更正拒之门外。

解法是给每条记录维护一个上次同步后的行指纹(对所有比对字段做归一化后取哈希),存进状态文件:

1
比对出差异行后,看该行的表内当前指纹是否等于快照
hashes[instId]
2
指纹一致 → 说明上次同步后没人动过 → 差异来自源表 → 执行更新
听源表的
3
指纹不一致 / 无快照 / 在历史保留名单 → 判为页面改动 → 不覆盖,记入保留报告
听页面的
4
表内独有的行:上次就存在且不在保留名单 → 源表已删 → 删除;新出现 → 保留并报告
删除有据

三个刻意的安全设计:

四、三道保险丝

自动化写数据,最怕"一次性写入大量错误数据"。三道独立的保险:

保险 1:变更量阈值

单类变更(增/改/删)超过 max(基线值, 表内条数×30%) 时,全部拒写并告警。实际拦过的事故形态:xlsx 解析错位导致整列串位(几百行"更新")、HR 误删大片行再同步(大批"删除")。阈值触发后转人工:确认是正常批量变更就走 --force 或整表重灌,是事故就修源表。

保险 2:写后复核

全部写完后重新拉表、重算一次差异:只允许剩"保留"类,出现任何未落库的新差异即判通道异常,退出码置 1 告警。这一步专防平台侧的静默失败——参考我之前写的《宜搭"假成功"静默失败三连》,success 返回完全不可信,唯一的检验手段就是回读。

保险 3:同步报告落库

每轮结果(状态/版本/增改删计数/字段级明细)写进一张参数表,业务页面顶部读取后渲染成状态横幅:绿=同步正常、橙=群内有新版本未同步、红=异常。让看数据的人第一眼就知道"这份数据是几点的、哪个版本、可不可信"。

五、日期与数值的归一化细节

差异比对最大的噪音来源不是真差异,而是格式差异。几个必须统一口径的点:

六、踩坑清单(可直接复用)

  1. 匹配键选业务唯一且跨渠道稳定的组合:人员类用"证件号+姓名+关键日期"两级兜底,别单用一个可撞号的字段;
  2. 同键多行当组处理,组内差异最小化配对:顺序 zip 会因平台返回顺序不稳定造出成对假差异;
  3. 指纹快照仲裁冲突,首次运行安全方向:无快照一律保留报告不写,宁可多一轮人工确认;
  4. 保留名单要持续生效并出现在报告里:让 HR 看见"你的页面改动和源表不一致",而不是静默分叉;
  5. 变更量阈值 + 写后复核 + 状态横幅三道独立保险:分别拦解析错位、通道假成功、可信度未知;
  6. 差异比对先做归一化:日期到日粒度、数字字符串化、空值语义明确,否则报告全是噪音;
  7. 日期分量回读校验:JS Date 静默滚动(13 月、2 月 30 日)会悄悄把错数据写进库;
  8. 清库重灌保留为兜底手段:备份→清库→灌入→对账四步闭环,同步引擎异常时的人工逃生门。
✅ 现状:两套业务表(人员花名册约数百行、编制表约百行)已稳定运行数月,HR 只改群文件,数据每天两次自动进系统;页面紧急修改会被保护并出现在同步报告里,由人工决定去留。下一篇写这条链路的延伸:当业务系统要对外开放匿名查询时,登录墙怎么绕。

评 论