22 KiB
22 KiB
审计模块审计报告
审计范围:
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.md2.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 节)
- 修正 actions 清单:删除不存在的
getAuditLogsAction/getLoginLogsAction;补充说明页面直接调 data-access - 补全组件清单:新增
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(新增) - 更新行数:actions.ts 214→拆分后;data-access.ts 290;新增 export.ts、hooks/
- 新增 hooks 清单:
use-log-pagination.ts - 更新已知问题:标记 P0-1~P1-8 已修复
005_architecture_data.json(audit 节点)
- 修正 actions:删除
getAuditLogsAction/getLoginLogsAction;usedBy更新为api/export/route.ts - 补全 components:新增缺失组件
- 新增 hooks 节点:
useLogPagination - 新增 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
}
通用分页 Hook(P1-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通过 mocknext/navigation的useRouter/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)
- 架构影响地图需同步更新(见第五节)