保留策略要同时回答四件事:删哪类证据、属于哪个租户、早于什么时间,以及谁批准了这次不可逆操作。当前实现已经把租户和逻辑范围带到存储端,不再出现“一类策略触发全表删除”的旧偏差。
1. 默认期限与法定下限
0 表示永久保留;正数表示只保留最近 N 天。租户策略保存时,正数不能低于对应下限。
| 数据 | 全局/租户默认 | 允许清理的最低天数 |
|---|---|---|
| HTTP Request | 180 | 180 |
| 业务 Activity | 0,永久 | 90 |
| EntityChange | 0,永久 | 90 |
| Security | 0,永久 | 365 |
| Exception | 365 | 365 |
| ExternalRequest | 180 | 180 |
| CapConsumer | 90 | 90 |
| BackgroundJob | 90 | 90 |
租户策略实体还限制最大值为 365,000 天,并以 ExpectedVersion 做乐观并发。未保存显式策略的租户使用默认值。
2. 租户策略 API
| 方法与路由 | 行为 |
|---|---|
GET /api/audit/retention-policy | 返回显式策略或默认值、法定下限、版本和 IsConfigured |
PUT /api/audit/retention-policy | 校验所有期限与 ExpectedVersion 后创建或更新 |
POST /api/audit/retention-policy/preview | 估算拟定期限下各类别可清理行数 |
三个入口只接受认证主体的租户所有者,不允许平台租户 0,也不因 operate-as 自动切换到目标租户。
当前授权映射有一项需要单独治理:保存命令与读取/预估一样使用 auditing.audit.view。这意味着具备查看权限的主体也可能修改保留策略。生产 RBAC 应暂时只把该权限授予审计管理员,并计划把 mutation 拆到 Update/Manage 动作。
3. 影响预估的准确边界
Preview 使用同一个 IAuditQueryPort 统计截止时间之前的记录,适合在保存前评估数量,但它不是删除审批,也不是执行结果保证。
当前源码遍历七个 AuditCategory,而 HTTP Request 与业务 Activity 共用 Activity 类别:
RequestRetentionDays没有形成独立预估行;- Activity 的 TotalCount 可能同时包含 HTTP 与业务活动;
- Preview 没有先执行完整策略合法性校验,非法输入也可能得到一个估算;
- 查询与清理之间继续写入或并发清理时,实际删除数会变化。
因此 UI 应把预估标成“当前快照”,并在保存前单独展示 Request 期限;不能把七行估算误称为八类精确删除计划。
4. 自动执行路径
默认 Cron 为 0 3 * * *。JobHost 由 Quartz 调度;显式启用 API fallback 时,统一 Cron 计算器调用同一个 Executor。Audit:Retention:Enabled=false 会完全跳过自动清理。
Executor 当前遍历七个物理清理范围:HttpRequest、Activity、EntityChange、Security、Exception、ExternalRequest、Communication。它为每个租户加载显式策略或全局值,使用受信 RootCrossTenantOperation 进入存储,并记录意图与结果。单个清理失败会继续后续租户/范围,最终使 Job 失败并保留首个异常。
策略 Store 查询返回 Failure 时,当前实现会跳过该租户而不计入最终失败数。这是运维可见性缺口:应监控策略存储错误,不能仅以 Job 最终 Success 证明所有租户都被处理。
5. 分类映射
| Retention Scope | SqlSugar | Mongo |
|---|---|---|
| HttpRequest | 仅 SysAuditLog | audit_activity 中 module=Http 且有方法 |
| Activity | 仅 SysActivityLog | audit_activity 中排除 HTTP 条件 |
| EntityChange | SysEntityPropertyChangesLogs | audit_entity_change |
| Security | SysSpecialLog 的 Security/旧 Authorization Info | audit_security |
| Exception | SysSpecialLog 的 Exception/Error/Fatal | audit_exception |
| ExternalRequest | SysExternalRequestLogRecord | audit_external_request |
| Communication | SysCommunicationLog | audit_cap_consumer + audit_background_job |
Communication 的两类数据共用一个 Scope。租户策略为 CAP 和 BackgroundJob 提供两个期限时,Executor 取较长者,避免先删掉应保留更久的一类;代价是期限较短的一类也会多保留一段时间。
6. 有界物理删除
SqlSugar 和 Mongo 都按 1,000 条主键批次执行物理删除,每张表/每个集合最多 10,000 批,即单次最多处理 1,000 万条。达到上限会抛错,下一次执行从剩余记录继续,而不是发出一个无界 DeleteMany。
SqlSugar 先按分表日期裁剪旧分片,再在记录上同时应用 TenantId、Scope 和 CreateTime < cutoff。Mongo 使用 tenant_id、类别/HTTP 条件与 occurred_at < cutoff,先取稳定 _id 批次再删除。
删除不是软删除,也没有自动恢复。执行前应完成数据库备份策略、Legal Hold 核对和影响预估;大租户还应观察锁等待、事务日志、复制延迟与 Job 超时。
7. 手工清理入口
手工清理要求 operations.audit.delete,资源敏感级别为 admin:
POST /api/operations/audit/prune:JSON Body 包含beforeDate与approvalTicket;DELETE /api/audit/retention/{beforeDate}?ApprovalTicket=...:兼容路由,使用同一个 Command。
Handler 会拒绝早于 SQL Server 1753 边界的日期、未来日期、非规范或超过 64 字符的工单号,以及无法形成稳定 Actor 的调用者。Production 下 ISensitiveOperationApprovalPort 必须确认工单有效;其他环境是否要求审批由组合根决定。
删除前先写入 PruneAudit:<cutoff>:started 活动记录。若这条意图无法进入审计边界,物理删除不会开始。成功后记录总删除数;失败时记录异常类型并保留原异常。
8. 合规删除流程
建议把一次生产清理当成有编号的变更,而不是普通 API 调用:
- 冻结范围:Tenant、Scope、Cutoff、Purpose 与发起人;
- 预览:行数、最老/最新时间、分表和存储容量影响;
- 核对:法定下限、合同保留期、Legal Hold、诉讼保全与备份可用性;
- 审批:Incident、双人复核、有效期与执行窗口;
- 执行:先意图审计,再有界删除;
- 验证:删除计数、剩余最老记录、失败批次和复制状态;
- 证明:把工单、命令、结果和核验人写入保留期更长、不可被同一命令删除的合规账本。
通用 Activity 仍在待删审计存储内,不能作为唯一删除证明。监管要求严格时,应把清理证明投递到独立 Ledger/WORM。
9. 保留策略验收
预估与保存接受同一份输入。下面的示例保留 HTTP/外呼 180 天、异常 365 天,并让业务活动、实体变更和安全审计永久保留:
{ "requestRetentionDays": 180, "activityRetentionDays": 0, "entityChangeRetentionDays": 0, "securityRetentionDays": 0, "exceptionRetentionDays": 365, "externalRequestRetentionDays": 180, "capConsumerRetentionDays": 90, "backgroundJobRetentionDays": 90, "expectedVersion": 0}先保留预估响应,再把 GET 返回的最新版本写回文件后执行 PUT;不要在并发冲突后盲目把版本改大:
# 同一输入先预估、经审批后再保存。curl -fsS -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \ --data @retention-policy.json https://<HOST>/api/audit/retention-policy/previewcurl -fsS -X PUT -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \ --data @retention-policy.json https://<HOST>/api/audit/retention-policy- 每个 Scope 只删除对应物理源,Security/Exception 与 HTTP/Activity 严格分离;
- 永久类别在其他 Scope/Cutoff 下保持不变;
- 两租户、平台租户、operate-as 和租户目录枚举;
- 显式策略、缺省策略、乐观并发、法定下限和 365,000 上限;
- Request 独立执行与 Preview 的已知合并边界;
- Communication 取 CAP/Job 较长期限;
- 月/年分表边界、时区、闰日、相等时间和 1753 下限;
- 1,000 条批次、1,000 万条上限、取消、部分失败与下次续跑;
- Production POST/DELETE 的工单校验、过期工单和审批服务不可用;
- 意图审计失败时零删除,删除失败与结果审计失败的组合故障;
- 策略 Store Failure 被监控发现,而不是静默遗漏租户。