背景脱敏:一个棋牌项目,策划用 Excel 配表,经 xresloader 按 proto 定义转成 protobuf 二进制发布给客户端和服务端。一张锦标赛配置表的数据整列错位了一个多月没人发现,一个合并单元格让整个 xlsx 存不了 1.5 小时。两件事最后落成同一条门禁。
现象一:错位的数据看起来完全合法
锦标赛基础配置表的数据行从第 8 列起整体左偏两列,至少从 7 月起就是这样。最隐蔽的一格:字符串 'blue'(本该是进度条颜色)落进了 bool 列「版本向下兼容」。
xresloader 处理 bool 列的规则是:任何非空字符串转成 true,不报错不告警。导出的 json 里 VersionCompat: True,看着完全正常。整列开关全开,而这列本来就是多数为真的开关,没人觉得异常。
留空反而更容易发现。静默转换出来的假数据没有任何异常信号。
判据不用猜,proto 注释就是标尺:ProgressColor 注释写「可选值: blue/red/green/gray/orange」,HighValueFilter 写「all/high_value/non_high_value」,而这两个值当时都待在左边两格的位置上,偏移量完全一致。整列错位,不是个别填错。
现象二:一个合并单元格让整表存不了
同一天,策划改角色主表保存失败,弹窗提示「文件可能被 Excel 占用或磁盘只读」,关掉 Excel 依然失败。真因是当天新加的一个 sheet 用 A1:H1 合并单元格做整行注释。配表编辑器的通用保存路径逐格无条件写入:
for ri, row in enumerate(rows, 1):
for ci, val in enumerate(row, 1):
ws.cell(row=ri, column=ci).value = parse_val(val)
# AttributeError: 'MergedCell' object attribute 'value' is read-only
openpyxl 里合并区只有左上角可写,其余是只读的 MergedCell。前端一次提交全部 sheet,所以改哪个 sheet 都存不了。这段裸写从工具初版起就在,四个月没炸只是因为没人在配表里用过合并单元格。
修复口径是直接报错,不做兼容。「跳过合并格照常保存」意味着策划以为存上了、实际那几格没写进去,是静默丢数据,比失败危险。
门禁:断言正确形态,不匹配违规长相
配表的标准结构是三行:第 1 行逐列中文名(给人看),第 2 行 proto 字段名(xresloader 按名匹配),第 3 行起数据。门禁 check_table_header.py 就围绕这三行做检查:
| 检查 | 后果 |
|---|---|
| 任何 sheet 不得有合并单元格 | 编辑器整表无法保存 |
| 第 2 行不得有空列、重复列 | 导表静默丢列或结果不可预期 |
| 第 2 行必须匹配 proto 字段名或别名 | 写错字段名不报错,整列从未导出(实发一列 DescKey 从没导出过) |
第 1 行逐列须有中文名、禁 # 注释;第 2 行禁中文 |
两行互换或混写会让编辑器、门禁、脚本各按各的假设取行 |
bool 列取值只能是 0/1/true/false/yes/no/是/否 |
非法字符串会被静默转 true,多为整列错位 |
bool 列检查的链路:从 convertlist 反查该 sheet 对应的 ProtoName → 解析 proto 拿到 bool 字段(认字段名与 field_alias)→ 逐行校验。全仓 196 个 sheet 只命中那一处真实病灶,零误报。
上线第二天就复发了一次。初版对「注释行」的检查只拦「A1 是 # 且整行其余为空」这一种形态,结果注释写在 B1 的、尾部还留着别的中文名的,四个 sheet 全部绕过,而全量门禁报通过。教训:门禁写成「匹配某一种违规长相」必然被下一个变体绕过,要写成「断言正确形态」,改成只要第 2 行有字段名,第 1 行同列就必须有非注释中文名。
存量处理:第 2 行与 proto 不匹配的 9 处进只减不增的白名单,第 1 行整行注释的 15 个 sheet 当天整改,改前改后全量重导、251 个产物 json 逐字段比对 0 差异。
区间门禁:只查本次改动
门禁挂在 pre-push,最初写成跑全量,立刻踩到:别人表里的存量问题把无关的推送全部挡下。改成只校验本次推送区间(--range=<remote_sha>..<local_sha>),新分支退化为相对主干的改动。
写钩子时踩到两个「看起来在跑、其实没生效」:git lfs pre-push 会把 stdin 吃光,之后 read 一行都读不到;用管道喂 while 会让循环跑在子 shell 里,里面的 exit 1 拦不住推送。两者都不报错。门禁验收必须造一个违规区间、断言退出码为 1,不能只看它打印了检查通过。
流程:配表改动须策划确认
门禁能拦结构,拦不住语义。8 月底一次故障定位到房间表里一个「显然该去掉」的机器人禁用项,技术侧直接改表被要求回滚:一个看似多余的配置可能承载策划意图(纯真人场、赛事场),技术侧只有故障视角没有玩法视角。
固定流程:技术定位到具体表、字段、行 → 通知策划,给出根因与建议选项 → 策划确认后再改,或由策划自己改。紧急止血也不例外。协议、代码、部署侧的修复不受此限。
教训
- 静默转换比报错危险:非法值变成合法值后没有任何信号,靠 proto 注释里的取值域做校验,而不是等有人觉得数据不对。
- 门禁断言正确形态,不要枚举违规长相,第二天就会有新变体。
- 区间门禁只查本次改动,全量会让存量拦住所有人,最后大家都绕过。
- 钩子必须用「造违规断言退出码」验收,静默失效的钩子和没有钩子一样。
- 容错分支写之前先问:跳过之后用户会不会以为成功了。会,就必须报错。