用户问「删了吗」,你能拿出的不该是一句「删了」,而是一条时间轴。
一、注销四十天后的那封回信
一个用户注销账号四十天后又回来问:「我的数据真的删了吗?」陆远记得自己执行过删除,users 表里那一行确实没了。但他顺着 user_id 去查,comments 还在,files 表里的十八条记录还在,对象存储里的十八个文件也还在;五个外部服务里,邮件列表仍留着这个地址,分析后台还能看到这个人的事件。
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
他这才意识到,自己理解的「删除」只是 DELETE FROM users,而用户理解的「删除」是「所有地方都不再持有」。
删除是一个跨系统动作,不是一条 SQL。 主库、对象存储、外部服务、备份,四处各有各的删除方式与时间常数,少算一处,承诺就不成立。
「已删除」至少有三种状态。 软删是标记不可见但仍持有,硬删是行从主库消失,副本过期是备份与分析明细到期。只做到第一种就回复「已删除」,是把内部状态当成了用户视角。
外键级联只覆盖同一个库。 PostgreSQL 的外键能保证 9 张表跟着 users 一起走,但它管不到对象存储里的文件,也管不到五个外部服务里的记录。
承诺必须写成时间轴,而不是形容词。 「尽快删除」无法验收;「主库 30 天、全部副本 60 天」才能被检查,也才能被写进回复。
二、十二张表与三类处理方式
12 张含个人信息的表不是同一种命运:9 张按 user_id 级联硬删,2 张按保留期清理,1 张因法定要求只能匿名化。
| 类别 | 表数 | 处理方式 | 规模与说明 |
|---|---|---|---|
| 按 user_id 级联 | 9 | 软删 30 天后硬删 | 单账号 97 行 |
| 按保留期清理 | 2 | events 90 天、audit_logs 按事件 90 天至 36 个月 | 随事件增长 |
| 法定保留 | 1 | payments 与发票保留 7 年,只匿名化 | 不可删除 |
9 张级联表是 users、profiles、organizations、memberships、invitations、projects、files、comments、messages。它们的共同点是都带 user_id,能在同一个事务里按人清掉。
97 行是九张表加起来的行数,不是一个账号在每张表各有一行。有的账号只在 users 与 profiles 留痕,有的账号建过项目、传过文件、写过评论,行数会集中在后几张表;删除时按 user_id 扫一遍,不能假设每张表都是 0 或 1。
2 张按保留期清理的是 events 与 audit_logs。events 走 90 天分析明细口径;audit_logs 不能统一写成一个数字,登录失败 90 天、删除事件 36 个月、账单事件 7 年,按事件类型分别到期。
1 张法定保留的是 payments 与发票。它不能删,只能匿名化:去掉 user_id 与邮箱,保留金额、日期与发票号,满足 7 年留存要求。
所以「删一个账号」在数据库里是 9 张表,在时间上是 30 天,在外部是 5 个接口,在账上还有 1 张不能删的表。把这几件事分开登记,才不会在回复用户时把「已匿名化」说成「已删除」。
三、五步流程与五个外部接口
删除流程固定五步,顺序不能换:
- 验证身份,沿用访问请求的三问核验。
- 软删 30 天,账号不可登录,但数据仍持有。
- 硬删 9 张表,单账号 97 行。
- 调 5 个外部删除接口,留存回执。
- 等 2 个保留期,分析明细 90 天、备份 30 天。
7 个外部处理者里,5 个提供删除接口,调用后能拿到回执;其余只能停用账号,等对方自己的保留期到期。
| 外部处理者 | 数量 | 删除方式 |
|---|---|---|
| 提供删除接口 | 5 | 调接口并保存回执 |
| 只能停用 | 2 | 停用账号,等其保留期 |
软删期仍然算持有。用户在这 30 天里反悔可以恢复,所以这段时间不能对外说「已经删除」,只能说「已停用,30 天后清除」。
身份核验在删除请求上比访问请求更重要。删错人的代价不可逆,所以三问核验之外,还要在软删窗口内保留一次可撤销的机会;核验不通过就不进入删除队列。
硬删必须排在软删之后,不能提前。提前硬删会让「30 天冷静期」变成一句空话,也会让误删无法回滚。
外部接口要留下回执:调用时间、返回码、对方单号。没有回执的删除,在复盘时等于没做。
两个保留期是承诺里最容易漏掉的部分。备份的 30 天从备份生成日算起,不是从删除日算起,所以最坏情况下副本要到第 60 天才消失;分析明细的 90 天同理,只能等。
四、三十天加三十天与一年的删除量
对外的删除承诺写成两段:主库 30 天,全部副本 60 天。前一段是软删窗口,后一段是软删 30 天加备份 30 天,30 + 30 = 60。
| 对象 | 单账号 | 36 个账号/年 |
|---|---|---|
| 9 张级联表 | 97 行 | 3,492 行 |
| 文件 | 18 个 | 648 个 |
| 事件 | 452 条/年 | 16,272 条 |
复算:
年删除占比:36 ÷ 344 = 10.465% ≈ 10.5%
级联行数:97 × 36 = 3,492 行
文件数:18 × 36 = 648 个
事件数:452 × 36 = 16,272 条
副本口径:软删 30 天 + 备份 30 天 = 60 天
一年删 36 个账号,只占 344 个账号的 10.5%,看起来不多;但每个账号都要走完 9 张表、18 个文件、5 个接口和 2 个保留期,任何一个环节漏掉,回复就不成立。
10.5% 是账号口径,不是行数口径。一个重度账号可能带走上百行与几十个文件,一个只注册未使用的账号可能只有几行;按账号数排期,按实际行数与文件数准备清理窗口,两者不能互相替代。
发票的匿名化要单独记账:删掉 user_id 与邮箱,保留金额、日期、发票号,7 年后由清理任务统一处理。这部分不能算进「已删除」,只能算「已匿名化」。
删除日志本身也要最小化。只记请求编号、时间、操作人、结果与回执号,不要为了「可追溯」把被删的内容再抄一份进日志,那等于用日志重建了一份副本。
每年抽 3 个已删除账号做验证:查 9 张表是否为零行、文件是否 404、5 个接口是否有回执、备份是否已过期。抽 3 个不多,但足以发现流程在哪一步断掉。
五、行动清单与复算验收
把删除从一条 SQL 拆成一条时间轴,用下面八条确认每个环节都有人负责、有证据可查。
清单按「表、窗口、接口、账本、抽检」五类排:前三类保证删得掉,后两类保证说得清。每完成一项就在工单里留一条日期,年底复盘时才能看出哪一步平均耗时最长。
- 列出 12 张含个人信息的表,写清表名与字段数
- 标出 9 张级联、2 张按保留期、1 张法定保留
- 在 9 张表上写清 user_id 外键与级联规则
- 软删固定 30 天,期间对外只说「已停用」
- 对接 5 个外部删除接口,保存调用回执
- 把分析 90 天与备份 30 天写进承诺,合计 60 天
- 发票只做匿名化,保留 7 年并单独记账
- 每年抽 3 个已删除账号,逐项验证 9 表、文件与回执
验收标准:任取一个已删除账号,能在 60 天内拿出 9 张表为零、18 个文件不可访问、5 个接口有回执的证据链。
使用边界与实操验收
边界一:删除不是一键。
它至少涉及主库、对象存储、外部服务与备份四类系统,任何一类没覆盖,都不能对外说「已删除」。
边界二:备份与法定保留是例外。
备份 30 天、发票 7 年都属于不能立即清除的情形,要在回复里写清范围与到期时间。
边界三:外部服务可能只支持停用。
7 个处理者里只有 5 个提供删除接口,其余只能停用账号,效果是「不再处理」,不是「数据消失」。
边界四:软删期仍算持有。
30 天冷静期里数据还在,此时收到访问或导出请求,仍要按持有中的数据来响应。
边界五:删除日志本身要最小化。
日志只记请求编号、时间、操作人、结果与回执号,不复制被删内容,避免制造新的副本。
关键参考与核验范围
- GDPR Art 17 :用于核对删除权的适用条件与例外。
- ICO 删除权 :用于核对删除请求的处理与回复口径。
- PostgreSQL 外键约束 :用于核对 9 张表的级联删除写法。
- S3 生命周期 :用于核对对象存储与备份的到期清理。
- GDPR Art 28 :用于核对处理者的删除协助义务。
以上链接于 2026-10-26 实测可公开访问;未核验版本、价格与配额。核验日期:2026-10-26。
系列导航
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。