# 审计模块审计报告

> 审计范围：`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)
  └─ <AuditLogView items={...} />  (Client Component)
       ├─ <AuditLogFilters>  (nuqs URL 状态)
       └─ <AuditLogTable>    (分页 + 空状态)
```

导出流：`<AuditLogExportButton>` → `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` 均直接渲染 `<AuditLogView>` / `<DataChangeLogTable>` 无 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<string, string>` 类型断言
- **位置**：`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<PaginatedResult<AuditLog>>
  getLoginLogs(params?: LoginLogQueryParams): Promise<PaginatedResult<LoginLog>>
  getDataChangeLogs(params?: DataChangeLogQueryParams): Promise<PaginatedResult<DataChangeLog>>
  getAuditModuleOptions(): Promise<string[]>
  getDataChangeStats(): Promise<DataChangeStat[]>
  getDataChangeTableOptions(): Promise<string[]>
}

// 角色实现示例（中期）
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<Props, State> {
  state: State = { hasError: false }
  static getDerivedStateFromError(): State { return { hasError: true } }
  render() {
    if (this.state.hasError) {
      return <AuditErrorFallback onRetry={() => 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")
<TableHead>{t("table.user")}</TableHead>
<EmptyTableRow colSpan={7} message={t("empty.audit")} />
```

### e. 错误处理与加载状态示例

```tsx
// page.tsx
<AuditErrorBoundary>
  <Suspense fallback={<AuditLogTableSkeleton />}>
    <AuditLogView items={result.items} /* ... */ />
  </Suspense>
</AuditErrorBoundary>
```

### 最终检查

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