副标题:网信办新规征求意见稿把注销时限写死后,我们重新梳理了婚恋系统里删除一条会员数据的完整工程链路
9 月 2 日上午,一位会员在某婚恋小程序里点了注销。三分钟后,负责牵线的红娘打开工作台,还能看到她的完整资料,以及她和另一位会员的匹配记录。红娘以为是 bug,提了工单。
不是 bug。那条数据只是还没走到删除环节。
今年 1 月,国家网信办就《互联网应用程序个人信息收集使用规定》公开征求意见,其中一条被很多技术团队忽略了:用户注销账号的,应当在 15 个工作日内完成注销,删除已收集的相关个人信息或者进行匿名化处理。对普通工具类 App 来说,删一个账号无非清几张表。婚恋系统把这事的难度放大了好几倍。
一个会员的数据,从来不是"一条数据"
做婚恋系统的删除功能之前,我们先把一个注销会员的数据被谁引用摸了一遍,结果比预想的长:
数据位置
内容
特殊性
会员主表
实名信息、择偶条件
主体本人,直接删
红娘端缓存
匹配推荐列表
定时刷新,有滞后期
牵线记录
谁给谁牵过线、结果如何
机构的经营流水,含双方信息
聊天消息
双方对话
删一方,另一方的上下文还在
审计日志
谁在何时看过这份资料
合规留痕,本身不能随便删
第三行和第四行是婚恋行业特有的麻烦。普通社交产品里,用户数据基本只属于用户自己;而在婚恋系统里,一份会员资料从录入那天起就被三方使用:会员本人、牵线的红娘、以及被推荐给的其他会员。删除请求指向的是一个人,但数据关系网挂在多个人身上。
删除不是一条 DELETE:先建任务表
想清楚引用关系后,第一步不是写 DELETE,而是把注销做成一个可追踪的任务。理由很直接:15 个工作日是监管时限,你得能回答"这条注销请求处理到哪一步了",而不是"应该已经删了吧"。
CREATE TABLE deletion_request (
id BIGINT PRIMARY KEY,
member_id BIGINT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT ‘PENDING’,
– PENDING -> PROCESSING -> VERIFYING -> DONE / FAILED
deadline_at DATETIME NOT NULL, – 申请时间 + 15 个工作日
fail_reason VARCHAR(255),
retried_times INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
KEY idx_status_deadline (status, deadline_at)
);
每类数据注册一个独立的处理器:主表删除器、缓存失效器、牵线记录匿名化器、消息打标器、日志保留器。任务表按 status + deadline_at 建索引,调度器每小时扫一次 PENDING 和 FAILED 的任务,临近 deadline 未完成的优先重试。任何一步失败只标记 FAILED 并记录原因,不影响其他处理器继续执行——婚恋系统的注销链路最怕的不是慢,是某一步悄悄挂了没人知道。
牵线记录删不得,但必须匿名化
最容易做错的环节在这里。在婚恋系统里,牵线记录是婚介机构的经营流水:红娘哪天给谁推荐了谁、结果成没成。如果物理删除会员 A,把涉及她的牵线记录一并清掉,机构的对账和历史业绩就断了——这既不合理,也不是新规的本意。征求意见稿给的是"删除或者匿名化处理"两个选项,落到工程上就是按数据类型分路走:能反推到个人的字段直接删,作为经营事实要留存的做去标识。牵线记录属于后者,示例 SQL 长这样:
– 不删行,去标识:姓名/手机号/身份证等直接字段置空并打散主键关联
UPDATE match_record
SET member_name = ‘已注销用户’,
member_phone = NULL,
member_idcard = NULL,
member_ref = NULL, – 断开与会员主表的关联
anonymized_at = NOW()
WHERE member_id = 42;
判断标准可以归纳成一句话:能反推到具体个人的,删;作为经营事实需要留存的,去标识后留。匿名化之后的牵线记录还能回答"去年 3 月总共建了多少条线",但再也回答不了"会员张三参与过哪些"。示例数据上我们验证过,去标识后的记录对机构报表的完整度没有影响,因为报表聚合的维度本来就不含个人标识。
聊天记录按人删,不按会话删
聊天记录在婚恋系统里是另一个坑。会员 A 和会员 B 聊过 200 条消息,A 注销了,这 200 条怎么办?整段会话删掉,B 的聊天列表会突然出现空洞,某些对话语境直接丢失;一条不删,A 的个人信息又原封不动躺在服务器上。
我们的做法是按主体维度打标,而不是按会话删除。A 注销后,会话记录保留,但所有由 A 发出的消息内容置为占位符,B 端看到的是"该用户已注销,消息内容已按规范处理"。这本质上是给消息表加了一层 tombstone:
UPDATE chat_message
SET content = NULL,
is_tombstoned = 1
WHERE sender_id = 42 AND is_tombstoned = 0;
收到方(B)不修改。B 看自己这边的聊天记录时,A 发出的内容已不可读,但"这里曾经有过一段对话"的事实还在,时间线完整。这个取舍不一定是最优解,但它同时满足了两个约束:A 的个人信息不可恢复,B 的使用体验不塌方。
15 个工作日的工程解读:备份窗口和幂等
最后是时限本身。15 个工作日听起来很宽裕,有两个容易被忽略的细节藏在里面。
先说备份。大部分系统的数据库备份周期是 7 到 30 天,今天的删除操作执行完了,昨天的全量备份里那份会员资料还在。哪天从备份恢复数据,等于把注销悄悄撤销了。所以备份保留策略得纳入注销链路一起设计:在删除完成的校验环节,除了校验在线库,还要记录当时未过期的备份份数和最长过期时间,作为注销任务的收尾字段。换个角度看,“15 个工作日"给的不只是执行时间,还隐含了让备份自然滚出保留期的缓冲。
再说幂等。调度器会重试,处理器就必须幂等:匿名化重复执行无害(UPDATE 条件里带 anonymized_at IS NULL),删除用软删标记加异步物理清理,任务状态机保证同一个请求不会被并发执行两次。重试看似简单,注释掉重试逻辑的注销系统,在第一次网络抖动后就会开始欠账。
写在最后
注销功能在婚恋系统的需求优先级排序里常年垫底,但这次新规把时限写死之后,它从"体验优化"变成了"合规必答题”,尤其对持有大量敏感个人信息的婚恋系统更是如此。好在整套链路拆开看并没有黑科技:摸清引用关系,任务化,分处理器,删除和匿名化分开对待,重试写成幂等的,都是些朴素的工程手段。
文中方案来自我们在婚恋行业 SaaS(云中红线)的一线实践,欢迎交流。
15 个工作日内“删干净“一个会员:婚恋系统的注销链路比想象中难