# 审计模块审计报告
> 审计范围:`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] 架构影响地图需同步更新(见第五节)