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

23 KiB
Raw Blame History

审计模块审计报告 v2

审计范围:src/modules/audit/**src/app/(dashboard)/admin/audit-logs/**src/app/api/export/route.ts(审计导出分支)、src/app/api/cron/audit-cleanup/route.tssrc/shared/lib/audit-logger.tssrc/shared/lib/change-logger.tssrc/shared/i18n/messages/{zh-CN,en}/audit.json 审计日期2026-06-25 审计依据:docs/architecture/004_architecture_impact_map.md2.15 节)、docs/architecture/005_architecture_data.jsonaudit 节点)、docs/standards/coding-standards.md、项目 project_rules.md 前置报告:docs/architecture/audit/audit-module-audit-report.mdv12026-06-24


一、现有实现概要

1.1 文件分布

v1 审计后已完成两轮重构P0-P2当前文件分布如下

文件 行数 职责
路由 app/(dashboard)/admin/audit-logs/page.tsx 79 操作日志列表页RSC调 data-access
路由 app/(dashboard)/admin/audit-logs/login-logs/page.tsx 79 登录日志列表页
路由 app/(dashboard)/admin/audit-logs/data-changes/page.tsx 83 数据变更日志列表页
路由 app/(dashboard)/admin/audit-logs/overview/page.tsx 34 审计概览仪表盘( v1 P2-4 新增)
路由 4 个 loading.tsx + 4 个 error.tsx 骨架屏 + 错误边界( v1 P0-2 已修复)
路由 app/api/export/route.ts 202 统一导出 API含 audit/login/dataChange 三分支)
路由 app/api/cron/audit-cleanup/route.ts 75 定时清理 cron v1 P2-5 新增)
actions modules/audit/actions.ts 195 7 个 Server Action1 查询 + 3 导出 + 3 保留策略)
data-access modules/audit/data-access.ts 387 12 个查询函数(分页 + 导出 + 选项 + 统计 + 趋势)
export modules/audit/export.ts 133 Excel 列定义 + 行映射 + buildExport v1 P1-3 已抽取)
retention modules/audit/retention.ts 123 保留策略配置读写 + 清理 + 纯函数( v1 P2-5 新增)
types modules/audit/types.ts 145 类型定义 + 状态映射常量
services modules/audit/services/audit-service.tsx 124 AuditService 接口 + Context + hook v1 P2-8 新增)
services modules/audit/services/admin-audit-service.ts 40 管理员默认实现
services modules/audit/services/mock-audit-service.ts 57 测试用 Mock 实现
hooks modules/audit/hooks/use-log-pagination.ts 24 分页 URL 状态 hook v1 P1-2 已抽取)
组件 15 个组件文件 19-191 表格/筛选器/视图/详情对话框/概览/图表/保留配置/骨架屏/错误边界
测试 export.test.ts / retention.test.ts 191/81 纯函数单测( v1 P2-6 已补充)
i18n shared/i18n/messages/{zh-CN,en}/audit.json 130 完整翻译键table/filter/empty/export/error/detail/overview/retention
shared shared/lib/audit-logger.ts / change-logger.ts logAudit() / logDataChange() 写入日志

1.2 数据流

page.tsx (RSC)
  ├─ requirePermission(AUDIT_LOG_READ)
  ├─ getAuditLogs/getLoginLogs/getDataChangeLogs (data-access 直调)
  └─ <AuditErrorBoundary>
       <AuditLogView items={...} />  (Client Component)
         ├─ <AuditLogFilters>  (nuqs URL 状态)
         └─ <AuditLogTable>    (分页 + 空状态 + 详情对话框)

overview/page.tsx (RSC)
  ├─ requirePermission(AUDIT_LOG_READ)
  ├─ getAuditOverviewStats / getAuditTrend / getDataChangeActionStats
  └─ <AuditOverviewView>  (RSC)
       ├─ <AuditOverviewStatsBar>
       ├─ <AuditActivityTrendChart>
       ├─ <DataChangeDistributionChart>
       └─ <AuditRetentionSettings>  (Client Component直调 Server Actions)

导出流:<AuditLogExportButton>fetch /api/exportexportAuditLogsActiongetAuditLogsForExportbuildAuditLogExport → Excel buffer。

定时清理流:/api/cron/audit-cleanupCRON_SECRET 鉴权)→ getAuditRetentionConfigpurgeExpiredAuditLogs

1.3 架构图记录情况

  • 0042.15 节):记录较完整,含 services/hooks/export/retention 节点,行数标注准确。
  • 005audit 节点)data-access/actions/types 记录完整,含 services/hooks/export/retention 节点。
  • 已知不一致services 层的 AuditServiceProvider 在架构图中标注为"已接入",但实际未被任何页面使用(详见 P0-1

二、现存问题与原因分析

P0 — 严重违规

P0-1 Service 抽象层定义但从未使用(虚假完成)

  • 位置services/audit-service.tsxAuditServiceProvideruseAuditServiceuseAuditAnalytics)、services/admin-audit-service.tsservices/mock-audit-service.ts
  • 违反规则:任务要求 → "完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context或组合 Provider注入数据服务模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"project_rules.md → "架构图优先规则:改码必同步图"
  • 问题
    1. 全局搜索 AuditServiceProvider 的实际使用:仅在 audit-service.tsxmock-audit-service.ts 的 JSDoc @example 中出现,没有任何页面或组件实际注入
    2. 4 个 page.tsx 均直接 import { getAuditLogs, ... } from "@/modules/audit/data-access"
    3. audit-retention-settings.tsx 直接 import { getAuditRetentionConfigAction, ... } from "@/modules/audit/actions"
    4. audit-log-export-button.tsx 直接 fetch("/api/export") 绕过 Service 层
    5. 架构图 004 第 1468 行声称" P2-8 已修复:无 Service 接口抽象",实际为"定义但未接线"
  • 原因P2-8 仅创建了接口文件,未在页面层注入 Provider、未将组件改造为从 Context 获取 service
  • 后果
    • DI 架构形同虚设,组件仍硬编码依赖 data-access无法在测试中注入 mock
    • 架构图声称已修复,误导后续审计与维护决策
    • mockAuditService 完全无引用(死代码)

P0-2 mock-audit-service.ts 使用 9 处 as 类型断言

  • 位置services/mock-audit-service.ts 第 29、31、33、36、45、46、47、53、60 行
  • 违反规则project_rules.md → "禁止 as 断言(除类型收窄外)"project_memory.md → "TypeScript strict mode: no any, no as assertions (except for type narrowing)"
  • 示例
    getAuditLogs: async () =>
      ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult<AuditLog>,
    
  • 原因:对象字面量缺少 items: [] as AuditLog[] 类型标注,导致需要 as 断言整个返回值
  • 后果:违反 TS 严格规则lint 通过但 code review 应拦截

P0-3 全部 7 个 Server Action 缺少 revalidatePath

  • 位置actions.ts 所有 7 个 ActiongetDataChangeLogsActionexportAuditLogsActionexportLoginLogsActionexportDataChangeLogsActiongetAuditRetentionConfigActionsaveAuditRetentionConfigActionpurgeAuditLogsAction
  • 违反规则project_rules.md → "Server Action 规范:使用 revalidatePath 精确刷新缓存"
  • 问题saveAuditRetentionConfigActionpurgeAuditLogsAction 修改数据后未刷新 overview 页面缓存,用户保存保留策略后看到的仍是旧配置
  • 原因:遗漏
  • 后果:保留策略变更后概览页缓存不刷新,显示过期数据

P1 — 重要缺陷

P1-1 trackEvent event 名称误用(保留策略操作误标为导出)

  • 位置actions.ts 第 170、199 行(saveAuditRetentionConfigActionpurgeAuditLogsAction)、app/api/cron/audit-cleanup/route.ts 第 54 行
  • 违反规则:任务要求 → "监控:方案中预留关键操作埋点接口"
  • 问题:保留策略的保存/清理操作使用 event: "audit.exported",语义错误
    // saveAuditRetentionConfigAction
    await trackEvent({ event: "audit.exported", ... properties: { action: "save_config" } })
    // purgeAuditLogsAction
    void trackEvent({ event: "audit.exported", ... properties: { action: "purge" } })
    // cron route
    void trackEvent({ event: "audit.exported", ... properties: { action: "cron_purge" } })
    
  • 后果:分析平台中所有保留策略操作被归类为"导出",无法区分导出与清理行为

P1-2 AuditErrorBoundary 使用错误的 i18n namespace

  • 位置components/audit-error-boundary.tsx 第 19 行 namespace="common"
  • 违反规则project_rules.md → "所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"
  • 问题SectionErrorBoundary 读取 t("error.boundaryTitle") / t("error.boundaryDescription") / t("error.retry")audit.json 定义了 error.title / error.description / error.retry(键名不匹配),而 common.jsonerror.boundaryTitle / error.boundaryDescription。当前传 "common" 可工作但使用的是通用错误文案,未利用 audit 专属的 error.description"数据加载时发生错误,请稍后重试。"
  • 后果:区块级错误边界显示通用错误文案而非审计模块专属文案

P1-3 AuditAnalytics 接口定义为死代码

  • 位置services/audit-service.tsx 第 64-84 行(AuditAnalytics 接口 + noopAnalytics + AuditAnalyticsContext + useAuditAnalytics hook
  • 违反规则:任务要求 → "监控:方案中预留关键操作埋点接口"
  • 问题useAuditAnalytics hook 全局搜索仅出现在定义处,无任何组件调用trackLogView / trackExport / trackOverviewView / trackRetentionConfigChange / trackPurge 五个埋点方法均为死代码
  • 后果:客户端层面无日志查看/导出/概览查看埋点,无法统计用户行为

P1-4 data-access 错误处理不一致

  • 位置data-access.ts
    • 吞没错误(返回空数组):getAuditModuleOptions(第 152 行)、getDataChangeTableOptions(第 237 行)
    • 抛出错误getAuditLogsgetLoginLogsgetDataChangeLogsgetDataChangeStatsgetAuditOverviewStatsgetAuditTrendgetDataChangeActionStats
  • 违反规则project_rules.md → "错误与边界处理:明确处理空数据、无权限、网络异常等边界状态"
  • 后果DB 故障时筛选器选项静默显示为空,用户误认为"无数据"而非"查询失败"

P1-5 日期筛选器 aria-label 不区分起止

  • 位置audit-log-filters.tsx 第 94、101 行;login-log-filters.tsx 第 78、85 行;data-change-log-filters.tsx 第 117、124 行
  • 违反规则project_rules.md → "可访问性a11y语义化标签、ARIA 属性、键盘导航"
  • 问题:两个日期 Input 均使用 aria-label={t("table.time")}"时间"),屏幕阅读器无法区分"开始日期"与"结束日期"
  • 后果:视障用户无法区分两个日期输入框

P1-6 AuditLogDetailDialog DialogDescription 重复标题

  • 位置components/audit-log-detail-dialog.tsx 第 135-137 行
  • 问题
    <DialogTitle>{t("detail.title")}</DialogTitle>
    <DialogDescription className="sr-only">{t("detail.title")}</DialogDescription>
    
    Description 与 Title 完全相同,无信息增量
  • 后果:无障碍辅助技术读出重复信息

P1-7 骨架屏缺少 aria-hidden

  • 位置components/audit-log-table-skeleton.tsx全文件、4 个 loading.tsx
  • 违反规则project_rules.md → "可访问性a11y"
  • 问题Skeleton 元素无 aria-hidden="true",屏幕阅读器会逐个朗读骨架占位块
  • 后果:加载期间屏幕阅读器体验差

P1-8 window.confirm 阻塞式确认

  • 位置components/audit-retention-settings.tsx 第 80 行
  • 问题window.confirm(t("purgeConfirm")) 使用浏览器原生确认框不可自定义样式、阻塞主线程、a11y 差
  • 后果:与其他模块(使用 AlertDialog 组件)交互不一致

P2 — 改进项

P2-1 数据变更 diff 无可视化

  • 位置data-change-log-table.tsx 第 118-129 行使用 <pre> 纯文本展示 oldValue / newValue
  • 差距PowerSchool / Google Workspace Audit / Microsoft Purview 均提供 JSON diff 高亮(增行绿、删行红、左右对比)

P2-2 无失败登录异常告警

  • 位置:全模块
  • 差距:行业产品支持阈值告警(如 5 分钟内同一 IP 失败登录 > 10 次触发告警)

P2-3 无 IP 地理位置

  • 位置:表格仅展示 IP 字符串
  • 差距:行业产品将 IP 解析为地理位置(城市/国家),辅助判断异地登录

P2-4 概览趋势仅 7 天不可配置

  • 位置overview/page.tsx 第 29 行 getAuditTrend(7) 硬编码 7 天
  • 差距:行业产品支持 7/30/90 天切换

P2-5 多学校数据隔离缺失

  • 位置data-access.ts 所有查询无 schoolId 过滤
  • 说明audit_logs / login_logs / data_change_logs 表无 school_id 字段schema 确认audit 模块设计为系统级跨校审计。当前 AUDIT_LOG_READ 权限若仅授予超级管理员则可接受,但需在文档中明确标注此设计决策
  • 风险:若未来授予校级管理员 AUDIT_LOG_READ,将导致跨校数据泄露

P2-6 useLogPagination hook 无单测

  • 位置hooks/use-log-pagination.ts
  • 违反规则project_rules.md → "可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks"
  • 说明v1 已补充 export.test.ts 和 retention.test.ts但 hook 层无单测

P2-7 导出按钮无进度反馈

  • 位置audit-log-export-button.tsx
  • 问题:大范围导出仅显示 spinner无进度条/计数
  • 差距:行业产品显示"已导出 X / Y 条"

三、行业差距对比

能力 PowerSchool / Veracross Google Workspace Audit Microsoft Purview 本系统现状 影响
统一审计概览仪表盘 已有概览页
按用户/模块/动作/状态筛选 已有完整筛选器
日志详情视图 点击展开完整 JSON 详情面板 活动详情 已有详情对话框
数据变更 diff 可视化 左右对比 + 高亮 差异高亮 纯文本 pre 展示 变更审查效率低
失败登录监控/告警 异常登录告警 可疑活动检测 实时告警 仅展示无告警 无法及时发现暴力破解
导出调度/定时 计划报告 导出 + 邮件 合规报告 仅手动导出 合规审计需人工操作
数据保留策略 可配置保留期 已有保留策略配置
IP 地理位置 仅显示 IP 无法判断异地登录
多角色审计视图 管理员/合规官分级 ⚠️ 仅 admin 单角色 + Service 接口预留 合规官角色需独立视图
i18n 多语言 已完整 i18n
概览趋势可配置时间范围 7/30/90 天 硬编码 7 天 无法查看长期趋势
DI 架构(可测试) ⚠️ 定义未接线 组件无法 mock 数据源

四、改进优先级建议

P0 — 立即修复(合规与架构正确性)

编号 问题 改进方向
P0-1 Service 抽象层未接线 overview 页面接入 AuditServiceProviderAuditOverviewView 内组件改用 useAuditService() 获取数据;或若评估后认为 RSC + props 模式已满足需求则删除未使用的 services 层避免误导
P0-2 mock-audit-service.ts 9 处 as 断言 为对象字面量添加元素类型标注(如 items: [] as AuditLog[] → 改用 items: new Array<AuditLog>() 或显式标注返回类型)
P0-3 7 个 Action 缺 revalidatePath 写操作saveAuditRetentionConfigAction、purgeAuditLogsAction添加 revalidatePath("/admin/audit-logs/overview")

P1 — 本轮实施(规范与体验)

编号 问题 改进方向
P1-1 trackEvent event 误标 保留策略操作改用 event: "audit.retention"
P1-2 ErrorBoundary namespace 错误 audit.json 新增 error.boundaryTitle / error.boundaryDescriptionAuditErrorBoundarynamespace="audit"
P1-3 AuditAnalytics 死代码 组件中调用 useAuditAnalytics() 接入客户端埋点(日志查看、概览查看、导出、保留策略变更)
P1-4 data-access 错误处理不一致 getAuditModuleOptions / getDataChangeTableOptions 改为抛出错误
P1-5 日期 aria-label 不区分 新增 i18n filter.startDate / filter.endDate,替换 t("table.time")
P1-6 DialogDescription 重复 新增 detail.description 翻译键
P1-7 骨架屏缺 aria-hidden Skeleton 容器添加 aria-hidden="true"
P1-8 window.confirm 替换为 AlertDialog 组件(与其他模块一致)

P2 — 中长期迭代(功能增强)

编号 问题 改进方向
P2-1 diff 无可视化 引入 JSON diff 组件(左右对比 + 增删高亮)
P2-2 无失败登录告警 新增异常检测规则 + 告警通知
P2-3 无 IP 地理位置 接入 IP 反查服务(如 MaxMind GeoLite2
P2-4 趋势不可配置 概览页新增 7/30/90 天切换按钮
P2-5 多校数据隔离 文档标注"系统级审计"设计决策;若需校级审计则新增 school_id 字段
P2-6 hook 无单测 useLogPagination 补充单测
P2-7 导出无进度 大范围导出显示进度条

五、架构图同步说明

本次审计发现架构图以下遗漏/不一致,需在实现后同步更新:

004_architecture_impact_map.md2.15 节)

  1. 修正 services 层状态:第 1468 行" P2-8 已修复"改为"P2-8 已定义接口P0-1 待接线"(实施后改回"已接线"
  2. 修正 trackEvent 埋点描述:保留策略操作的 event 名称从 audit.exported 改为 audit.retention
  3. 新增 revalidatePath 说明actions 节标注 saveAuditRetentionConfigAction / purgeAuditLogsAction 已添加 revalidatePath

005_architecture_data.jsonaudit 节点)

  1. services 节点:标注 AuditServiceProviderusedBy(实施后从空改为 overview page
  2. actions 节点:更新 saveAuditRetentionConfigAction / purgeAuditLogsAction 的 trackEvent event 名称
  3. components 节点:更新 audit-error-boundary.tsx 的 namespace 配置

六、重构方案设计

a. P0-1 Service 抽象层接线方案

决策:保留 Service 接口(满足任务要求的 DI 架构),在 overview 页面接线。

由于 AuditOverviewView 是 RSCasync server componentAuditServiceProvider 是 Client Component"use client"),无法直接在 RSC 中使用 Context Provider。因此采用混合模式

  1. RSC 页面:仍由 page.tsx 调用 data-access 获取初始数据(保留 SSR 性能优势)
  2. 客户端交互组件AuditRetentionSettings):通过 AuditServiceProvider 注入 service组件内部用 useAuditService() 获取数据
  3. adminAuditService 改为可被 Client Component 引用的轻量包装(仅委托给 Server Action不直接 import server-only 的 data-access
// services/admin-audit-service.ts —— 改造为 Client-safe 实现
// 不 import "server-only",改为委托 Server Actions
import { getAuditRetentionConfigAction, saveAuditRetentionConfigAction, purgeAuditLogsAction } from "../actions"

export const adminAuditService: AuditService = {
  // 保留策略相关:通过 Server Action 调用
  getAuditRetentionConfig: async () => {
    const res = await getAuditRetentionConfigAction()
    if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
    return res.data
  },
  saveAuditRetentionConfig: async (config) => {
    const res = await saveAuditRetentionConfigAction(config)
    if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
  },
  purgeExpiredAuditLogs: async (retentionDays) => {
    const res = await purgeAuditLogsAction(retentionDays)
    if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
    return res.data
  },
  // 查询类RSC 页面已通过 props 传入,客户端不需要
  // ...其余方法可暂不实现或抛错
}

AuditRetentionSettings 改造:

// 在组件内部使用 useAuditService() 而非直接调 Server Action
const service = useAuditService()
const res = await service.getAuditRetentionConfig()

页面注入:

// overview/page.tsx
import { AuditServiceProvider } from "@/modules/audit/services/audit-service"
import { adminAuditService } from "@/modules/audit/services/admin-audit-service"

return (
  <AuditServiceProvider service={adminAuditService}>
    <AuditOverviewView stats={stats} trend={trend} distribution={distribution} />
  </AuditServiceProvider>
)

b. P0-2 mock-audit-service 类型标注修复

// 修复前
getAuditLogs: async () =>
  ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult<AuditLog>,

// 修复后 —— 为返回值添加显式类型标注,无需 as
getAuditLogs: async (): Promise<PaginatedResult<AuditLog>> => ({
  items: [],
  total: 0,
  page: 1,
  pageSize: 20,
  totalPages: 0,
}),

c. P0-3 revalidatePath

import { revalidatePath } from "next/cache"

// saveAuditRetentionConfigAction 末尾
revalidatePath("/admin/audit-logs/overview")
return { success: true, data: config }

// purgeAuditLogsAction 末尾
revalidatePath("/admin/audit-logs/overview")
revalidatePath("/admin/audit-logs")
return { success: true, data: result }

d. P1-1 trackEvent event 修正

// 保留策略操作
await trackEvent({
  event: "audit.retention",  // 原: "audit.exported"
  userId: session?.user?.id,
  targetType: "audit_retention",
  properties: { action: "save_config", ... },
})

e. P1-2 ErrorBoundary namespace 修复

audit.json 新增:

"error": {
  "title": "加载失败",
  "description": "数据加载时发生错误,请稍后重试。",
  "retry": "重试",
  "boundaryTitle": "审计数据加载失败",
  "boundaryDescription": "审计数据区块加载时发生错误。"
}

audit-error-boundary.tsx

<SectionErrorBoundary namespace="audit">

f. i18n 翻译键补充

"filter": {
  "startDate": "开始日期",
  "endDate": "结束日期"
},
"detail": {
  "description": "查看日志的完整字段信息。"
}

g. 最终检查

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