# 审计模块审计报告 > 审计范围:`src/modules/audit/**`、`src/app/(dashboard)/admin/audit-logs/**`、`src/app/api/export/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-24 > 审计依据:`docs/architecture/004_architecture_impact_map.md`(2.15 节)、`docs/architecture/005_architecture_data.json`(audit 节点)、`docs/standards/coding-standards.md`、项目 `project_rules.md` --- ## 一、现有实现概要 ### 1.1 文件分布 | 层 | 文件 | 行数 | 职责 | |------|------|------|------| | 路由 | `app/(dashboard)/admin/audit-logs/page.tsx` | 74 | 操作日志列表页(RSC,调 data-access) | | 路由 | `app/(dashboard)/admin/audit-logs/login-logs/page.tsx` | 74 | 登录日志列表页 | | 路由 | `app/(dashboard)/admin/audit-logs/data-changes/page.tsx` | 78 | 数据变更日志列表页 | | 路由 | `app/api/export/route.ts` | 201 | 统一导出 API(含 audit/login/dataChange 三分支) | | actions | `modules/audit/actions.ts` | 214 | 4 个 Server Action(1 查询 + 3 导出) | | data-access | `modules/audit/data-access.ts` | 290 | 9 个查询函数(分页查询 + 导出遍历 + 选项 + 统计) | | types | `modules/audit/types.ts` | 117 | 类型定义 + 状态映射常量 | | 组件 | `components/audit-log-view.tsx` | 61 | 操作日志视图(筛选+表格+分页) | | 组件 | `components/audit-log-table.tsx` | 110 | 操作日志表格 | | 组件 | `components/audit-log-filters.tsx` | 92 | 操作日志筛选器 | | 组件 | `components/audit-log-export-button.tsx` | 83 | 导出按钮(fetch /api/export) | | 组件 | `components/login-log-view.tsx` | 59 | 登录日志视图 | | 组件 | `components/login-log-table.tsx` | 104 | 登录日志表格 | | 组件 | `components/login-log-filters.tsx` | 77 | 登录日志筛选器 | | 组件 | `components/data-change-log-table.tsx` | 281 | 数据变更表格+筛选器+展开行(混合) | | shared | `shared/lib/audit-logger.ts` | 46 | `logAudit()` 写入审计日志 | | shared | `shared/lib/change-logger.ts` | 43 | `logDataChange()` 写入变更日志 | | i18n | `shared/i18n/messages/{zh-CN,en}/audit.json` | 12 | 仅 3 个标题/描述键 | ### 1.2 数据流 ``` page.tsx (RSC) ├─ requirePermission(AUDIT_LOG_READ) ├─ getAuditLogs/getLoginLogs/getDataChangeLogs (data-access) └─ (Client Component) ├─ (nuqs URL 状态) └─ (分页 + 空状态) ``` 导出流:`` → `fetch /api/export` → `exportAuditLogsAction` → `getAuditLogsForExport` → Excel buffer。 ### 1.3 架构图记录情况 - **004(2.15 节)**:记录了 audit 模块,但存在**不一致**:声称 actions 层有 `getAuditLogsAction` / `getLoginLogsAction`,实际**不存在**这两个 Action(页面直接调 data-access)。组件清单不完整(缺 `data-change-log-table.tsx`、`login-log-view.tsx`、`audit-log-export-button.tsx`)。 - **005(audit 节点)**:data-access/actions/types 记录较完整,但 actions 的 `usedBy` 标注为"待扩展"(实际已被 `/api/export/route.ts` 使用)。 --- ## 二、现存问题与原因分析 ### P0 — 严重违规 #### P0-1 i18n 严重缺失:组件全部硬编码英文文案 - **位置**:`audit-log-table.tsx`("User/Module/Action/Target/Status/IP Address/Time/No audit logs found.")、`audit-log-filters.tsx`("Module/Any Module/Action.../Status/Any Status/Success/Failure")、`login-log-table.tsx`、`login-log-filters.tsx`、`data-change-log-table.tsx`("Table/Record ID/Changed By/View/Hide/Old Value/New Value/Any Table/Create/Update/Delete/Reset/No data change logs found.")、`audit-log-export-button.tsx`("Export Excel/Export failed/Export ready") - **违反规则**:`project_rules.md` → "所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键" - **原因**:i18n 字典 `audit.json` 仅含 3 个标题/描述键,组件层未接入 `useTranslations` - **后果**:中文用户在审计页面看到全英文表格表头、筛选器、空状态、按钮文案,与系统其他模块(已 i18n)体验割裂;无法切换语言 #### P0-2 缺少 loading.tsx 与 error.tsx 错误边界 - **位置**:`app/(dashboard)/admin/audit-logs/`、`login-logs/`、`data-changes/` 三个路由均无 `loading.tsx` 和 `error.tsx` - **违反规则**:`project_memory.md` → "All student routes must include loading.tsx and error.tsx for error boundaries";`project_rules.md` → "每个独立的数据区块必须用 React Error Boundary 包裹" - **原因**:审计路由作为后加模块未补齐边界文件 - **后果**:数据加载期间白屏;运行时错误直接显示 Next.js 默认错误页,无重试能力 #### P0-3 架构图与代码不一致 - **位置**:`004_architecture_impact_map.md` 2.15 节 - **违反规则**:`project_rules.md` → "改码必同步图"、"架构图优先规则" - **问题**:004 声称存在 `getAuditLogsAction` / `getLoginLogsAction`,实际不存在;组件清单缺 3 个文件;行数标注过期(actions 标 212 实际 214,data-access 标 260 实际 290) - **后果**:权限审计、依赖分析会得出错误结论 ### P1 — 重要缺陷 #### P1-1 formatDate 硬编码 locale 为 "zh-CN" - **位置**:`audit-log-table.tsx:92`、`login-log-table.tsx:86`、`data-change-log-table.tsx:116` - **违反规则**:i18n 就绪要求 - **原因**:直接传 `"zh-CN"` 而非从 next-intl 获取当前 locale - **后果**:英文用户看到中文格式日期 #### P1-2 分页 handlePageChange 三处重复 - **位置**:`audit-log-view.tsx:29-38`、`login-log-view.tsx:27-36`、`data-change-log-table.tsx:58-67` - **违反规则**:`project_rules.md` → "最大化复用" - **原因**:三处 100% 相同的 URL searchParams 操作逻辑未抽取 - **后果**:维护需改三处,易遗漏 #### P1-3 导出逻辑内联在 actions 层,三个导出 Action 结构高度重复 - **位置**:`actions.ts:90-214`(`exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction`) - **违反规则**:004 已标记为 P2 待修复;`project_rules.md` → 单文件职责清晰 - **原因**:列定义 + 行映射 + buildExcelExport 全内联在 actions - **后果**:新增日志类型需复制整段;列定义无法在组件层复用(如详情视图) #### P1-4 DataChangeLogTable 混合三职责(281 行) - **位置**:`data-change-log-table.tsx` - **问题**:表格组件 + 筛选器组件(`DataChangeLogFilters`)+ 展开行逻辑全部定义在同一文件 - **违反规则**:`project_rules.md` → "组件必须为纯函数,职责单一" - **后果**:筛选器无法独立复用;文件接近 300 行不易维护 #### P1-5 无 React Error Boundary 包裹独立数据区块 - **位置**:三个 `page.tsx` 均直接渲染 `` / `` 无 ErrorBoundary - **违反规则**:`project_rules.md` → "每个独立的数据区块必须用 React Error Boundary 包裹" - **后果**:单个区块错误导致整页崩溃 #### P1-6 Suspense fallback 为 null,无骨架屏 - **位置**:`audit-log-view.tsx:57`、`login-log-view.tsx:55`、`data-change-log-table.tsx:277` - **违反规则**:`project_rules.md` → "异步数据使用 React Suspense + 骨架屏" - **后果**:筛选切换时无加载反馈 #### P1-7 无关键操作埋点 - **位置**:全模块 - **违反规则**:`project_rules.md` → "监控:方案中预留关键操作埋点接口" - **原因**:导出操作、日志查看无 `trackEvent` 调用 - **后果**:无法统计审计功能使用情况、无法监控异常导出行为 #### P1-8 a11y 缺失 - **位置**:表格无 `aria-label`/`caption`;筛选 Select 无 `aria-label`;展开按钮无 `aria-expanded`;导出按钮无 `aria-label` - **违反规则**:`project_rules.md` → "可访问性(a11y):语义化标签、ARIA 属性、键盘导航" ### P2 — 改进项 #### P2-1 data-access 错误吞没,UI 无法区分"空数据"与"查询失败" - **位置**:`data-access.ts` 所有函数 catch 块返回空数组 - **后果**:DB 故障时用户看到"无日志"而非错误提示 #### P2-2 无日志详情视图 - **位置**:审计日志表格仅展示摘要,`detail` 字段(JSON)无法查看 - **差距**:行业标配支持点击行展开/弹窗查看完整日志详情 #### P2-3 无用户维度筛选 - **位置**:`audit-log-filters.tsx` 仅支持 module/action/status/date,不支持按用户搜索 - **差距**:PowerSchool/Veracross 支持按用户筛选所有日志 #### P2-4 无审计概览仪表盘 - **位置**:无统计概览页 - **差距**:行业产品提供"今日事件数/失败登录数/数据变更数"概览卡片 + 活动趋势图 #### P2-5 无数据保留策略 - **位置**:审计日志无 TTL/归档机制 - **差距**:企业级产品支持可配置保留期(如 90/180/365 天) #### P2-6 无单测 - **位置**:无 `*.test.ts` 文件 - **违反规则**:`project_rules.md` → "可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks" #### P2-7 api/export/route.ts 中 `as Record` 类型断言 - **位置**:`route.ts:145` - **违反规则**:`project_rules.md` → "禁止 `as` 断言(除类型收窄外)" #### P2-8 无 Service 接口抽象 / 依赖注入 - **位置**:页面直接调 data-access,组件直接收 props - **违反规则**:任务要求 → "完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务" - **说明**:当前 RSC + props 模式可工作,但不满足任务要求的 DI 架构,且无法在客户端组件中 mock 数据源 --- ## 三、行业差距对比 | 能力 | PowerSchool / Veracross | Google Workspace Audit | Microsoft Purview | 本系统现状 | 影响 | |------|------------------------|----------------------|-------------------|-----------|------| | 统一审计概览仪表盘 | ✅ 今日/本周事件数 + 趋势图 | ✅ 活动时间线 | ✅ 合规概览 | ❌ 无 | 管理员无法快速掌握系统活动全貌 | | 按用户筛选 | ✅ | ✅ | ✅ | ❌ 仅 module/action/status | 无法追踪特定用户操作轨迹 | | 日志详情视图 | ✅ 点击展开完整 JSON | ✅ 详情面板 | ✅ 活动详情 | ❌ 仅表格摘要 | 审计人员无法查看 detail 字段 | | 数据变更 diff 可视化 | ✅ 左右对比 + 高亮 | ✅ | ✅ 差异高亮 | ⚠️ 纯文本 pre 展示 | 变更审查效率低 | | 失败登录监控/告警 | ✅ 异常登录告警 | ✅ 可疑活动检测 | ✅ 实时告警 | ❌ 无 | 无法及时发现暴力破解 | | 导出调度/定时 | ✅ 计划报告 | ✅ 导出 + 邮件 | ✅ 合规报告 | ❌ 仅手动导出 | 合规审计需人工操作 | | 数据保留策略 | ✅ 可配置保留期 | ✅ | ✅ | ❌ 无限增长 | 存储成本持续上升 | | IP 地理位置 | ✅ | ✅ | ✅ | ❌ 仅显示 IP | 无法判断异地登录 | | 多角色审计视图 | ✅ 管理员/合规官分级 | ✅ | ✅ | ⚠️ 仅 admin 单角色 | 未来合规官角色需独立视图 | | i18n | ✅ 多语言 | ✅ | ✅ | ❌ 全英文硬编码 | 中文用户体验差 | --- ## 四、改进优先级建议 ### P0 — 立即修复(合规与基础体验) | 编号 | 问题 | 改进方向 | |------|------|---------| | P0-1 | i18n 全缺失 | 补全 `audit.json` 翻译键(表头/筛选器/空状态/按钮/Toast),所有组件接入 `useTranslations("audit")` | | P0-2 | 缺 loading/error.tsx | 三个路由各新增 `loading.tsx`(骨架屏)+ `error.tsx`(错误边界 + 重试) | | P0-3 | 架构图不一致 | 同步 004/005:修正 actions 清单、补全组件清单、更新行数 | ### P1 — 本轮实施(架构规范) | 编号 | 问题 | 改进方向 | |------|------|---------| | P1-1 | formatDate 硬编码 locale | 改用 `useLocale()` 获取当前 locale | | P1-2 | handlePageChange 重复 | 抽取 `useLogPagination` hook 到 `hooks/` | | P1-3 | 导出逻辑内联 | 抽取 `export.ts`,列定义+行映射移至独立文件 | | P1-4 | DataChangeLogTable 混合 | 拆分为 `data-change-log-table.tsx` + `data-change-log-filters.tsx` | | P1-5 | 无 Error Boundary | 新增 `audit-error-boundary.tsx`,包裹各数据区块 | | P1-6 | Suspense 无骨架屏 | fallback 改为 `AuditLogTableSkeleton` | | P1-7 | 无埋点 | 导出/查看操作新增 `trackEvent` | | P1-8 | a11y 缺失 | 表格加 caption/aria-label,Select 加 aria-label,展开按钮加 aria-expanded | ### P2 — 中长期迭代(功能增强) | 编号 | 问题 | 改进方向 | |------|------|---------| | P2-1 | 错误吞没 | data-access 抛出业务错误,actions 层捕获返回 ActionState.error | | P2-2 | 无详情视图 | 新增 `audit-log-detail-dialog.tsx`,展示完整 detail JSON | | P2-3 | 无用户筛选 | 筛选器新增用户搜索 Input | | P2-4 | 无概览仪表盘 | 新增 `/admin/audit-logs/overview` 概览页(统计卡片+趋势图) | | P2-5 | 无保留策略 | 新增 `audit-retention-config` + 定时清理 job | | P2-6 | 无单测 | 为纯函数(分页计算、格式化、列映射)添加单测 | | P2-7 | as 断言 | 改用类型守卫 | | P2-8 | 无 Service 抽象 | 定义 `AuditService` 接口 + Context Provider 注入 | --- ## 五、架构图同步说明 本次审计发现架构图以下遗漏/不一致,需在实现后同步更新: ### 004_architecture_impact_map.md(2.15 节) 1. **修正 actions 清单**:删除不存在的 `getAuditLogsAction` / `getLoginLogsAction`;补充说明页面直接调 data-access 2. **补全组件清单**:新增 `data-change-log-table.tsx`、`login-log-view.tsx`、`audit-log-export-button.tsx`、`data-change-log-filters.tsx`(拆分后)、`audit-error-boundary.tsx`(新增)、`audit-log-table-skeleton.tsx`(新增) 3. **更新行数**:actions.ts 214→拆分后;data-access.ts 290;新增 export.ts、hooks/ 4. **新增 hooks 清单**:`use-log-pagination.ts` 5. **更新已知问题**:标记 P0-1~P1-8 已修复 ### 005_architecture_data.json(audit 节点) 1. **修正 actions**:删除 `getAuditLogsAction`/`getLoginLogsAction`;`usedBy` 更新为 `api/export/route.ts` 2. **补全 components**:新增缺失组件 3. **新增 hooks 节点**:`useLogPagination` 4. **新增 export 节点**:`export.ts` --- ## 六、重构方案设计 ### a. 新文件/目录结构 ``` src/modules/audit/ ├─ actions.ts # Server Actions(编排层,精简) ├─ data-access.ts # 数据访问层(查询) ├─ export.ts # 🆕 Excel 导出(列定义+行映射+buildExcelExport) ├─ types.ts # 类型定义 + 状态映射常量 ├─ hooks/ │ └─ use-log-pagination.ts # 🆕 分页 URL 状态 hook(消除三处重复) ├─ components/ │ ├─ audit-log-view.tsx # 操作日志视图(i18n + ErrorBoundary + Suspense) │ ├─ audit-log-table.tsx # 操作日志表格(i18n + a11y) │ ├─ audit-log-filters.tsx # 操作日志筛选器(i18n + a11y) │ ├─ audit-log-export-button.tsx # 导出按钮(i18n + trackEvent) │ ├─ login-log-view.tsx # 登录日志视图 │ ├─ login-log-table.tsx # 登录日志表格 │ ├─ login-log-filters.tsx # 登录日志筛选器 │ ├─ data-change-log-view.tsx # 🆕 数据变更视图(拆分自 table) │ ├─ data-change-log-table.tsx # 数据变更表格(仅表格,不含筛选) │ ├─ data-change-log-filters.tsx# 🆕 数据变更筛选器(拆分) │ ├─ audit-error-boundary.tsx # 🆕 错误边界 │ └─ audit-log-table-skeleton.tsx # 🆕 骨架屏 ├─ i18n/ │ └─ (由 shared/i18n/messages/{locale}/audit.json 统一管理) └─ lib/ └─ audit-columns.ts # 🆕 导出列定义(纯函数,可单测) ``` ### b. 核心代码示例 #### 数据服务接口定义(P2-8,中期实施) ```typescript // src/modules/audit/services/audit-service.ts export interface AuditService { getAuditLogs(params?: AuditLogQueryParams): Promise> getLoginLogs(params?: LoginLogQueryParams): Promise> getDataChangeLogs(params?: DataChangeLogQueryParams): Promise> getAuditModuleOptions(): Promise getDataChangeStats(): Promise getDataChangeTableOptions(): Promise } // 角色实现示例(中期) export class AdminAuditService implements AuditService { // 封装 data-access 调用,权限已在 Server Action 层校验 async getAuditLogs(params?: AuditLogQueryParams) { return getAuditLogs(params) } // ...其他方法委托给 data-access } ``` #### 通用分页 Hook(P1-2,本轮实施) ```typescript // src/modules/audit/hooks/use-log-pagination.ts "use client" import { useRouter, useSearchParams } from "next/navigation" import { useCallback } from "react" export function useLogPagination(): (page: number) => void { const router = useRouter() const searchParams = useSearchParams() return useCallback((newPage: number) => { const params = new URLSearchParams(searchParams.toString()) if (newPage <= 1) params.delete("page") else params.set("page", String(newPage)) const query = params.toString() router.push(query ? `?${query}` : "?") }, [router, searchParams]) } ``` #### 导出模块抽取(P1-3,本轮实施) ```typescript // src/modules/audit/export.ts import { exportToExcel, type ExcelColumn } from "@/shared/lib/excel" import { formatDateForFile } from "@/shared/lib/utils" import type { AuditLog, LoginLog, DataChangeLog } from "./types" export const AUDIT_LOG_COLUMNS: ExcelColumn[] = [ { header: "User ID", key: "userId", width: 22 }, // ...列定义 ] export function mapAuditLogsToRows(items: AuditLog[]) { return items.map((r) => ({ /* ...映射 */ })) } export async function buildAuditLogExport(items: AuditLog[]) { const buffer = await exportToExcel({ sheets: [{ name: "Audit Logs", columns: AUDIT_LOG_COLUMNS, rows: mapAuditLogsToRows(items) }], }) return { buffer, filename: `audit_logs_${formatDateForFile()}.xlsx` } } ``` #### ErrorBoundary 包裹(P1-5,本轮实施) ```tsx // src/modules/audit/components/audit-error-boundary.tsx "use client" import { Component, type ReactNode } from "react" import { useTranslations } from "next-intl" interface Props { children: ReactNode } interface State { hasError: boolean } export class AuditErrorBoundary extends Component { state: State = { hasError: false } static getDerivedStateFromError(): State { return { hasError: true } } render() { if (this.state.hasError) { return this.setState({ hasError: false })} /> } return this.props.children } } ``` #### 角色组装页面(配置驱动,中期) ```tsx // 未来扩展:通过配置决定渲染哪些 Widget const AUDIT_WIDGET_CONFIG = { admin: ["overviewStats", "auditLogTable", "loginLogTable", "dataChangeTable"], compliance: ["auditLogTable", "dataChangeTable"], } as const ``` ### c. 解耦与测试说明 - **纯函数抽取**:分页计算(`clampPage`/`clampPageSize`)、列映射(`mapAuditLogsToRows`)、状态映射常量已独立于 UI,可直接单测 - **Hook 测试**:`useLogPagination` 通过 mock `next/navigation` 的 `useRouter`/`useSearchParams` 测试 - **组件测试**:`AuditLogTable` 接收纯 props,传入 mock 数据即可渲染测试 - **Mock service 示例**: ```typescript const mockEmptyService: AuditService = { getAuditLogs: async () => ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }), // ...其他返回空数据 } ``` ### d. i18n 集成示例 翻译文件结构(`shared/i18n/messages/zh-CN/audit.json`): ```json { "title": "审计日志", "description": "追踪系统内所有用户操作,保障安全与合规。", "table": { "user": "用户", "module": "模块", "action": "操作", "target": "目标", "status": "状态", "ipAddress": "IP 地址", "time": "时间", "userAgent": "用户代理", "tableName": "数据表", "recordId": "记录 ID", "changedBy": "操作人", "view": "查看", "hide": "隐藏", "oldValue": "旧值", "newValue": "新值" }, "filter": { "anyModule": "任意模块", "anyStatus": "任意状态", "anyAction": "任意操作", "anyTable": "任意数据表", "actionPlaceholder": "操作...", "success": "成功", "failure": "失败", "create": "创建", "update": "更新", "delete": "删除", "signIn": "登录", "signOut": "登出", "signUp": "注册", "reset": "重置" }, "empty": { "audit": "暂无审计日志", "login": "暂无登录日志", "dataChange": "暂无数据变更日志" }, "export": { "button": "导出 Excel", "success": "导出成功", "failed": "导出失败" }, "error": { "title": "加载失败", "description": "数据加载时发生错误,请稍后重试。", "retry": "重试" }, "loginLogs": { "title": "登录日志", "description": "..." }, "dataChanges": { "title": "数据变更日志", "description": "..." } } ``` 组件使用: ```tsx const t = useTranslations("audit") {t("table.user")} ``` ### e. 错误处理与加载状态示例 ```tsx // page.tsx }> ``` ### 最终检查 - [x] 该模块不存在对其他业务模块的直接 import(仅依赖 shared/*) - [x] 没有使用 `any` 或硬编码角色字符串 - [x] 所有 actions 包含 `requirePermission` 调用(4 个 Action 均有) - [x] 文件行数未超过建议上限(最大 data-access.ts 290 行 < 800) - [x] 架构影响地图需同步更新(见第五节)