开发教程

AI 写的批量删除清空了流水:事故复盘与上线检查清单

编辑部阅读约 5 分钟

一次对账流水被整体清空的事故,起因是新字段默认值为 null,AI 生成的批量写操作没有加条件,又直接在生产环境执行。文章拆解事故链条,说明为什么“代码能跑通”不等于安全,并给出批量写防护、软删除陷阱、备份恢复校验和上线检查清单等可落地的做法。

一条“逻辑没问题,通过”的批量写代码,把整张对账流水表写成了空值。事后回看,代码本身只有几行,真正让事故从小 bug 变成数据灾难的,是缺失的测试、审查和备份校验。这类事故和写代码的是人还是 AI 关系不大,但 AI 参与后,出问题的速度和规模被明显放大了。

事故是怎么发生的

事情的经过大致是这样:为了让对账支持分批处理,表里新加了一个批次字段,默认值留空。随后用 AI 生成了一段处理逻辑,把符合条件的订单批量写入这个新字段。代码在本地跑了一遍,主流程通畅,于是直接发布到生产。

问题在于,批量写没有带上足够严格的过滤条件,而存量数据的批次字段全都是空值。结果不是“删了一单”,而是整张表的对账记录被卷进了同一次操作,历史数据全部被覆盖成空,对账自然“全不见了”。

修复阶段又踩了第二个坑:从备份恢复表数据后发现金额对不上。排查了几个小时才意识到,备份是某个时间点的快照,而在下载备份之后,业务侧又撤销了两笔单据。快照加上后续增量没有对齐,账面当然对不上。

三个失误叠加,才变成事故

把链条拆开看,单独任何一环被守住,都不会有这次事故:

  1. 数据结构变更加上批量写,没有考虑存量数据。新字段默认值空,代码假定“所有记录都已经有值”,这个假设在测试库里成立,在生产库不成立。
  2. 批量操作没有条件和范围约束。没有 where 限制、没有 limit 分批、没有先查询影响行数,一次调用覆盖了全表。
  3. 生产环境成了第一个测试环境。没有用样本数据跑一遍,没有代码审查,没有灰度,执行前也没有确认会影响多少行。

为什么“跑通了”不等于安全

生成代码时,模型看到的上下文通常只有你贴出的片段和表结构,它不知道线上有几百万行历史数据,不知道哪些字段在旧记录里是空值,也不知道这张表昨天刚被业务手动改过。它给的是一段在理想数据上成立的逻辑。

这也不是 AI 独有的问题。手写代码同样会漏条件,区别在于人在写 where 时往往会犹豫一下:这张表有多大、条件够不够、删错了能不能恢复。而生成代码的过程太快,这段犹豫被跳过了。所以更准确的说法是:工具放大了流程里原有的漏洞。

代码层面的几道防线

批量写之前先确认影响范围

凡是 update / delete,先写查询、再写操作。这一步在多数框架里只要几行代码:

// 危险:条件宽松,一旦命中全表就是灾难
Order::where('status', 'paid')->update(['batch_id' => $batchId]);

// 更稳的做法:先确认影响行数,再限定主键范围分批处理
$ids = Order::where('status', 'paid')
    ->whereNull('batch_id')
    ->where('created_at', '<', $cutoff)
    ->limit(500)
    ->pluck('id');

if ($ids->count() > $expected) {
    throw new \RuntimeException('影响行数超出预期,终止执行');
}

DB::transaction(function () use ($ids, $batchId) {
    Order::whereIn('id', $ids)->update(['batch_id' => $batchId]);
});

关键点有三个:加上非空或时间范围等约束条件、用 limit 分批、把“影响行数”当成一个需要校验的返回值,而不是忽略它。

新增字段要处理历史数据

给已有表加字段时,把两件事写进迁移脚本:字段的默认值是什么,存量行怎么回填。如果业务上不允许为空,就在回填完成后再加非空约束,而不是让代码去猜。回填本身也要分批执行,避免一次锁表。

软删除不是加了字段就生效

很多项目给表加了删除标记字段,就以为删除是安全的。但软删除通常依赖模型层的配置,如果模型没有启用对应特性,批量删除仍然会走物理删除。更隐蔽的是,某些批量操作会绕过模型事件和观察者,导致审计日志、级联清理统统不触发。

不要假设“应该会触发”。上线前用一条测试数据实际验证一遍,或者干脆在数据库层加约束和审计表,把安全边界放在不容易被绕过的地方。

收敛生产环境的账号权限

日常开发连接生产库的账号,不应该具备 delete 和全表 update 的权限。危险操作交给受限账号或运维流程执行,是最省事的一道保险。

上线前检查清单

检查项 具体做法
数据样本验证 从生产抽样并脱敏,灌入测试库跑完整 CRUD
影响范围预演 在只读副本上执行一次查询,统计将影响的行数
批量写约束 必须有 where 条件、有 limit、有行数上限断言
事务与回滚 写入包在事务里,准备好回滚脚本
代码审查 重点看迁移脚本、批量写、删除逻辑,不看样式
执行窗口 避开业务高峰,执行时有人盯着监控
执行后校验 立刻核对行数、金额汇总等关键指标
备份可用性 确认备份存在,且知道怎么恢复、恢复到哪个时间点

数据恢复为什么会对不上

备份恢复失败往往不是因为备份坏了,而是因为时间点没对齐。可行的做法:

  • 恢复前先记录当前数据库的日志位点或时间戳,冻结相关表的写入;
  • 用快照恢复主体数据,再回放快照之后的增量变更;
  • 校验不只看行数,还要看金额汇总、状态分布这类业务指标;
  • 恢复完成后重新跑一遍完整校验,不要拿旧备份直接覆盖生产。

如果业务在恢复期间仍在写入,先停写再恢复,比恢复后补救便宜得多。

关于“没时间写测试”

讨论里明显有两种态度。一种认为必须补测试,没有覆盖就没法放心用 AI 提速;另一种认为小团队根本没这个时间,逻辑跑通就行,出事再说。

现实一点的分寸是:完整测试套件成本确实高,但有三件事几乎没有成本,不能省——执行前确认影响行数、把危险操作放进事务、确保备份能恢复。如果连这三件都做不到,那么用 AI 提速带来的收益,迟早会被一次事故抹平。

另外,软删除是否生效、批量操作会不会触发模型事件,不同框架和版本的行为并不一致,务必以所用版本的官方文档和一次实际测试为准,不要凭印象判断。

小结

这次事故的教训可以压缩成三句话:AI 不知道你有存量数据,凡是涉及批量写和删除的生成代码都要人工审查;生产环境不能当测试环境,执行前先算清楚会影响多少行;备份只是恢复的起点,增量对齐和恢复后校验才是完整方案。工具越快,流程里的窟窿就越贵。

AI 编程代码审查数据安全数据库上线流程