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

22 KiB
Raw Blame History

审计模块审计报告

审计范围:src/modules/audit/**src/app/(dashboard)/admin/audit-logs/**src/app/api/export/route.ts(审计导出分支)、src/shared/lib/audit-logger.tssrc/shared/lib/change-logger.tssrc/shared/i18n/messages/{zh-CN,en}/audit.json 审计日期2026-06-24 审计依据:docs/architecture/004_architecture_impact_map.md2.15 节)、docs/architecture/005_architecture_data.jsonaudit 节点)、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/exportexportAuditLogsActiongetAuditLogsForExport → Excel buffer。

1.3 架构图记录情况

  • 0042.15 节):记录了 audit 模块,但存在不一致:声称 actions 层有 getAuditLogsAction / getLoginLogsAction,实际不存在这两个 Action页面直接调 data-access。组件清单不完整data-change-log-table.tsxlogin-log-view.tsxaudit-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.tsxlogin-log-filters.tsxdata-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.tsxerror.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:92login-log-table.tsx:86data-change-log-table.tsx:116
  • 违反规则i18n 就绪要求
  • 原因:直接传 "zh-CN" 而非从 next-intl 获取当前 locale
  • 后果:英文用户看到中文格式日期

P1-2 分页 handlePageChange 三处重复

  • 位置audit-log-view.tsx:29-38login-log-view.tsx:27-36data-change-log-table.tsx:58-67
  • 违反规则project_rules.md → "最大化复用"
  • 原因:三处 100% 相同的 URL searchParams 操作逻辑未抽取
  • 后果:维护需改三处,易遗漏

P1-3 导出逻辑内联在 actions 层,三个导出 Action 结构高度重复

  • 位置actions.ts:90-214exportAuditLogsAction / 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:57login-log-view.tsx:55data-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.tsxlogin-log-view.tsxaudit-log-export-button.tsxdata-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/getLoginLogsActionusedBy 更新为 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中期实施

// 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本轮实施

// 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本轮实施

// 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本轮实施

// 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
  }
}

角色组装页面(配置驱动,中期)

// 未来扩展:通过配置决定渲染哪些 Widget
const AUDIT_WIDGET_CONFIG = {
  admin: ["overviewStats", "auditLogTable", "loginLogTable", "dataChangeTable"],
  compliance: ["auditLogTable", "dataChangeTable"],
} as const

c. 解耦与测试说明

  • 纯函数抽取:分页计算(clampPage/clampPageSize)、列映射(mapAuditLogsToRows)、状态映射常量已独立于 UI可直接单测
  • Hook 测试useLogPagination 通过 mock next/navigationuseRouter/useSearchParams 测试
  • 组件测试AuditLogTable 接收纯 props传入 mock 数据即可渲染测试
  • Mock service 示例
const mockEmptyService: AuditService = {
  getAuditLogs: async () => ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }),
  // ...其他返回空数据
}

d. i18n 集成示例

翻译文件结构(shared/i18n/messages/zh-CN/audit.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": "..." }
}

组件使用:

const t = useTranslations("audit")
<TableHead>{t("table.user")}</TableHead>
<EmptyTableRow colSpan={7} message={t("empty.audit")} />

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

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

最终检查

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