业务方说"这个操作必须留痕",技术团队当天加了一张日志表。半年后审计进场,第一个问题是:"上个月 12 号,谁把这条商品的价格从 199 改成了 89?"日志表里有时间、有 SQL 片段、有请求路径,唯独没有操作人,也没有旧值。于是"留痕"做完了,"审计"没做。
这不是执行不到位,而是转译缺失:业务方给的是一个词,技术团队接到的是一张表,中间那一步没人做。
1. "审计"这个词,在业务方口中通常是四件事
四件事的提问方式不同,实现机制不同,成本差一到两个数量级。混在一起谈,必然做成最贵又最不顶用的那一种。
| 那件事 | 将来谁会这样问 | 实现机制 | 成本量级 |
|---|---|---|---|
| 谁做的 | "这笔操作是谁提交的?" | 身份贯穿:请求上下文带操作者,落到记录里 | 低,主要是链路改造成本 |
| 能不能查到 | "上个月那批异常操作,列出来。" | 可检索的结构化记录 + 索引 + 查询入口 | 中,写入量决定存储与查询成本 |
| 能不能证明没被改 | "你怎么保证这份记录不是事后补的?" | 追加写、哈希链、外部锚定、独立存储 | 高,涉及流程与留存制度 |
| 能不能还原当时状态 | "改之前这条数据长什么样?" | 前后值快照或版本表,而非仅变更动作 | 高,写入放大约 2 倍以上,且要定义快照粒度 |
// 四件事对应的实现,成本从低到高
谁做的 → 请求上下文里带身份,落到记录
能不能查到 → 结构化字段 + 索引 + 查询入口
能不能证明没被改 → 只追加写 / 哈希链 / 独立存储
能不能还原当时状态 → 前后值或版本表(写入放大约 2 倍)
判断清单:
- 谁会来问?审计部门、外部监管、法务、内部风控,四者的口径不同。
- 问的是事实还是证明力?只有后者需要防篡改机制。
- 需要还原的是单条记录还是整批状态?后者要考虑快照与重放,成本陡增。
- 留存期是谁定的?来自审计要求,还是来自存储预算的倒推?
- 记录是否需要对外出示?需要时即可举证性要求,属于合规范畴。
- 现有采集点能否拿到操作者?拿不到时,其余字段再全也没用。
一句话判据:"留痕"回答的是有没有写下来,"审计"回答的是能不能据此下结论。
2. 转译的第一步:把"要审计"拆成问题清单
在写任何表结构之前,先产出一份问题清单:列出将来可能被问的具体问句。这份清单是最小可交付物,也是唯一能验证"做够了没有"的东西。
// 问题清单:每条都要能映射到字段与采集点,映射不上的先标缺口
questions:
- q: "上个月 12 号谁改了商品 X 的价格?"
needs: [operator, occurred_at, object_id, before, after]
source: app-audit
- q: "这批下架操作里,有没有人用了别人的账号?"
needs: [operator, session_id, object_id, action]
source: app-audit
- q: "这条记录从写入到现在有没有被改过?"
needs: [record_hash, prev_hash, storage]
source: append-only-log
- q: "退款审批当时看到的订单金额是多少?"
needs: [snapshot_ref, occurred_at]
source: version-table
判断清单:
- 每个问题是否有明确的提问人角色?没有角色的问题,多半是想象出来的。
- 每个问题是否能映射到具体字段?映射不上的,就是当前方案的缺口。
- 问题是否可以回答"查不到"?有些问题本来就该答"我们没留",提前说清比事后解释便宜。
- 问题清单是否被业务方签字确认?没确认的清单,审计时会被重新解释一遍。
- 清单是否随业务变化更新?口径变更次数本身是一项度量。
问题清单的价值不在于穷尽,而在于让"够不够"变成可讨论的事:业务方会说"还要能查金额",技术方会说"那需要快照",成本与必要性在这一刻才第一次对齐。
3. 五个必答字段与各自的缺失后果
| 字段 | 缺失后果 | 采集难点 |
|---|---|---|
| 操作者 | 记录只能证明"发生过",不能证明"谁做的",审计价值归零 | 异步任务、消息消费、内部调用没有天然身份 |
| 时间 | 无法定位窗口,也无法排序与去重 | 多时钟源、时区、精度不一致 |
| 对象 | 无法回答"这条记录关于谁",只能全表人工比对 | 对象标识在级联操作中会变(合并、拆分) |
| 动作 | 无法区分查询与修改,也无法归类 | 动作词表不统一,"更新"与"修改"混用 |
| 前后值 | 能证明改过,不能证明改成了什么,也无法回滚 | 大字段、关联表、批量操作的成本 |
// 一条能回答审计问题的记录:字段齐备只是下限
{
"operator": { "type": "user", "id": "u_8821", "via": "web" },
"occurred_at": "2026-03-12T10:22:31.482+08:00",
"object": { "type": "product", "id": "p_3391" },
"action": "price.update",
"before": { "price": 19900, "currency": "CNY" },
"after": { "price": 8900, "currency": "CNY" },
"traceId": "b71e-4c02",
"prevHash": "9ac1...", "hash": "3f7d..."
}
判断清单:
- 操作者是否区分了人、系统、定时任务?三类混在一个字段里,查询时无法过滤。
- 时间是否带时区且精度足够?秒级精度在批量操作里无法排序。
- 对象标识是否稳定?会变的标识需要在记录里同时写旧标识。
- 动作是否有受控词表?自由文本会在半年后变成几十种同义写法。
- 前后值是否只记变化的字段?全量快照会让写入量翻倍,但不是所有场景都该省。
4. 采集点、可读性与保留期
| 采集点 | 能拿到什么 | 盲区 |
|---|---|---|
| 应用层 | 操作者、业务语义、前后值 | 绕过应用的直连改库完全没有记录 |
| 数据库层(触发器、binlog、审计插件) | 每一行的真实变更,无法绕过 | 查不到是谁做的,只有连接账号;批量与关联变更难以还原业务语义 |
| 网关层 | 请求方、路径、时间、响应码 | 只知道"调了这个接口",不知道改了哪条数据 |
采集点选错,最典型的表现就是"数据库层的操作查不到是谁"。数据库审计表里有账号 app_rw,几万个请求共用它,等于没有操作者。反过来,只做应用层,一次手工 UPDATE 就能让记录与事实脱节。
判断清单:
- 是否存在绕过应用的写路径(运维脚本、数据修复、DBA 直连)?存在就必须补数据库层记录。
- 应用层记录是否带请求上下文?拿不到会话与身份时,等于只记了动作。
- 两层记录是否能对齐?用
traceId或业务键串起来,否则对账时互相矛盾。 - 记录是否追加写?可更新可删除的表,无法回答"有没有被改过"。
- 记录是否与业务库同库?同库时,一次误删会同时毁掉数据与证据。
可读性是被低估的一半。原始日志给审计者看,等于没给:他要的是"谁在什么时候把什么改成了什么",而不是一行带转义符的 JSON。可读性有三层:字段命名可读、动作词表可读、能按对象与时间直接查出变更轨迹。
─── 按对象查轨迹:审计者要的是这个,不是原始日志
SELECT occurred_at, operator_id, action, before_json, after_json
FROM audit_record
WHERE object_type = 'product' AND object_id = 'p_3391'
AND occurred_at >= '2026-03-01'
ORDER BY occurred_at;
─── 只追加写:应用账号没有 UPDATE / DELETE 权限,才能回答"有没有被改过"
GRANT INSERT, SELECT ON audit_record TO 'app_audit'@'%';
─── 哈希链:hash = H(prev_hash || 本条规范化字段),链断即被改
审计需求与合规需求不是一回事:前者是自查(自己想知道发生了什么),后者是对外举证(要能向第三方证明)。举证要求通常包含独立存储、不可否认、留存年限与访问记录,成本高出一个台阶。把自查做成举证,是浪费;把举证当自查做,是缺口。
| 度量 | 说明 |
|---|---|
| 字段完整率 | 记录中五个必答字段齐备的比例 |
| 可回答的问题比例 | 问题清单中能实际回答的条目占比 |
| 取证耗时 | 从接到提问到给出结论的时间 |
| 审计口径变更次数 | 问题清单被重新解释的次数 |
5. 排错:五种"做了审计但答不上来"
| 现象 | 原因 | 处理 |
|---|---|---|
| 审计说"查不到谁改的" | 采集点在数据库层,只有共用连接账号 | 补应用层身份贯穿,或用会话级标记 |
| 记录里有操作,业务上说不通 | 动作词表不统一,多条记录表达同一件事 | 建受控词表,历史数据做一次归一 |
| 记录被质疑"可以事后补" | 记录表可更新,无追加写约束 | 改为只追加,加哈希链或独立存储 |
| 需要还原旧值却只有变更动作 | 只记了"改过",没记"改成什么" | 加前后值或版本表,明确快照粒度 |
| 审计期发现保留期已过 | 保留期按存储成本设定 | 以审计要求为准重定,并留到期流程 |
6. 边界:什么情况不必做完整转译
| 场景 | 可压缩为 | 理由 |
|---|---|---|
| 内部小工具,操作人只有几名 | 一份问题清单 + 一张带五字段的表 | 人少,身份可从上下文直接推断 |
| 无对外承诺、无监管要求 | 只做"谁做的"与"能不能查到" | 证明力与快照成本收不回来 |
| 数据本身可重放(事件溯源已存在) | 复用事件流,不另建审计表 | 重复建设只会制造第二份真相 |
| 低频关键操作 | 单条记录 + 人工复核 | 频率低,人工成本低于系统成本 |
转译的终点不是"建一张表",而是一张业务方能读懂的问题清单,加一份能回答其中大部分问题的记录结构。答不了的那几条写明,比假装都能答更安全。
自测
- "留痕"与"审计"的差别,用一句话怎么表述?
- 四类审计诉求里,哪两类的实现成本明显更高,为什么?
- 五个必答字段中,缺失哪一个会让记录整体失去审计价值?
- 数据库层采集的最大盲区是什么,如何补救?
- 为什么"审计"与"合规"不能按同一套标准建设?
- 什么条件下可以把完整转译压缩成"问题清单加一张表"?