概述
删除请求是数据流问题,不是一条 DELETE。先用 information_schema 找出直接标识列,再人工检查 JSON 字段、日志、备份和下游系统。对每个位置选择删除、匿名化或在有记录的法律依据下保留。订单通常需要保留金额和日期用于税务,但不需要保留邮箱和收货地址;营销事件和会话一般可以直接删除;审计事件可能需要留存,但可以把操作者邮箱置空。任务应把这些决定写进代码,运行手册说明为什么某些行仍然存在。
步骤
先发现数据位置:
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
AND (column_name = 'user_id'
OR column_name ILIKE '%email%'
OR column_name ILIKE '%phone%')
ORDER BY table_schema, table_name;
创建请求表和墓碑表。请求表是幂等键,完成后也不能删除;墓碑只保存加盐哈希和法律保留依据:
CREATE TABLE app.erasure_requests (
user_id uuid PRIMARY KEY,
requested_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
attempts integer NOT NULL DEFAULT 0,
last_error text
);
CREATE TABLE app.erasure_tombstones (
user_id_hash text PRIMARY KEY,
erased_at timestamptz NOT NULL DEFAULT now(),
retained_legal_basis text NOT NULL
);
任务在一个事务中执行:用 pg_advisory_xact_lock(hashtext($1)) 锁住用户,SELECT ... FOR UPDATE 检查 completed_at;已完成直接返回 already-erased。随后删除营销事件和会话,把订单的邮箱、地址置空,把审计事件的操作者邮箱置空,把个人资料改写为 erased-<hash>@invalid.example 和固定显示名,最后写入墓碑并标记请求完成。墓碑哈希使用服务端 pepper 和 SHA-256,不保存原始用户 ID。失败时回滚所有删除,并在事务外累加 attempts、记录 last_error。
验证
用一次性用户准备 profile、order、marketing event 和请求行,运行两次任务。第一次输出 erased,第二次输出 already-erased。随后查询断言:营销行 0、订单 PII 行 0、资料 PII 行 0、已完成请求 1、墓碑 1。再在测试环境制造一次外键冲突,确认事务回滚、请求仍未完成且 attempts 增加。
注意事项
备份和快照会保留数据直到保留期结束,不能承诺所有副本立即消失,也不要直接改写备份。税务、反欺诈和会计记录可能需要伪匿名化而不是物理删除,必须由法务确认保留依据。数据仓库、邮件供应商和日志系统需要各自的删除任务,本方案只覆盖 PostgreSQL。处理过程中不要记录原始用户 ID 或邮箱,因为日志本身也是个人数据存储。