437 lines
23 KiB
Markdown
437 lines
23 KiB
Markdown
# 审计模块审计报告 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] 架构影响地图需同步更新(见第五节)
|