# 审计模块审计报告 v2 > 审计范围:`src/modules/audit/**`、`src/app/(dashboard)/admin/audit-logs/**`、`src/app/api/export/route.ts`(审计导出分支)、`src/app/api/cron/audit-cleanup/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-25 > 审计依据:`docs/architecture/004_architecture_impact_map.md`(2.15 节)、`docs/architecture/005_architecture_data.json`(audit 节点)、`docs/standards/coding-standards.md`、项目 `project_rules.md` > 前置报告:`docs/architecture/audit/audit-module-audit-report.md`(v1,2026-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 Action(1 查询 + 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 直调) └─ (Client Component) ├─ (nuqs URL 状态) └─ (分页 + 空状态 + 详情对话框) overview/page.tsx (RSC) ├─ requirePermission(AUDIT_LOG_READ) ├─ getAuditOverviewStats / getAuditTrend / getDataChangeActionStats └─ (RSC) ├─ ├─ ├─ └─ (Client Component,直调 Server Actions) ``` 导出流:`` → `fetch /api/export` → `exportAuditLogsAction` → `getAuditLogsForExport` → `buildAuditLogExport` → Excel buffer。 定时清理流:`/api/cron/audit-cleanup`(CRON_SECRET 鉴权)→ `getAuditRetentionConfig` → `purgeExpiredAuditLogs`。 ### 1.3 架构图记录情况 - **004(2.15 节)**:记录较完整,含 services/hooks/export/retention 节点,行数标注准确。 - **005(audit 节点)**:data-access/actions/types 记录完整,含 services/hooks/export/retention 节点。 - **已知不一致**:services 层的 `AuditServiceProvider` 在架构图中标注为"已接入",但实际**未被任何页面使用**(详见 P0-1)。 --- ## 二、现存问题与原因分析 ### P0 — 严重违规 #### P0-1 Service 抽象层定义但从未使用(虚假完成) - **位置**:`services/audit-service.tsx`(`AuditServiceProvider`、`useAuditService`、`useAuditAnalytics`)、`services/admin-audit-service.ts`、`services/mock-audit-service.ts` - **违反规则**:任务要求 → "完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context(或组合 Provider)注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access";`project_rules.md` → "架构图优先规则:改码必同步图" - **问题**: 1. 全局搜索 `AuditServiceProvider` 的实际使用:**仅在 `audit-service.tsx` 和 `mock-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)" - **示例**: ```typescript getAuditLogs: async () => ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult, ``` - **原因**:对象字面量缺少 `items: [] as AuditLog[]` 类型标注,导致需要 `as` 断言整个返回值 - **后果**:违反 TS 严格规则;lint 通过但 code review 应拦截 #### P0-3 全部 7 个 Server Action 缺少 revalidatePath - **位置**:`actions.ts` 所有 7 个 Action(`getDataChangeLogsAction`、`exportAuditLogsAction`、`exportLoginLogsAction`、`exportDataChangeLogsAction`、`getAuditRetentionConfigAction`、`saveAuditRetentionConfigAction`、`purgeAuditLogsAction`) - **违反规则**:`project_rules.md` → "Server Action 规范:使用 `revalidatePath` 精确刷新缓存" - **问题**:`saveAuditRetentionConfigAction` 和 `purgeAuditLogsAction` 修改数据后未刷新 overview 页面缓存,用户保存保留策略后看到的仍是旧配置 - **原因**:遗漏 - **后果**:保留策略变更后概览页缓存不刷新,显示过期数据 ### P1 — 重要缺陷 #### P1-1 trackEvent event 名称误用(保留策略操作误标为导出) - **位置**:`actions.ts` 第 170、199 行(`saveAuditRetentionConfigAction`、`purgeAuditLogsAction`)、`app/api/cron/audit-cleanup/route.ts` 第 54 行 - **违反规则**:任务要求 → "监控:方案中预留关键操作埋点接口" - **问题**:保留策略的保存/清理操作使用 `event: "audit.exported"`,语义错误 ```typescript // 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.json` 有 `error.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 行) - **抛出错误**:`getAuditLogs`、`getLoginLogs`、`getDataChangeLogs`、`getDataChangeStats`、`getAuditOverviewStats`、`getAuditTrend`、`getDataChangeActionStats` - **违反规则**:`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 行 - **问题**: ```tsx {t("detail.title")} {t("detail.title")} ``` 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 行使用 `
` 纯文本展示 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 页面接入 `AuditServiceProvider`,`AuditOverviewView` 内组件改用 `useAuditService()` 获取数据;或若评估后认为 RSC + props 模式已满足需求则**删除未使用的 services 层**避免误导 |
| P0-2 | mock-audit-service.ts 9 处 as 断言 | 为对象字面量添加元素类型标注(如 `items: [] as AuditLog[]` → 改用 `items: new Array()` 或显式标注返回类型) |
| 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.boundaryDescription`,`AuditErrorBoundary` 传 `namespace="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.md(2.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.json(audit 节点)

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

---

## 六、重构方案设计

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

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

由于 `AuditOverviewView` 是 RSC(async server component),而 `AuditServiceProvider` 是 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)

```typescript
// 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` 改造:
```tsx
// 在组件内部使用 useAuditService() 而非直接调 Server Action
const service = useAuditService()
const res = await service.getAuditRetentionConfig()
```

页面注入:
```tsx
// overview/page.tsx
import { AuditServiceProvider } from "@/modules/audit/services/audit-service"
import { adminAuditService } from "@/modules/audit/services/admin-audit-service"

return (
  
    
  
)
```

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

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

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

### c. P0-3 revalidatePath

```typescript
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 修正

```typescript
// 保留策略操作
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` 新增:
```json
"error": {
  "title": "加载失败",
  "description": "数据加载时发生错误,请稍后重试。",
  "retry": "重试",
  "boundaryTitle": "审计数据加载失败",
  "boundaryDescription": "审计数据区块加载时发生错误。"
}
```

`audit-error-boundary.tsx`:
```tsx

```

### f. i18n 翻译键补充

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

### g. 最终检查

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