# 审计模块审计报告 v2

> 审计范围：`src/modules/audit/**`、`src/app/(dashboard)/admin/audit-logs/**`、`src/app/api/export/route.ts`（审计导出分支）、`src/app/api/cron/audit-cleanup/route.ts`、`src/shared/lib/audit-logger.ts`、`src/shared/lib/change-logger.ts`、`src/shared/i18n/messages/{zh-CN,en}/audit.json`
> 审计日期：2026-06-25
> 审计依据：`docs/architecture/004_architecture_impact_map.md`（2.15 节）、`docs/architecture/005_architecture_data.json`（audit 节点）、`docs/standards/coding-standards.md`、项目 `project_rules.md`
> 前置报告：`docs/architecture/audit/audit-module-audit-report.md`（v1，2026-06-24）

---

## 一、现有实现概要

### 1.1 文件分布

v1 审计后已完成两轮重构（P0-P2），当前文件分布如下：

| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| 路由 | `app/(dashboard)/admin/audit-logs/page.tsx` | 79 | 操作日志列表页（RSC，调 data-access） |
| 路由 | `app/(dashboard)/admin/audit-logs/login-logs/page.tsx` | 79 | 登录日志列表页 |
| 路由 | `app/(dashboard)/admin/audit-logs/data-changes/page.tsx` | 83 | 数据变更日志列表页 |
| 路由 | `app/(dashboard)/admin/audit-logs/overview/page.tsx` | 34 | 审计概览仪表盘（✅ v1 P2-4 新增） |
| 路由 | 4 个 `loading.tsx` + 4 个 `error.tsx` | — | 骨架屏 + 错误边界（✅ v1 P0-2 已修复） |
| 路由 | `app/api/export/route.ts` | 202 | 统一导出 API（含 audit/login/dataChange 三分支） |
| 路由 | `app/api/cron/audit-cleanup/route.ts` | 75 | 定时清理 cron（✅ v1 P2-5 新增） |
| actions | `modules/audit/actions.ts` | 195 | 7 个 Server Action（1 查询 + 3 导出 + 3 保留策略） |
| data-access | `modules/audit/data-access.ts` | 387 | 12 个查询函数（分页 + 导出 + 选项 + 统计 + 趋势） |
| export | `modules/audit/export.ts` | 133 | Excel 列定义 + 行映射 + buildExport（✅ v1 P1-3 已抽取） |
| retention | `modules/audit/retention.ts` | 123 | 保留策略配置读写 + 清理 + 纯函数（✅ v1 P2-5 新增） |
| types | `modules/audit/types.ts` | 145 | 类型定义 + 状态映射常量 |
| services | `modules/audit/services/audit-service.tsx` | 124 | AuditService 接口 + Context + hook（✅ v1 P2-8 新增） |
| services | `modules/audit/services/admin-audit-service.ts` | 40 | 管理员默认实现 |
| services | `modules/audit/services/mock-audit-service.ts` | 57 | 测试用 Mock 实现 |
| hooks | `modules/audit/hooks/use-log-pagination.ts` | 24 | 分页 URL 状态 hook（✅ v1 P1-2 已抽取） |
| 组件 | 15 个组件文件 | 19-191 | 表格/筛选器/视图/详情对话框/概览/图表/保留配置/骨架屏/错误边界 |
| 测试 | `export.test.ts` / `retention.test.ts` | 191/81 | 纯函数单测（✅ v1 P2-6 已补充） |
| i18n | `shared/i18n/messages/{zh-CN,en}/audit.json` | 130 | 完整翻译键（table/filter/empty/export/error/detail/overview/retention） |
| shared | `shared/lib/audit-logger.ts` / `change-logger.ts` | — | `logAudit()` / `logDataChange()` 写入日志 |

### 1.2 数据流

```
page.tsx (RSC)
  ├─ requirePermission(AUDIT_LOG_READ)
  ├─ getAuditLogs/getLoginLogs/getDataChangeLogs (data-access 直调)
  └─ <AuditErrorBoundary>
       <AuditLogView items={...} />  (Client Component)
         ├─ <AuditLogFilters>  (nuqs URL 状态)
         └─ <AuditLogTable>    (分页 + 空状态 + 详情对话框)

overview/page.tsx (RSC)
  ├─ requirePermission(AUDIT_LOG_READ)
  ├─ getAuditOverviewStats / getAuditTrend / getDataChangeActionStats
  └─ <AuditOverviewView>  (RSC)
       ├─ <AuditOverviewStatsBar>
       ├─ <AuditActivityTrendChart>
       ├─ <DataChangeDistributionChart>
       └─ <AuditRetentionSettings>  (Client Component，直调 Server Actions)
```

导出流：`<AuditLogExportButton>` → `fetch /api/export` → `exportAuditLogsAction` → `getAuditLogsForExport` → `buildAuditLogExport` → Excel buffer。

定时清理流：`/api/cron/audit-cleanup`（CRON_SECRET 鉴权）→ `getAuditRetentionConfig` → `purgeExpiredAuditLogs`。

### 1.3 架构图记录情况

- **004（2.15 节）**：记录较完整，含 services/hooks/export/retention 节点，行数标注准确。
- **005（audit 节点）**：data-access/actions/types 记录完整，含 services/hooks/export/retention 节点。
- **已知不一致**：services 层的 `AuditServiceProvider` 在架构图中标注为"已接入"，但实际**未被任何页面使用**（详见 P0-1）。

---

## 二、现存问题与原因分析

### P0 — 严重违规

#### P0-1 Service 抽象层定义但从未使用（虚假完成）

- **位置**：`services/audit-service.tsx`（`AuditServiceProvider`、`useAuditService`、`useAuditAnalytics`）、`services/admin-audit-service.ts`、`services/mock-audit-service.ts`
- **违反规则**：任务要求 → "完全解耦：通过定义 TypeScript 接口抽象数据依赖，使用 React Context（或组合 Provider）注入数据服务，模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"；`project_rules.md` → "架构图优先规则：改码必同步图"
- **问题**：
  1. 全局搜索 `AuditServiceProvider` 的实际使用：**仅在 `audit-service.tsx` 和 `mock-audit-service.ts` 的 JSDoc `@example` 中出现**，没有任何页面或组件实际注入
  2. 4 个 `page.tsx` 均直接 `import { getAuditLogs, ... } from "@/modules/audit/data-access"`
  3. `audit-retention-settings.tsx` 直接 `import { getAuditRetentionConfigAction, ... } from "@/modules/audit/actions"`
  4. `audit-log-export-button.tsx` 直接 `fetch("/api/export")` 绕过 Service 层
  5. 架构图 004 第 1468 行声称"✅ P2-8 已修复：~~无 Service 接口抽象~~"，实际为"定义但未接线"
- **原因**：P2-8 仅创建了接口文件，未在页面层注入 Provider、未将组件改造为从 Context 获取 service
- **后果**：
  - DI 架构形同虚设，组件仍硬编码依赖 data-access，无法在测试中注入 mock
  - 架构图声称已修复，误导后续审计与维护决策
  - `mockAuditService` 完全无引用（死代码）

#### P0-2 mock-audit-service.ts 使用 9 处 `as` 类型断言

- **位置**：`services/mock-audit-service.ts` 第 29、31、33、36、45、46、47、53、60 行
- **违反规则**：`project_rules.md` → "禁止 `as` 断言（除类型收窄外）"；`project_memory.md` → "TypeScript strict mode: no `any`, no `as` assertions (except for type narrowing)"
- **示例**：
  ```typescript
  getAuditLogs: async () =>
    ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult<AuditLog>,
  ```
- **原因**：对象字面量缺少 `items: [] as AuditLog[]` 类型标注，导致需要 `as` 断言整个返回值
- **后果**：违反 TS 严格规则；lint 通过但 code review 应拦截

#### P0-3 全部 7 个 Server Action 缺少 revalidatePath

- **位置**：`actions.ts` 所有 7 个 Action（`getDataChangeLogsAction`、`exportAuditLogsAction`、`exportLoginLogsAction`、`exportDataChangeLogsAction`、`getAuditRetentionConfigAction`、`saveAuditRetentionConfigAction`、`purgeAuditLogsAction`）
- **违反规则**：`project_rules.md` → "Server Action 规范：使用 `revalidatePath` 精确刷新缓存"
- **问题**：`saveAuditRetentionConfigAction` 和 `purgeAuditLogsAction` 修改数据后未刷新 overview 页面缓存，用户保存保留策略后看到的仍是旧配置
- **原因**：遗漏
- **后果**：保留策略变更后概览页缓存不刷新，显示过期数据

### P1 — 重要缺陷

#### P1-1 trackEvent event 名称误用（保留策略操作误标为导出）

- **位置**：`actions.ts` 第 170、199 行（`saveAuditRetentionConfigAction`、`purgeAuditLogsAction`）、`app/api/cron/audit-cleanup/route.ts` 第 54 行
- **违反规则**：任务要求 → "监控：方案中预留关键操作埋点接口"
- **问题**：保留策略的保存/清理操作使用 `event: "audit.exported"`，语义错误
  ```typescript
  // saveAuditRetentionConfigAction
  await trackEvent({ event: "audit.exported", ... properties: { action: "save_config" } })
  // purgeAuditLogsAction
  void trackEvent({ event: "audit.exported", ... properties: { action: "purge" } })
  // cron route
  void trackEvent({ event: "audit.exported", ... properties: { action: "cron_purge" } })
  ```
- **后果**：分析平台中所有保留策略操作被归类为"导出"，无法区分导出与清理行为

#### P1-2 AuditErrorBoundary 使用错误的 i18n namespace

- **位置**：`components/audit-error-boundary.tsx` 第 19 行 `namespace="common"`
- **违反规则**：`project_rules.md` → "所有用户可见文本必须适配 i18n（使用 next-intl），提取翻译键"
- **问题**：`SectionErrorBoundary` 读取 `t("error.boundaryTitle")` / `t("error.boundaryDescription")` / `t("error.retry")`。`audit.json` 定义了 `error.title` / `error.description` / `error.retry`（键名不匹配），而 `common.json` 有 `error.boundaryTitle` / `error.boundaryDescription`。当前传 `"common"` 可工作但使用的是通用错误文案，未利用 audit 专属的 `error.description`（"数据加载时发生错误，请稍后重试。"）
- **后果**：区块级错误边界显示通用错误文案而非审计模块专属文案

#### P1-3 AuditAnalytics 接口定义为死代码

- **位置**：`services/audit-service.tsx` 第 64-84 行（`AuditAnalytics` 接口 + `noopAnalytics` + `AuditAnalyticsContext` + `useAuditAnalytics` hook）
- **违反规则**：任务要求 → "监控：方案中预留关键操作埋点接口"
- **问题**：`useAuditAnalytics` hook 全局搜索仅出现在定义处，**无任何组件调用**。`trackLogView` / `trackExport` / `trackOverviewView` / `trackRetentionConfigChange` / `trackPurge` 五个埋点方法均为死代码
- **后果**：客户端层面无日志查看/导出/概览查看埋点，无法统计用户行为

#### P1-4 data-access 错误处理不一致

- **位置**：`data-access.ts`
  - **吞没错误**（返回空数组）：`getAuditModuleOptions`（第 152 行）、`getDataChangeTableOptions`（第 237 行）
  - **抛出错误**：`getAuditLogs`、`getLoginLogs`、`getDataChangeLogs`、`getDataChangeStats`、`getAuditOverviewStats`、`getAuditTrend`、`getDataChangeActionStats`
- **违反规则**：`project_rules.md` → "错误与边界处理：明确处理空数据、无权限、网络异常等边界状态"
- **后果**：DB 故障时筛选器选项静默显示为空，用户误认为"无数据"而非"查询失败"

#### P1-5 日期筛选器 aria-label 不区分起止

- **位置**：`audit-log-filters.tsx` 第 94、101 行；`login-log-filters.tsx` 第 78、85 行；`data-change-log-filters.tsx` 第 117、124 行
- **违反规则**：`project_rules.md` → "可访问性（a11y）：语义化标签、ARIA 属性、键盘导航"
- **问题**：两个日期 Input 均使用 `aria-label={t("table.time")}`（"时间"），屏幕阅读器无法区分"开始日期"与"结束日期"
- **后果**：视障用户无法区分两个日期输入框

#### P1-6 AuditLogDetailDialog DialogDescription 重复标题

- **位置**：`components/audit-log-detail-dialog.tsx` 第 135-137 行
- **问题**：
  ```tsx
  <DialogTitle>{t("detail.title")}</DialogTitle>
  <DialogDescription className="sr-only">{t("detail.title")}</DialogDescription>
  ```
  Description 与 Title 完全相同，无信息增量
- **后果**：无障碍辅助技术读出重复信息

#### P1-7 骨架屏缺少 aria-hidden

- **位置**：`components/audit-log-table-skeleton.tsx`（全文件）、4 个 `loading.tsx`
- **违反规则**：`project_rules.md` → "可访问性（a11y）"
- **问题**：Skeleton 元素无 `aria-hidden="true"`，屏幕阅读器会逐个朗读骨架占位块
- **后果**：加载期间屏幕阅读器体验差

#### P1-8 window.confirm 阻塞式确认

- **位置**：`components/audit-retention-settings.tsx` 第 80 行
- **问题**：`window.confirm(t("purgeConfirm"))` 使用浏览器原生确认框，不可自定义样式、阻塞主线程、a11y 差
- **后果**：与其他模块（使用 AlertDialog 组件）交互不一致

### P2 — 改进项

#### P2-1 数据变更 diff 无可视化

- **位置**：`data-change-log-table.tsx` 第 118-129 行使用 `<pre>` 纯文本展示 oldValue / newValue
- **差距**：PowerSchool / Google Workspace Audit / Microsoft Purview 均提供 JSON diff 高亮（增行绿、删行红、左右对比）

#### P2-2 无失败登录异常告警

- **位置**：全模块
- **差距**：行业产品支持阈值告警（如 5 分钟内同一 IP 失败登录 > 10 次触发告警）

#### P2-3 无 IP 地理位置

- **位置**：表格仅展示 IP 字符串
- **差距**：行业产品将 IP 解析为地理位置（城市/国家），辅助判断异地登录

#### P2-4 概览趋势仅 7 天不可配置

- **位置**：`overview/page.tsx` 第 29 行 `getAuditTrend(7)` 硬编码 7 天
- **差距**：行业产品支持 7/30/90 天切换

#### P2-5 多学校数据隔离缺失

- **位置**：`data-access.ts` 所有查询无 `schoolId` 过滤
- **说明**：`audit_logs` / `login_logs` / `data_change_logs` 表无 `school_id` 字段（schema 确认），audit 模块设计为系统级跨校审计。当前 `AUDIT_LOG_READ` 权限若仅授予超级管理员则可接受，但需在文档中明确标注此设计决策
- **风险**：若未来授予校级管理员 `AUDIT_LOG_READ`，将导致跨校数据泄露

#### P2-6 useLogPagination hook 无单测

- **位置**：`hooks/use-log-pagination.ts`
- **违反规则**：`project_rules.md` → "可测试性：数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks"
- **说明**：v1 已补充 export.test.ts 和 retention.test.ts，但 hook 层无单测

#### P2-7 导出按钮无进度反馈

- **位置**：`audit-log-export-button.tsx`
- **问题**：大范围导出仅显示 spinner，无进度条/计数
- **差距**：行业产品显示"已导出 X / Y 条"

---

## 三、行业差距对比

| 能力 | PowerSchool / Veracross | Google Workspace Audit | Microsoft Purview | 本系统现状 | 影响 |
|------|------------------------|----------------------|-------------------|-----------|------|
| 统一审计概览仪表盘 | ✅ | ✅ | ✅ | ✅ 已有概览页 | — |
| 按用户/模块/动作/状态筛选 | ✅ | ✅ | ✅ | ✅ 已有完整筛选器 | — |
| 日志详情视图 | ✅ 点击展开完整 JSON | ✅ 详情面板 | ✅ 活动详情 | ✅ 已有详情对话框 | — |
| 数据变更 diff 可视化 | ✅ 左右对比 + 高亮 | ✅ | ✅ 差异高亮 | ❌ 纯文本 pre 展示 | 变更审查效率低 |
| 失败登录监控/告警 | ✅ 异常登录告警 | ✅ 可疑活动检测 | ✅ 实时告警 | ❌ 仅展示无告警 | 无法及时发现暴力破解 |
| 导出调度/定时 | ✅ 计划报告 | ✅ 导出 + 邮件 | ✅ 合规报告 | ❌ 仅手动导出 | 合规审计需人工操作 |
| 数据保留策略 | ✅ 可配置保留期 | ✅ | ✅ | ✅ 已有保留策略配置 | — |
| IP 地理位置 | ✅ | ✅ | ✅ | ❌ 仅显示 IP | 无法判断异地登录 |
| 多角色审计视图 | ✅ 管理员/合规官分级 | ✅ | ✅ | ⚠️ 仅 admin 单角色 + Service 接口预留 | 合规官角色需独立视图 |
| i18n | ✅ 多语言 | ✅ | ✅ | ✅ 已完整 i18n | — |
| 概览趋势可配置时间范围 | ✅ 7/30/90 天 | ✅ | ✅ | ❌ 硬编码 7 天 | 无法查看长期趋势 |
| DI 架构（可测试） | — | — | — | ⚠️ 定义未接线 | 组件无法 mock 数据源 |

---

## 四、改进优先级建议

### P0 — 立即修复（合规与架构正确性）

| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | Service 抽象层未接线 | overview 页面接入 `AuditServiceProvider`，`AuditOverviewView` 内组件改用 `useAuditService()` 获取数据；或若评估后认为 RSC + props 模式已满足需求则**删除未使用的 services 层**避免误导 |
| P0-2 | mock-audit-service.ts 9 处 as 断言 | 为对象字面量添加元素类型标注（如 `items: [] as AuditLog[]` → 改用 `items: new Array<AuditLog>()` 或显式标注返回类型） |
| P0-3 | 7 个 Action 缺 revalidatePath | 写操作（saveAuditRetentionConfigAction、purgeAuditLogsAction）添加 `revalidatePath("/admin/audit-logs/overview")` |

### P1 — 本轮实施（规范与体验）

| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | trackEvent event 误标 | 保留策略操作改用 `event: "audit.retention"` |
| P1-2 | ErrorBoundary namespace 错误 | `audit.json` 新增 `error.boundaryTitle` / `error.boundaryDescription`，`AuditErrorBoundary` 传 `namespace="audit"` |
| P1-3 | AuditAnalytics 死代码 | 组件中调用 `useAuditAnalytics()` 接入客户端埋点（日志查看、概览查看、导出、保留策略变更） |
| P1-4 | data-access 错误处理不一致 | `getAuditModuleOptions` / `getDataChangeTableOptions` 改为抛出错误 |
| P1-5 | 日期 aria-label 不区分 | 新增 i18n `filter.startDate` / `filter.endDate`，替换 `t("table.time")` |
| P1-6 | DialogDescription 重复 | 新增 `detail.description` 翻译键 |
| P1-7 | 骨架屏缺 aria-hidden | Skeleton 容器添加 `aria-hidden="true"` |
| P1-8 | window.confirm | 替换为 AlertDialog 组件（与其他模块一致） |

### P2 — 中长期迭代（功能增强）

| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | diff 无可视化 | 引入 JSON diff 组件（左右对比 + 增删高亮） |
| P2-2 | 无失败登录告警 | 新增异常检测规则 + 告警通知 |
| P2-3 | 无 IP 地理位置 | 接入 IP 反查服务（如 MaxMind GeoLite2） |
| P2-4 | 趋势不可配置 | 概览页新增 7/30/90 天切换按钮 |
| P2-5 | 多校数据隔离 | 文档标注"系统级审计"设计决策；若需校级审计则新增 `school_id` 字段 |
| P2-6 | hook 无单测 | 为 `useLogPagination` 补充单测 |
| P2-7 | 导出无进度 | 大范围导出显示进度条 |

---

## 五、架构图同步说明

本次审计发现架构图以下遗漏/不一致，需在实现后同步更新：

### 004_architecture_impact_map.md（2.15 节）

1. **修正 services 层状态**：第 1468 行"✅ P2-8 已修复"改为"P2-8 已定义接口，P0-1 待接线"（实施后改回"已接线"）
2. **修正 trackEvent 埋点描述**：保留策略操作的 event 名称从 `audit.exported` 改为 `audit.retention`
3. **新增 revalidatePath 说明**：actions 节标注 saveAuditRetentionConfigAction / purgeAuditLogsAction 已添加 revalidatePath

### 005_architecture_data.json（audit 节点）

1. **services 节点**：标注 `AuditServiceProvider` 的 `usedBy`（实施后从空改为 overview page）
2. **actions 节点**：更新 saveAuditRetentionConfigAction / purgeAuditLogsAction 的 trackEvent event 名称
3. **components 节点**：更新 `audit-error-boundary.tsx` 的 namespace 配置

---

## 六、重构方案设计

### a. P0-1 Service 抽象层接线方案

**决策**：保留 Service 接口（满足任务要求的 DI 架构），在 overview 页面接线。

由于 `AuditOverviewView` 是 RSC（async server component），而 `AuditServiceProvider` 是 Client Component（"use client"），无法直接在 RSC 中使用 Context Provider。因此采用**混合模式**：

1. **RSC 页面**：仍由 page.tsx 调用 data-access 获取初始数据（保留 SSR 性能优势）
2. **客户端交互组件**（`AuditRetentionSettings`）：通过 `AuditServiceProvider` 注入 service，组件内部用 `useAuditService()` 获取数据
3. **`adminAuditService`** 改为可被 Client Component 引用的轻量包装（仅委托给 Server Action，不直接 import server-only 的 data-access）

```typescript
// services/admin-audit-service.ts —— 改造为 Client-safe 实现
// 不 import "server-only"，改为委托 Server Actions
import { getAuditRetentionConfigAction, saveAuditRetentionConfigAction, purgeAuditLogsAction } from "../actions"

export const adminAuditService: AuditService = {
  // 保留策略相关：通过 Server Action 调用
  getAuditRetentionConfig: async () => {
    const res = await getAuditRetentionConfigAction()
    if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
    return res.data
  },
  saveAuditRetentionConfig: async (config) => {
    const res = await saveAuditRetentionConfigAction(config)
    if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
  },
  purgeExpiredAuditLogs: async (retentionDays) => {
    const res = await purgeAuditLogsAction(retentionDays)
    if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
    return res.data
  },
  // 查询类：RSC 页面已通过 props 传入，客户端不需要
  // ...其余方法可暂不实现或抛错
}
```

`AuditRetentionSettings` 改造：
```tsx
// 在组件内部使用 useAuditService() 而非直接调 Server Action
const service = useAuditService()
const res = await service.getAuditRetentionConfig()
```

页面注入：
```tsx
// overview/page.tsx
import { AuditServiceProvider } from "@/modules/audit/services/audit-service"
import { adminAuditService } from "@/modules/audit/services/admin-audit-service"

return (
  <AuditServiceProvider service={adminAuditService}>
    <AuditOverviewView stats={stats} trend={trend} distribution={distribution} />
  </AuditServiceProvider>
)
```

### b. P0-2 mock-audit-service 类型标注修复

```typescript
// 修复前
getAuditLogs: async () =>
  ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult<AuditLog>,

// 修复后 —— 为返回值添加显式类型标注，无需 as
getAuditLogs: async (): Promise<PaginatedResult<AuditLog>> => ({
  items: [],
  total: 0,
  page: 1,
  pageSize: 20,
  totalPages: 0,
}),
```

### c. P0-3 revalidatePath

```typescript
import { revalidatePath } from "next/cache"

// saveAuditRetentionConfigAction 末尾
revalidatePath("/admin/audit-logs/overview")
return { success: true, data: config }

// purgeAuditLogsAction 末尾
revalidatePath("/admin/audit-logs/overview")
revalidatePath("/admin/audit-logs")
return { success: true, data: result }
```

### d. P1-1 trackEvent event 修正

```typescript
// 保留策略操作
await trackEvent({
  event: "audit.retention",  // 原: "audit.exported"
  userId: session?.user?.id,
  targetType: "audit_retention",
  properties: { action: "save_config", ... },
})
```

### e. P1-2 ErrorBoundary namespace 修复

`audit.json` 新增：
```json
"error": {
  "title": "加载失败",
  "description": "数据加载时发生错误，请稍后重试。",
  "retry": "重试",
  "boundaryTitle": "审计数据加载失败",
  "boundaryDescription": "审计数据区块加载时发生错误。"
}
```

`audit-error-boundary.tsx`：
```tsx
<SectionErrorBoundary namespace="audit">
```

### f. i18n 翻译键补充

```json
"filter": {
  "startDate": "开始日期",
  "endDate": "结束日期"
},
"detail": {
  "description": "查看日志的完整字段信息。"
}
```

### g. 最终检查

- [x] 该模块不存在对其他业务模块的直接 import（仅依赖 shared/* 和 settings data-access for retention config）
- [x] 没有使用 `any` 或硬编码角色字符串
- [x] 所有 actions 包含 `requirePermission` 调用（7 个 Action 均有）
- [x] 文件行数未超过建议上限（最大 data-access.ts 387 行 < 800）
- [x] 架构影响地图需同步更新（见第五节）
