## Context `internal/query/retention` 以聚合查询计算审计与外部交互日志的在线留存边界。PostgreSQL 在没有匹配行时会为 `MAX` 和 `MIN` 返回 `NULL`,而当前扫描目标不能接收空值,导致所有依赖该边界的审计调查接口失败。 ## Goals / Non-Goals **Goals:** - 将空聚合结果识别为“尚无边界”,而非数据库错误。 - 保持已有已清理边界、最早在线记录和当月兜底的语义。 - 修复所有通过同一留存边界查询函数进入的审计接口。 **Non-Goals:** - 不执行审计物理清理,不改变归档或保留策略。 - 不修改数据库结构、迁移记录或历史审计数据。 - 不修改 API 路由、权限或响应字段。 ## Decisions ### 使用可空时间承接 SQL 聚合结果 留存边界查询使用标准库可空时间值承接 `MAX(range_end)` 与 `MIN()`。仅在值有效时转换为目标时区并作为边界返回。 拒绝将聚合结果用当前时间或固定时间 SQL `COALESCE`:这会把“尚未清理”误判为已归档,改变响应中的留存语义。 ### 同时覆盖已清理和最早在线两个聚合路径 `MAX(range_end)` 是本次线上错误入口;`MIN` 在在线表为空时具有相同的空值扫描风险。两个路径共用相同的可空聚合边界,应一次修复。 ## Risks / Trade-offs - [风险] 空边界被误判为已清理,错误拒绝历史查询 → 仅在聚合值有效时设置已清理标记与归档边界。 - [风险] 修复遗漏其他审计接口 → 保持修改在所有审计查询共用的留存边界函数内。 ## Migration Plan 1. 修改留存边界的空时间扫描逻辑。 2. 格式化并构建 API。 3. 仅替换 API 二进制并重启 API Unit。 4. 以 `GET /api/admin/audit/events?page=1&page_size=20` 验证无已清理记录时接口不再返回 500。 ### Rollback 无数据库迁移。若 API 启动或查询异常,覆盖回本次发布前 API 二进制并重启 API Unit。