销毁已过保存期限的个人信息
目标
把保存期限表和保存令作为数据,将已过期的个人信息分为演练和真实销毁两步来删除。留下销毁凭证,并确认被删除者的信息是否真的读不出来,以及备份中残留了什么。
为什么重要
个人信息不删除是事故,删错了也是事故。保存期限届满后应毫不迟延地销毁,但因纠纷或监管机构要求而被施加保存令的案件,即使期限已过也不能删除。如果把这两条规则相冲突的地方交给人来判断,每次处理都会不同,所以要把判定做成可以重新计算的数据。 尤其是仅起算日一项,就会让结果整体改变。索赔的 3 年是从事故日期算起还是从结案日期算起,会使仍需保存的资料被删除几十笔,而这样的事故只有在删除之后才会暴露。 评分器不会相信你写出来的清单。它会根据合同、索赔、规则表直接重新计算并核对,销毁之后,还会检查从备份中找回的联系方式,在备份之外的文件中是否仍然残留。
步骤
- 创建并运行
/root/retention/gen_retention.py,生成/root/retention/ins.db以及材料和备份。包含 120 份合同、240 笔索赔、360 行个人信息、4 行规则、12 项保存令。 - 把起算日改成旧版规则后会多删除的案件写入
/root/retention/basis_gap.csv,把五个数字写入/root/retention/basis.txt。 - 把截至基准日已超过保存期限的案件写入
/root/retention/due.csv。此时还不考虑保存令。 - 把因保存令而排除的案件写入
/root/retention/hold_excluded.csv,把最终对象写入/root/retention/due_final.csv。 - 在
/root/retention/purge_plan.json中输出演练计划。这一步不删除任何东西。 - 创建并运行
/root/retention/purge.py,按计划删除,并在purge_log中留下记录。 - 在
/root/retention/purge_evidence.json中输出销毁凭证。 - 把备份中残留的副本写入
/root/retention/backup_residual.csv,把报告写入/root/retention/retention_report.md。
参考
- 判定规则:销毁对象是满足
기산일 + 보존연수 <= sys_param.asof的案件。起算日为空的案件(尚未结案的索赔)不是对象。 - 以年为单位的加减用
date.replace(year=...)进行。2 月 29 日如果那一年不存在,就调整为 28 日。 - 最终对象是从销毁对象中去掉被
legal_hold命中的案件。 - 销毁包括删除
subject表的行、删除该索赔的材料文件,以及写入purge_log。不要触碰备份。 purge_log的列为subject_type, subject_id, rule_kind, basis_date, expiry_date, purged_at, doc_path,前两列是主键。rule_kind为 policy 或 claim,doc_path在合同中为空。- 把规则计算集中放在一处,第 3 步到第 8 步就不需要重新编写。
- 确认基准日:
sqlite3 -readonly /root/retention/ins.db "SELECT * FROM sys_param;" - 常见错误:把起算日为空的案件当作很久以前处理,先看保存令再做期限判定,只删表中的行而留下材料文件,连备份也一起删除。
- 本实验中的人名、联系方式、地址全部是合成的。保存年限中不是法定时效的值是合成的公司内部规定,正文中已经这样写明。
创建合同和索赔快照
创建并运行 /root/retention/gen_retention.py,生成 /root/retention/ins.db、/root/retention/docs/、/root/retention/backup/2026-08-31/docs/。包含 120 份合同、240 笔索赔、360 行个人信息、4 行规则、12 项保存令。
一共六张表。每九笔索赔中约有一笔,结案日期应当为空(尚未结束的案件),规则表中同时放入当前规则(policy、claim)和旧版规则(policy_alt、claim_alt)。每笔索赔生成一份材料,并原样复制到备份目录中。
更改起算日会带来什么变化
把按旧版规则(policy_alt、claim_alt)属于销毁对象、但按当前规则尚未到期的案件,以 subject_type,subject_id,primary_expiry,alt_expiry 为表头写入 /root/retention/basis_gap.csv,并在 /root/retention/basis.txt 中写入 due_policy=、due_claim=、due_policy_alt=、due_claim_alt=、extra_under_alt=。
到期日是起算日加上年限的日期。使用 timedelta(days=365*n) 在闰年会发生偏移,所以请使用 date.replace(year=...)。起算日为空的案件没有到期日,所以 primary_expiry 一栏留空。
挑出期限已过的案件
把截至基准日(sys_param.asof)已超过保存期限的案件,以 subject_type,subject_id,rule_kind,basis_date,expiry_date 为表头写入 /root/retention/due.csv,按 subject_type、subject_id 升序排列。此时还不考虑保存令。
当前规则是:合同以 contract_end 为基准 5 年,索赔以 claim_closed 为基准 3 年。结案日期为空的索赔,时效尚未开始,所以不是对象。如果把空日期填成很久以前,进行中的索赔会被整个删除。
剔除被施加保存令的案件
把销毁对象中被 legal_hold 命中的案件,以 subject_type,subject_id,hold_id,reason 为表头写入 /root/retention/hold_excluded.csv,把其余的最终对象,以与 due.csv 相同的表头写入 /root/retention/due_final.csv。
保存令在期限判定之后应用。顺序一换,被施加了保存令但期限尚未过的案件也会进入排除清单,使数字变得模糊。reason 原样使用 legal_hold 表中写的文字。
删除之前先输出计划
在 /root/retention/purge_plan.json 中输出演练计划,其中包含 mode(dry-run)、executed(false)、asof、counts(policy、claim、files)、subjects(subject_type、subject_id、expiry_date)、files(要删除的材料路径)。这一步不删除任何东西。
如果用同一份代码里的一个标志位来区分计划和执行,总有一天这个标志会被设错。把计划输出为文件,让执行去读取这个文件,人就有了确认的机会。材料路径只存在于索赔中。
按计划进行真实删除
创建并运行 /root/retention/purge.py,删除计划中所列对象的 subject 行和材料文件,并在 purge_log 中留下记录。不要触碰备份。
只相信计划文件就去删除,会漏掉计划生成之后才施加的保存令。在执行之前再判定一次,如果与计划不同就停止。表中的行和文件必须一起处理,purged_at 是日期和时间。
输出销毁凭证,确认无法恢复
在 /root/retention/purge_evidence.json 中写入 asof、purged(policy、claim)、files_removed、held_kept、pii_rows_left_for_purged、doc_files_left_for_purged、backup_copies_left。数字必须是对当前状态亲自统计得出的值。
凭证不是文字,而是统计。不应当残留的两个数字(个人信息行、材料文件)必须为 0,而备份副本数则不会是 0。这个差距就是第 8 步的主题。
暴露备份中残留的副本并撰写报告
把已经删除但备份中仍留有副本的案件,以 subject_type,subject_id,backup_path 为表头写入 /root/retention/backup_residual.csv,并在 /root/retention/retention_report.md 中写出 ## 무엇을 지웠나、## 기산일을 어떻게 정했나、## 보존 명령、## 남은 사본、## 재발 방지 五个小节。
备份路径只写实际存在文件的。报告中要连同数字写入销毁笔数、索赔 3 年的依据条文、以什么日期作为起算日、因保存令而保留的笔数,以及备份中残留副本的情况。