Files
NextEdu/docs/architecture/audit/archive/audit-module-audit-report.md

437 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 审计模块审计报告
> 审计范围:`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 Action1 查询 + 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 架构图记录情况
- **0042.15 节)**:记录了 audit 模块,但存在**不一致**:声称 actions 层有 `getAuditLogsAction` / `getLoginLogsAction`,实际**不存在**这两个 Action页面直接调 data-access。组件清单不完整`data-change-log-table.tsx``login-log-view.tsx``audit-log-export-button.tsx`)。
- **005audit 节点)**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 实际 214data-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-labelSelect 加 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.md2.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.jsonaudit 节点)
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
}
```
#### 通用分页 HookP1-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] 架构影响地图需同步更新(见第五节)