docs(architecture): update impact map, data, audit reports, superpowers docs
Some checks failed
CI / scheduled-backup (push) Has been skipped
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled

- Update 004_architecture_impact_map.md and 005_architecture_data.json

- Add audit reports: data-access-audit-framework-v1, data-access-audit-v1-data.json,

  data-access-audit-v1, g1-g5 audit outputs

- Add superpowers plans and specs (logging-refactor, documentation-system-redesign)

- Update troubleshooting/known-issues.md
This commit is contained in:
SpecialX
2026-07-07 16:23:35 +08:00
parent 7387d70289
commit 5d9981fd7d
15 changed files with 10598 additions and 78 deletions

View File

@@ -0,0 +1,771 @@
# 文档体系重设计
> 日期2026-07-07
> 状态:待用户审阅
> 范围:全局规范文档(`project_rules.md` / `coding-standards.md` / `004` / `005` / `known-issues.md`+ 新增架构元数据库 + 模块工作手册
> 唯一源原则004 为架构设计意图唯一源arch.db 为代码结构唯一源known-issues.md 为经验唯一源
---
## 一、设计目标
### 1.1 核心诉求
1. **审核并修正现有规范文档**的合规性、遗漏、技术错误
2. **重新设计文档体系结构**,消除职责重叠,让每份文档只回答一类问题
3. **建立架构元数据库**arch.db让 AI 快速查询模块/函数/调用链/依赖关系,无需扫描整个代码库
4. **建立 AI 自我演进机制**,让 AI 越工作越了解项目,后来者可接力而非重新梳理
### 1.2 设计原则
| 原则 | 含义 |
|------|------|
| 单一来源 | 每类信息只有一个权威文档,其他文档引用而非重复 |
| 职责分离 | 每份文档只回答一类问题What / Why / How / When |
| 贴近代码 | 模块工作手册放在 `src/modules/[module]/README.md`,与代码同生命周期 |
| 自动优于手动 | 代码结构由扫描器自动生成arch.db减少人工维护 |
| 信任但验证 | AI 使用经验前必须审核,文档变更后重新审核 |
### 1.3 不做事项YAGNI
- 不建 Monorepo项目明确为单应用 + 模块化)
- 不引入 Storybook当前无需求
- 不建独立文档站点GitHub/Gitea 直接渲染 Markdown 足够)
- 不做实时热更新架构库CI + AI 工作前手动触发足够)
---
## 二、文档体系拓扑
### 2.1 整体结构
```
项目根/
├─ .trae/rules/project_rules.md # 全局硬性规则(强制约束)
├─ docs/
│ ├─ standards/coding-standards.md # 编码规范How to write code
│ ├─ architecture/
│ │ ├─ 004_architecture_impact_map.md # 架构设计意图Why - 瘦身后约 500 行)
│ │ ├─ 006_k12_feature_checklist.md # 功能模块清单(保留)
│ │ ├─ 007_gap_audit_report.md # 差距审计(保留)
│ │ ├─ 008_module_role_mapping.md # 模块角色映射(保留)
│ │ ├─ roadmap/ # 【新增】长远规划
│ │ │ ├─ README.md # 路线图索引
│ │ │ ├─ tech-debt.md # 技术债清单(从 004 第三部分迁入)
│ │ │ ├─ decoupling.md # 解耦路线图(从 audit/01 迁入)
│ │ │ └─ pending-features.md # 待开发功能(从 004 "未完成项"迁入)
│ │ └─ audit/archive/ # 【新增】历史审查报告归档(只读)
│ │ ├─ 005_architecture_data.json # 005 废弃后归档于此
│ │ └─ (现有 60+ 份 audit 报告迁入)
│ └─ troubleshooting/
│ └─ known-issues.md # 经验库(精简为索引式,无代码示例)
├─ src/modules/[module]/
│ └─ README.md # 【新增】模块工作手册(每模块一份)
└─ scripts/arch-scan/ # 【新增】架构扫描器
├─ scanner.ts # ts-morph 扫描器
├─ schema.ts # SQLite schema 定义
├─ query.ts # 查询函数
├─ cli.ts # CLI 入口
└─ arch.db # 生成的 SQLitegit 提交)
```
### 2.2 文档职责边界
| 文档 | 类型 | 回答的问题 | 谁维护 | 内容禁区 |
|------|------|-----------|--------|---------|
| `arch.db` | 自动生成 | What代码结构是什么 | 扫描器 | 无人为语义 |
| `004` 架构图 | 人类可读 | Why架构为什么这样设计 | 人/AI | 不含代码结构、不含规划、不含教程 |
| `project_rules.md` | 硬性规则 | Must必须遵守什么 | 人 | 不含详细规范、不含架构描述 |
| `coding-standards.md` | 编码规范 | How to write怎么写代码 | 人 | 不含架构事实、不含经验 |
| `known-issues.md` | 经验库 | Experience场景→技术、工作经验 | AI 思考后更新 | 不含代码示例、不含错误示范 |
| `modules/[m]/README.md` | 模块上下文 | How to work怎么上手模块 | 人/AI | 不重复 arch.db 的代码结构、不含经验 |
| `roadmap/*` | 规划 | When未来做什么 | 人/AI | 不含架构事实、不含经验 |
| `audit/archive/*` | 历史参考 | Past过去发现了什么 | 只读归档 | 不再更新 |
### 2.3 三类信息源的互补关系
```
arch.db (What) ── AI 查询代码结构、调用关系、依赖
↓ 基于代码生成
004 架构图 (Why) ── 人理解架构设计意图
↓ 解释决策
模块手册 (How to work) ── 人/AI 上手模块的工作流程
↓ 记录经验
known-issues (Experience) ── 遇到问题时查技术方向
```
### 2.4 废弃文档
| 文档 | 处理 | 理由 |
|------|------|------|
| `005_architecture_data.json` | 废弃,归档到 `audit/archive/` | arch.db 自动生成比手维护 JSON 准确 |
| `docs/architecture/audit/01_decoupling_roadmap.md` | 迁移到 `roadmap/decoupling.md` | 属于规划而非审查报告 |
| `docs/architecture/audit/` 下 60+ 份审查报告 | 迁移到 `audit/archive/`,只读归档 | 历史参考,不再更新 |
---
## 三、arch.db 架构元数据库
### 3.1 设计目标
让 AI 无需扫描整个代码库即可精准查询:
- 某模块的所有导出函数
- 某函数被谁调用(逆向追踪,递归)
- 某函数调用了什么(正向追踪,递归)
- 某模块依赖哪些模块(递归)
- 哪些地方用了某技术(如 cacheFn
- 架构违规检测超长文件、Server Action 缺权限校验等)
- 完整调用链(从 UI 到 DB
### 3.2 技术选型
| 决策 | 选择 | 理由 |
|------|------|------|
| 存储 | SQLite | 嵌入式、零配置、.db 文件可 git 提交、AI 可直接 sqlite3 查询 |
| 扫描器 | ts-morph | TypeScript AST 操作库,支持类型推导,能提取调用关系 |
| 扫描粒度 | 全调用图 | 模块 + 函数 + 类型 + 函数间调用关系,覆盖 UE 引用查看器所有用法 |
| 触发时机 | CI 自动 + 本地手动 + AI 工作前强制 | 三重保障架构库与代码同步 |
| 数据源 | 自动扫描代码 | 零人工维护,与代码同步 |
### 3.3 SQLite Schema
```sql
-- ============ 核心实体 ============
-- 1. 模块src/modules/* 下的目录)
CREATE TABLE modules (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
path TEXT NOT NULL,
description TEXT,
layer TEXT NOT NULL, -- "modules" | "shared" | "app" | "root"
created_at TEXT NOT NULL
);
-- 2. 文件
CREATE TABLE files (
id INTEGER PRIMARY KEY,
module_id INTEGER REFERENCES modules(id),
path TEXT NOT NULL UNIQUE,
kind TEXT NOT NULL, -- "actions" | "data-access" | "schema" | "types" | "component" | "hook" | "lib" | "config" | "route" | "page" | "layout"
lines INTEGER,
is_server INTEGER DEFAULT 0,
is_client INTEGER DEFAULT 0,
has_server_only INTEGER DEFAULT 0
);
-- 3. 导出符号(函数、类、类型、常量、组件)
CREATE TABLE symbols (
id INTEGER PRIMARY KEY,
file_id INTEGER NOT NULL REFERENCES files(id),
name TEXT NOT NULL,
kind TEXT NOT NULL, -- "function" | "class" | "type" | "interface" | "const" | "component"
is_exported INTEGER DEFAULT 0,
is_async INTEGER DEFAULT 0,
is_server_action INTEGER DEFAULT 0,
signature TEXT,
start_line INTEGER,
end_line INTEGER,
UNIQUE(file_id, name, start_line)
);
-- ============ 关系 ============
-- 4. 调用关系(符号间调用)
CREATE TABLE calls (
id INTEGER PRIMARY KEY,
caller_id INTEGER NOT NULL REFERENCES symbols(id),
callee_id INTEGER REFERENCES symbols(id),
callee_external TEXT, -- 项目外调用(如 "fetch", "console.log"
call_line INTEGER,
count INTEGER DEFAULT 1
);
-- 5. 文件级导入
CREATE TABLE file_imports (
id INTEGER PRIMARY KEY,
source_file_id INTEGER NOT NULL REFERENCES files(id),
imported_file_id INTEGER REFERENCES files(id),
import_path TEXT NOT NULL,
is_type_only INTEGER DEFAULT 0,
imported_names TEXT
);
-- 6. 模块间依赖(聚合视图)
CREATE TABLE module_deps (
id INTEGER PRIMARY KEY,
source_module_id INTEGER NOT NULL REFERENCES modules(id),
target_module_id INTEGER NOT NULL REFERENCES modules(id),
dep_type TEXT NOT NULL, -- "import" | "data-access-call" | "action-call"
UNIQUE(source_module_id, target_module_id, dep_type)
);
-- ============ 业务元数据 ============
-- 7. 技术标签
CREATE TABLE tech_tags (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
category TEXT
);
-- 8. 符号-技术标签关联
CREATE TABLE symbol_tech_tags (
symbol_id INTEGER NOT NULL REFERENCES symbols(id),
tag_id INTEGER NOT NULL REFERENCES tech_tags(id),
PRIMARY KEY(symbol_id, tag_id)
);
-- 9. 权限点
CREATE TABLE permissions (
id INTEGER PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
description TEXT
);
-- 10. 路由
CREATE TABLE routes (
id INTEGER PRIMARY KEY,
path TEXT NOT NULL UNIQUE,
kind TEXT NOT NULL, -- "page" | "api" | "layout"
file_id INTEGER REFERENCES files(id),
min_permission TEXT
);
-- 11. 数据库表
CREATE TABLE db_tables (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
module_id INTEGER REFERENCES modules(id),
description TEXT
);
-- ============ 扫描元数据 ============
-- 12. 扫描元数据
CREATE TABLE scan_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
```
### 3.4 关键索引
```sql
CREATE INDEX idx_symbols_file ON symbols(file_id);
CREATE INDEX idx_symbols_name ON symbols(name);
CREATE INDEX idx_calls_caller ON calls(caller_id);
CREATE INDEX idx_calls_callee ON calls(callee_id);
CREATE INDEX idx_file_imports_source ON file_imports(source_file_id);
CREATE INDEX idx_file_imports_target ON file_imports(imported_file_id);
CREATE INDEX idx_symbol_tech_tags_tag ON symbol_tech_tags(tag_id);
```
### 3.5 技术标签自动识别规则
扫描器通过启发式规则自动打标签:
| 标签 | 识别规则 | 类别 |
|------|---------|------|
| `cacheFn` | 文件内出现 `cacheFn(` 调用 | cache |
| `zustand` | import 自 `zustand` | state |
| `useOptimistic` | 文件内出现 `useOptimistic` 调用 | state |
| `react-hook-form` | import 自 `react-hook-form` | form |
| `TanStack Query` | import 自 `@tanstack/react-query` | state |
| `Server Action` | 文件顶部 `"use server"` 或函数级 `"use server"` | server |
| `Tiptap` | import 自 `@tiptap/*` | ui |
| `Drizzle` | import 自 `drizzle-orm` | db |
| `nuqs` | import 自 `nuqs` | state |
| `recharts` | import 自 `recharts` | ui |
### 3.6 递归查询能力6 类)
#### 查询 1逆向递归谁调用了 X递归到入口
```sql
WITH RECURSIVE upstream(caller_id, caller_name, caller_path, depth, path_chain) AS (
SELECT c.caller_id, s.name, f.path, 0, s.name
FROM calls c
JOIN symbols s ON c.caller_id = s.id
JOIN files f ON s.file_id = f.id
WHERE c.callee_id = (SELECT id FROM symbols WHERE name = ? LIMIT 1)
UNION
SELECT c.caller_id, s.name, f.path, u.depth + 1, u.path_chain || '' || s.name
FROM upstream u
JOIN calls c ON c.callee_id = u.caller_id
JOIN symbols s ON c.caller_id = s.id
JOIN files f ON s.file_id = f.id
WHERE u.depth < 10
AND u.caller_id NOT IN (SELECT caller_id FROM upstream)
)
SELECT caller_name, caller_path, depth, path_chain FROM upstream
ORDER BY depth, caller_path;
```
#### 查询 2正向递归X 调用了什么?递归到叶子)
```sql
WITH RECURSIVE downstream(callee_id, callee_name, callee_path, depth, path_chain) AS (
SELECT c.callee_id, s.name, f.path, 0, ?
FROM calls c
JOIN symbols s ON c.callee_id = s.id
JOIN files f ON s.file_id = f.id
WHERE c.caller_id = (SELECT id FROM symbols WHERE name = ? LIMIT 1)
UNION
SELECT c.callee_id, s.name, f.path, d.depth + 1, d.path_chain || '' || s.name
FROM downstream d
JOIN calls c ON c.caller_id = d.callee_id
JOIN symbols s ON c.callee_id = s.id
JOIN files f ON s.file_id = f.id
WHERE d.depth < 10
AND d.callee_id NOT IN (SELECT callee_id FROM downstream)
)
SELECT callee_name, callee_path, depth, path_chain FROM downstream
ORDER BY depth, callee_path;
```
#### 查询 3模块级逆向谁依赖了模块 X递归到根
```sql
WITH RECURSIVE mod_upstream(source_module, target_module, depth, path_chain) AS (
SELECT m.name, m2.name, 0, m.name || '' || m2.name
FROM module_deps md
JOIN modules m ON md.target_module_id = m.id
JOIN modules m2 ON md.source_module_id = m2.id
WHERE m.name = ?
UNION
SELECT mu.source_module, m.name, mu.depth + 1, mu.path_chain || '' || m.name
FROM mod_upstream mu
JOIN module_deps md ON md.target_module_id = (
SELECT id FROM modules WHERE name = mu.source_module
)
JOIN modules m ON md.source_module_id = m.id
WHERE mu.depth < 10
)
SELECT DISTINCT source_module, target_module, depth, path_chain
FROM mod_upstream ORDER BY depth;
```
#### 查询 4模块级正向模块 X 依赖了哪些模块?递归到叶子)
类似查询 3方向反转从 source_module_id 出发递归 target_module_id。实施时补全完整 SQL。
#### 查询 5双向依赖检测模块 A 和 B 之间是否有循环依赖)
同时执行查询 3A→B和查询 4B→A若两方向都存在路径则存在循环依赖。实施时封装为单一查询函数。
#### 查询 6完整调用链UI → Server Action → data-access → DB
```sql
WITH RECURSIVE trace(symbol_id, symbol_name, file_path, kind, depth, path_chain) AS (
-- 入口UI 事件处理函数handle*, onSubmit*, onClick*
SELECT s.id, s.name, f.path, f.kind, 0, s.name
FROM symbols s JOIN files f ON s.file_id = f.id
WHERE s.name LIKE 'handle%' OR s.name LIKE 'onSubmit%' OR s.name LIKE 'onClick%'
UNION
SELECT cs.id, cs.name, cf.path, cf.kind, t.depth + 1, t.path_chain || '' || cs.name
FROM trace t
JOIN calls c ON c.caller_id = t.symbol_id
JOIN symbols cs ON c.callee_id = cs.id
JOIN files cf ON cs.file_id = cf.id
WHERE t.depth < 15
AND t.symbol_id NOT IN (SELECT symbol_id FROM trace)
)
SELECT * FROM trace
WHERE kind IN ('actions', 'data-access')
ORDER BY depth, path_chain;
```
### 3.7 CLI 接口
```bash
# 1. 更新架构库
npm run arch:scan
# 2. 查询命令
npm run arch:query ref <symbolName> [--forward] [--depth=10] # 符号引用(默认逆向)
npm run arch:query module <moduleName> [--reverse] [--depth=10] # 模块依赖
npm run arch:query tech <techTag> # 技术使用
npm run arch:query path <moduleA> <moduleB> # 模块间路径
npm run arch:query violations # 架构违规
npm run arch:query trace <entrySymbol> # 完整调用链
npm run arch:query sql "<SQL>" # 自由 SQL
npm run arch:query repl # 交互式
```
### 3.8 输出格式
默认输出**树形可视化**(模仿 UE 引用查看器),可选 `--json` 输出机器可读格式:
```
$ npm run arch:query ref createExam
▼ createExam (src/modules/exams/data-access.ts#L42)
├─▼ createExamAction (src/modules/exams/actions.ts#L18) [Server Action]
│ │
│ └─▼ handleCreateExam (src/modules/exams/components/exam-form.tsx#L67) [Client]
│ │
│ └── <Form onSubmit> (src/modules/exams/components/exam-form.tsx#L120)
└─▼ importExams (src/modules/exams/import-export.ts#L234)
```
---
## 四、AI 自我演进机制
### 4.1 模块 README.md 标准结构
```markdown
# [模块名] 模块工作手册
> 经验查 known-issues.md代码结构查 arch.db本文件只记工作流程。
## 模块职责
一句话描述本模块做什么。
## 核心工作流程
1. 新增考试: ...
2. 修改成绩计算: ...
## 关键约束
- [不可违反的约束]
- [依赖关系,从 arch.db 提取]
## 架构决策(为什么这样设计)
- **为什么用 X 而不用 Y**: [决策理由]
```
### 4.2 known-issues.md 结构(唯一经验库)
```markdown
# 项目经验库
> AI 工作前必读使用经验前必审核。AI 发现更好办法时更新本文件。
> 最后审核: 2026-07-07 14:00 (commit: abc1234)
## 全局经验
### 缓存策略
| 场景 | 技术方向 | 模块 | 备注 |
|------|---------|------|------|
| 服务端数据缓存 | cacheFn + Redis | 全局 | 详见 arch.db tech_tags |
| 客户端数据缓存 | TanStack Query | 全局 | 禁止 useEffect+fetch |
### 状态管理
| 场景 | 技术方向 | 模块 | 备注 |
|------|---------|------|------|
| URL 状态 | nuqs | 全局 | 5 层状态模型 L1 |
| 表单状态 | react-hook-form + zodResolver | 全局 | L5 层 |
## 模块经验: exams
### 考试创建流程
| 场景 | 技术方向 | 备注 |
|------|---------|------|
| 考试数据缓存 | cacheFn 包裹 createExamRaw | 修改后须失效 importExams 缓存 |
| AI 题目解析 | 动态 import + webpackIgnore | 可选依赖 ollama |
## 模块经验: grades
...
## 工作经验日志(按时间倒序,定期提炼到上述分区)
### 2026-07-07 重构 createExam 调用链
- **模块**: exams
- **做了什么**: 拆分 createExam 为 createExamRaw + createExam(含缓存)
- **学到什么**: data-access 层已有 cacheFnactions 层无需再缓存
- **下次注意**: 修改 createExam 必须同步更新 importExams 的缓存失效
- **审核状态**: 待审核
```
### 4.3 known-issues.md 精简规则
| 内容类型 | 处理 |
|---------|------|
| 代码示例(多行代码块) | 删除,改为"技术方向"描述 |
| 错误示范 | 删除,只保留"正确做法" |
| 重复的架构规则 | 删除,引用 004/project_rules |
| "场景 → 技术"映射 | 保留,按模块分区 |
| 工作经验日志 | 保留(追加区) |
### 4.4 AI 工作强制流程(写入 project_rules.md
```
AI 进入项目工作流程(强制,违反即违规):
阶段 1: 上下文加载
1.1 npm run arch:scan # 更新 arch.db
1.2 npm run arch:query module <目标模块> # 查模块依赖
1.3 npm run arch:query ref <目标函数> --forward # 查调用链
1.4 阅读 src/modules/[模块]/README.md # 读模块工作流程
1.5 查 known-issues.md "模块经验: <模块>" 分区 # 读相关经验
1.5.1 审核相关经验(检查代码是否仍匹配)
1.5.2 若文档自上次审核后已变更 → 重新审核并标记
1.5.3 审核通过 → 使用;失败 → 标记失效,不使用
阶段 2: 执行工作
2.1 按规划执行
2.2 修改代码后立即运行 arch:scan
阶段 3: 经验沉淀(强制,不可跳过)
3.1 在 known-issues.md "工作经验日志" 区追加一条记录:
- 做了什么
- 学到什么
- 下次注意事项
- 审核状态: 待审核
3.2 若发现新的"场景→技术"映射 → 提炼到对应模块分区
3.3 若发现新的架构决策 → 更新 004
3.4 若代码结构变化 → arch:scan 确认 arch.db 已更新
阶段 4: 提交后审核(人工)
4.1 人工审查"待审核"日志条目
4.2 通过 → 标记"已审核 (commit, 审核人)"
4.3 失败 → 标记"审核失败,原因:..."
4.4 定期(如每两周)将成熟日志提炼到分区表格
```
### 4.5 信任但验证机制
```
AI 读取 known-issues.md 经验
检查该条经验的"审核状态":
├─ 已审核 → 检查代码是否仍匹配
│ ├─ 匹配 → 使用经验
│ └─ 不匹配 → 标记"待重新审核",不使用
├─ 待审核 → 标记"AI 使用前审核",验证后使用
└─ 审核失败 → 不使用,记录原因
使用经验工作时,若发现经验有误 → 标记"审核失败,原因:..."
```
### 4.6 防 known-issues.md 膨胀
- **工作经验日志区上限 50 条**——超过则人工提炼最早的到分区表格,删除原日志
- **分区表格无上限**——但每条保持单行索引式
- **精简目标**:从 1317 行降至约 300 行
---
## 五、004 瘦身方案
### 5.1 现状问题
004 当前 4227 行,远超架构文档应有体量。主要问题:
| 问题 | 表现 | 行数估算 |
|------|------|---------|
| 混入工作日志 | "1.1.1 M7 移动端 PWA 支持2026-07-01 新增)"等 7 个变更日志章节 | ~400 行 |
| 混入规划/待办 | "未完成项(待后续专项)"、各模块的 P0/P1/P2 修复标记 | ~600 行 |
| 混入实现细节 | 函数签名索引、文件行数表格、组件清单 | ~1500 行 |
| 模块清单冗长 | 27 个模块每个都用大段文字描述,含"V4 P2-4 已修复"等历史 | ~1500 行 |
| 真正的架构内容 | 分层图、依赖关系图、数据流向图、核心原则 | ~227 行 |
### 5.2 瘦身后目标结构(约 500 行)
```markdown
# Next_Edu 架构影响地图
> 唯一源:项目架构事实。代码结构查 arch.db经验查 known-issues.md规划查 roadmap/。
## 1. 分层架构
- 三层架构图app → modules → shared
- 分层规则4 条核心约束)
- 根模块说明auth.ts, proxy.ts
## 2. 模块清单
(表格形式,每模块一行,详情查 arch.db 和模块 README
| 模块 | 职责 | 核心依赖 | 被依赖 | README |
|------|------|---------|--------|--------|
| exams | 考试管理 | grades, classes, questions | dashboard | [README](../../src/modules/exams/README.md) |
| ... | ... | ... | ... | ... |
## 3. 模块依赖关系图
- 核心业务模块依赖图
- 扩展模块依赖图
- (循环依赖检测见 arch.db 查询)
## 4. 数据流向(核心场景)
- 考试流程数据流
- 学生提交作业数据流
- 仪表盘聚合数据流
## 5. 核心架构原则
- 三层架构单向依赖
- 模块间通过 data-access 通信
- Server Action 必须权限校验
- 设计令牌分层Primitive → Semantic → Tailwind
## 6. 设计令牌体系
- 文件分布src/app/styles/tokens/
- 令牌分层规则
- 强制约束(禁止硬编码颜色/字体/字号)
## 相关文档
- [arch.db 查询](../../scripts/arch-scan/) - 代码结构
- [known-issues.md](../troubleshooting/known-issues.md) - 经验库
- [roadmap/](./roadmap/) - 规划
- [模块 README](../../src/modules/) - 模块工作流程
```
### 5.3 迁移映射
| 004 现有内容 | 去向 | 理由 |
|------------|------|------|
| 1.1.1-1.1.7 变更日志章节 | 删除git 历史已记录) | 工作日志不属于架构事实 |
| "Phase X.X 新增"标记 | 删除 | 同上 |
| "P0-X 已修复"标记 | 删除(保留事实,删除修复历史) | 修复历史属于 git log |
| "未完成项(待后续专项)" | 迁移到 `roadmap/tech-debt.md` | 属于规划 |
| 各模块的"V1/V2/V3/V4"版本描述 | 删除,只保留当前状态 | 版本演进属于 git log |
| 函数签名索引(附录 C | 删除(查 arch.db | 代码结构属于 arch.db |
| 模块间依赖矩阵(附录 A | 删除(查 arch.db `module_deps` | 同上 |
| 关键参数影响链(附录 B | 保留(架构决策) | 属于架构意图 |
| 第三部分"已知架构问题和技术债" | 迁移到 `roadmap/tech-debt.md` | 属于规划 |
| 模块清单(第二部分) | 大幅精简为表格 | 详情查 arch.db + README |
---
## 六、规范文档修正要点
### 6.1 已发现的跨文档冲突
| 冲突项 | project_rules.md | coding-standards.md | 004 | 修正方向 |
|--------|-----------------|---------------------|-----|---------|
| 设计令牌位置 | `src/app/styles/tokens/` | `globals.css` | `src/app/styles/tokens/` | 统一为 `src/app/styles/tokens/`(以 004 为准) |
| 架构文档清单 | 仅列 004-008 + audit/01 | 无 | 自身 | project_rules.md 补全 001/002/003/008 |
| 002 编号冲突 | 无 | 无 | 无 | `002_rbac_refactoring.md``002_role_based_routing.md` 编号冲突,需重命名 |
| 缓存策略描述 | 无 | "用 unstable_cache" | 已迁移到 cacheFn | coding-standards.md 更新为 cacheFn |
### 6.2 project_rules.md 修正
1. **架构文档清单补全**:加入 001/002/003/008
2. **新增 AI 工作流程规则**:写入第四节"AI 工作强制流程"
3. **新增 arch.db 规则**AI 工作前必须 `npm run arch:scan`
4. **令牌位置统一**:与 004 一致,明确为 `src/app/styles/tokens/`
### 6.3 coding-standards.md 修正
1. **令牌位置统一**:从 `globals.css` 改为 `src/app/styles/tokens/`
2. **缓存策略更新**:从 `unstable_cache` 改为 `cacheFn`
3. **删除过时内容**tsconfig "当前差异"部分(已升级则删除,未升级则列入 roadmap
4. **状态管理章节更新**:加入 5 层状态模型L1 URL / L2 Server / L3 Client Business / L4 Global UI / L5 Form
5. **ESLint 配置章节更新**:反映已实现的 `no-restricted-syntax``design-tokens/no-hardcoded-fonts` 等规则
---
## 七、实施阶段划分
### 7.1 三阶段流水线
| 阶段 | 目标 | 产出 | 验收标准 |
|------|------|------|---------|
| 阶段 1: 同步实际 | 把 4 份文档对齐到代码现状 | 实际状态基线报告 | 4 份文档与代码零冲突 |
| 阶段 2: 审核正确性 | 基于阶段 1 基线,按 4 维度审核文档 | 问题清单 + 修正建议 | 文档内部一致、无技术错误、无遗漏、对齐大仓最佳实践 |
| 阶段 3: 反向修正代码 | 基于阶段 2 审核后文档,修正代码偏差 | 代码修正 PR | 代码与文档零冲突 |
### 7.2 阶段 1 任务分解
1. 扫描代码实际状态令牌位置、模块结构、tsconfig、Husky/lint-staged 是否配置等)
2. 对照 4 份文档找出不一致项
3. 修正文档使其与代码一致
4. 产出"实际状态基线报告"
### 7.3 阶段 2 任务分解
按 4 维度审核:
1. **合规性**:文档内部一致性、跨文档一致性
2. **遗漏**:对照大仓最佳实践找缺失规则(包边界、依赖方向、共享工具下沉)
3. **技术错误**tsconfig 目标版本、令牌位置、ESLint 规则等具体错误
4. **大仓规范对照**:提取适用于单应用模块化的部分
### 7.4 阶段 3 任务分解
1. 识别代码与审核后文档的偏差
2. 修正代码(如 tsconfig 升级、令牌位置迁移等)
3. 验证 `npm run lint``npx tsc --noEmit` 零错误
### 7.5 独立项目arch.db 扫描器
arch.db 扫描器作为独立项目,可与三阶段并行推进:
1. 实现 ts-morph 扫描器scanner.ts
2. 实现 SQLite schemaschema.ts
3. 实现查询函数query.ts
4. 实现 CLIcli.ts
5. 添加 `npm run arch:scan``npm run arch:query` 脚本
6. 配置 CI 自动运行
7. 写入 project_rules.md 作为 AI 工作前置规则
### 7.6 独立项目:模块 README 创建
约 27 个模块各创建一份 README.md实施时以 arch.db 扫描结果为准),可分批推进:
1. 标杆模块先做textbooks、grades 已在 004 标记为"标杆模块"
2. 核心业务模块exams、homework、questions
3. 教学管理模块classes、school、scheduling、attendance
4. 用户沟通模块users、messaging、notifications、parent
5. 扩展功能模块elective、proctoring、diagnostic、dashboard
6. 其他模块announcements、files、settings、auth、layout、student、lesson-preparation、standards、course-plans、audit、rbac、onboarding
---
## 八、验收标准
### 8.1 文档体系验收
- [ ] 4 份全局规范文档project_rules.md / coding-standards.md / 004 / known-issues.md内部无矛盾
- [ ] 4 份文档与代码零冲突
- [ ] 004 瘦身至约 500 行,不含规划/工作日志/代码结构
- [ ] 005 归档到 audit/archive/
- [ ] known-issues.md 精简至约 300 行,无代码示例
- [ ] roadmap/ 目录建立,包含 tech-debt.md / decoupling.md / pending-features.md
- [ ] audit/archive/ 目录建立60+ 份审查报告归档
### 8.2 arch.db 验收
- [ ] `npm run arch:scan` 能成功扫描全项目并生成 arch.db
- [ ] `npm run arch:query ref <symbol>` 能递归查询符号引用
- [ ] `npm run arch:query module <module>` 能递归查询模块依赖
- [ ] `npm run arch:query tech <tag>` 能查询技术使用
- [ ] `npm run arch:query violations` 能检测架构违规
- [ ] arch.db 与代码零偏差(扫描器在干净代码上运行无错误)
### 8.3 模块 README 验收
- [ ] 27 个模块各有 README.md
- [ ] 每个 README 含:模块职责、核心工作流程、关键约束、架构决策
- [ ] 每个 README 不含经验(查 known-issues.md、不含代码结构查 arch.db
### 8.4 AI 工作流程验收
- [ ] project_rules.md 写入 AI 工作强制流程
- [ ] AI 工作前运行 `npm run arch:scan` 成为硬性规则
- [ ] known-issues.md 含审核状态字段
- [ ] 模块 README 含审核标记
---
## 九、风险与缓解
| 风险 | 影响 | 缓解 |
|------|------|------|
| ts-morph 扫描大型项目慢 | CI 时间增加 | 扫描器增量扫描(仅变更文件),全量扫描仅 CI 触发 |
| arch.db 二进制文件 git diff 不友好 | Code review 难 | 配合导出 SQL 文本文件diff 看 SQL应用看 .db |
| 模块 README 维护成本 | AI/人遗忘更新 | project_rules.md 强制 AI 工作后更新CI 检查 README 格式 |
| known-issues.md 日志区膨胀 | 文件过大 | 50 条上限,定期提炼到分区表格 |
| 60+ 份 audit 报告归档后信息丢失 | 历史经验丢失 | 归档前提取有价值内容到模块 README 和 known-issues.md |
| AI 不遵守工作流程 | 文档体系失效 | project_rules.md 写为硬性规则CI 检查 arch.db 是否更新 |
---
## 十、未决事项
本 spec 已涵盖所有用户确认的决策。以下事项在实施阶段可能需要进一步决策:
1. **ts-morph 扫描器性能**:若全量扫描超过 30 秒,需考虑增量扫描策略
2. **arch.db 大小**:若超过 10MB需考虑是否排除部分表如 calls 表可能很大)
3. **模块 README 模板**:实施时可能需要根据实际模块调整模板
4. **技术标签体系扩展**:初始 10 个标签可能不够,实施时根据需要扩展

View File

@@ -0,0 +1,721 @@
# 日志系统重构设计文档
| 字段 | 值 |
|------|---|
| 文档版本 | v1 |
| 创建日期 | 2026-07-07 |
| 作者 | Trae 协作生成 |
| 状态 | 待用户审查 |
| 范围 | pino logger 抽象 + Request ID 贯穿 + 静默失败修复 + error.tsx 上报 + track-event 去重 |
| 策略 | 应用层 SDK 改造不部署外部可观测性服务OTel / Sentry / Prometheus 等) |
---
## 1. 背景与现状
### 1.1 现状调研摘要
基于 2026-07-07 全项目调研:
| 维度 | 状态 | 说明 |
|------|------|------|
| 业务审计日志 | ✅ 已成熟 | audit / login / change logger + 3 张 DB 表 + audit 模块管理后台 |
| 应用层结构化日志 | ❌ 未实现 | 88 处散乱 `console.*`,无级别、无 JSON、无 request ID |
| Request Correlation | ❌ 未实现 | proxy.ts 中间件零日志、不注入请求 ID |
| 错误边界上报 | ❌ 全部丢弃 | 130 个 `error.tsx` 接收 `error` prop 后直接丢弃,前端运行时错误对开发者不可见 |
| 静默失败 | ⚠️ 严重 | 三个 audit-logger 的 catch 块完全吞没错误,运维无法感知审计写入失败 |
| 统一错误处理 | ⚠️ 部分 | `handleActionError` / `handleApiError``console.error`,前缀混乱(`[ExamAction]` / `[ActionError]` / `[ApiError]` / 无前缀混用) |
| track-event | ⚠️ 重复 | 4 个 no-op stubshared / rbac / course-plans / questions 各一份) |
| 架构图覆盖 | ❌ 未覆盖 | 004 / 005 完全未记录可观测性基础设施 |
### 1.2 console.* 分布
| 类型 | 次数 | 文件数 | 主要位置 |
|------|------|--------|----------|
| `console.log` | 2 | 1 | `questions/utils/parse-content.ts` |
| `console.error` | 67 | 30 | data-access 的 catch 块、`action-utils.ts``api-response.ts`、各 Server Action |
| `console.warn` | 5 | 5 | `api/web-vitals/route.ts`、redis/store 等 |
| `console.info` | 10 | 6 | `track-event.ts``api/web-vitals/route.ts`、notifications channels |
| `console.debug` | 4 | 3 | 各模块 `track-event.ts`(仅 dev |
### 1.3 关键约束
- **proxy.ts 在 Edge Runtime 运行**[proxy.ts:64](file:///e:/Desktop/CICD/src/proxy.ts#L64) 注释明确说明),`AsyncLocalStorage` 不可用(依赖 `node:async_hooks`Edge 不支持)
- 其余部分RSC、Server Action、Route Handler、data-access运行在 Node.js runtime`AsyncLocalStorage` 完全支持
- 项目自部署(`next.config.ts``output: "standalone"`),运维通过 `docker logs` 查看日志
---
## 2. 目标与非目标
### 2.1 目标
1. **统一 logger 抽象**pino 替换 88 处散乱 `console.*`,提供 `info/error/warn/debug` + `createModuleLogger(module)` 子 logger
2. **级别控制**`LOG_LEVEL` 环境变量控制输出阈值debug/info/warn/errorZod 校验
3. **结构化输出**:生产环境 JSON含 timestamp/level/module/requestId/msg开发环境 `pino-pretty` 彩色文本
4. **Request ID 贯穿**proxy.ts 生成 ID 并注入请求头Node.js runtime 通过 `AsyncLocalStorage` 贯穿到 data-access / audit-logger
5. **静默失败告警**:三个 audit-logger 的 catch 块从 silent 改为 `logger.warn`
6. **统一错误处理接入**`handleActionError` / `handleApiError` / `safeActionCall` 接入 logger前缀通过 `module` 字段规范化
7. **track-event 去重合并**4 个 no-op stub 合并为 1 个,统一通过 `createModuleLogger("track")`
8. **error.tsx 错误上报**130 个 error.tsx 通过 `useErrorReport` Hook 上报到 `/api/client-error`,含节流防风暴
9. **架构图同步**004 / 005 / known-issues.md 全量更新
### 2.2 非目标YAGNI
- OpenTelemetry / 链路追踪trace
- Prometheus / `/metrics` 端点
- Sentry / Bugsnag / Datadog 等 SaaS 错误监控
- Web Vitals 后端持久化(保留现有 `console.warn`
- 数据库查询日志 / 慢查询日志
- 日志文件轮转(`docker logs` 已足够)
- next-auth events 回调改造(与本次重构解耦)
---
## 3. 架构设计
### 3.1 整体数据流
```
[Client] ──HTTP──> [proxy.ts (Edge Runtime)]
│ requestId = crypto.randomUUID() // Web Crypto API
│ NextResponse.next({ request: { headers } })
│ 注入 x-request-id 到下游请求头
[RSC / Server Action / Route Handler (Node.js Runtime)]
│ withRequestContext(fn):
│ 1. headers().get("x-request-id") 读取
│ 2. requestContextStorage.run({ requestId }, fn)
[data-access / audit-logger / 业务逻辑 (Node.js Runtime)]
│ logger.info({...}, "msg") 调用
│ pino mixin 自动从 getRequestContext() 取 requestId
[stdout: {"level":"info","time":...,"requestId":"abc-123","module":"audit","msg":"..."}]
```
### 3.2 新增文件清单
```
src/shared/lib/
├─ logger.ts # pino 实例 + createModuleLogger 工厂
├─ request-context.ts # AsyncLocalStorage仅 Node.js runtime
└─ with-request-context.ts # Server Action / Route Handler 入口包装
src/shared/hooks/
└─ use-error-report.ts # error.tsx 公共上报 Hook含节流
src/app/api/client-error/
└─ route.ts # 客户端错误接收端点
```
### 3.3 修改文件清单
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `package.json` | 新增依赖 | `pino``pino-pretty`dev |
| `src/env.mjs` | 新增字段 | `LOG_LEVEL`(默认 `info`Zod enum |
| `src/proxy.ts` | 修改 | 生成 requestId 并通过 `NextResponse.next` 注入请求头 |
| `src/shared/lib/action-utils.ts` | 修改 | `handleActionError` / `safeActionCall` 用 logger |
| `src/shared/lib/api-response.ts` | 修改 | `handleApiError` 用 logger |
| `src/shared/lib/audit-logger.ts` | 修改 | catch 块从 silent 改为 `logger.warn` |
| `src/shared/lib/change-logger.ts` | 修改 | 同上 |
| `src/shared/lib/login-logger.ts` | 修改 | 同上 |
| `src/shared/lib/track-event.ts` | 修改 | 改为 `createModuleLogger("track")`,删除 no-op 输出 |
| `src/modules/rbac/lib/track.ts` | 删除 | 引用方改为从 `@/shared/lib/track-event` 导入 |
| `src/modules/course-plans/lib/track-event.ts` | 删除 | 同上 |
| `src/modules/questions/utils/track-event.ts` | 删除 | 同上 |
| 88 处 `console.*` 调用点 | 修改 | 替换为 `logger.*``createModuleLogger(module)` |
| 130 个 `error.tsx` | 修改 | 在 useEffect 中调用 `useErrorReport(error)` |
| `next.config.ts` | 修改 | 在现有 `serverExternalPackages` 数组中添加 `"pino"`(当前已有 `mysql2`/`tencentcloud-sdk-nodejs`/`exceljs` |
| `.eslintrc` / `eslint.config.mjs` | 修改 | 新增 `no-console` 规则,仅允许 `logger.ts` 中使用 console |
---
## 4. 核心模块设计
### 4.1 `src/shared/lib/logger.ts`
```ts
import pino, { type Logger } from "pino"
import { env } from "@/env.mjs"
import { getRequestContext } from "./request-context"
/**
* 全局 logger 实例。
*
* - 生产环境JSON 输出到 stdoutdocker logs 友好)
* - 开发环境pino-pretty 彩色文本
* - 自动从 AsyncLocalStorage 混入 requestId / userId若存在
*/
export const logger = pino({
level: env.LOG_LEVEL,
base: { service: "cicd-app" },
formatters: {
level: (label) => ({ level: label }),
},
mixin: () => getRequestContext(),
...(env.NODE_ENV === "development" && {
transport: {
target: "pino-pretty",
options: { colorize: true, translateTime: "SYS:standard" },
},
}),
})
/**
* 创建模块级子 logger自动绑定 module 字段。
*
* @example
* ```ts
* const log = createModuleLogger("audit")
* log.info({ userId }, "User action logged")
* // 输出: {"level":"info","module":"audit","msg":"User action logged", ...}
* ```
*/
export function createModuleLogger(module: string): Logger {
return logger.child({ module })
}
export type { Logger }
```
### 4.2 `src/shared/lib/request-context.ts`
```ts
import { AsyncLocalStorage } from "node:async_hooks"
/**
* 请求上下文,贯穿整个请求生命周期。
*
* 仅在 Node.js Runtime 中可用proxy.ts 是 Edge Runtime不导入此模块
* 通过 withRequestContext 高阶函数注入。
*/
export interface RequestContext {
requestId: string
userId?: string
module?: string
}
export const requestContextStorage = new AsyncLocalStorage<RequestContext>()
/**
* 获取当前请求上下文(若存在)。
*
* - 在 withRequestContext 包装的调用栈内:返回完整上下文
* - 在调用栈外(如顶层模块初始化、定时任务):返回空对象
*
* pino logger 的 mixin 配置会自动调用此函数混入 requestId。
*/
export function getRequestContext(): Partial<RequestContext> {
return requestContextStorage.getStore() ?? {}
}
```
### 4.3 `src/shared/lib/with-request-context.ts`
```ts
import { headers } from "next/headers"
import { randomUUID } from "node:crypto"
import { requestContextStorage, type RequestContext } from "./request-context"
/**
* 包装 Server Action / Route Handler注入 requestId 到 AsyncLocalStorage。
*
* 工作流程:
* 1. 通过 `headers()` 读取 proxy.ts 注入的 `x-request-id`
* 2. 若请求头无此字段(如直接调用的内部函数),生成新 UUID
* 3. 通过 `requestContextStorage.run()` 注入到 AsyncLocalStorage
* 4. 在调用栈内的所有 logger 调用自动获得 requestId
*
* @example
* ```ts
* export const createUserAction = withRequestContext(
* async (state: ActionState<User>, input: CreateUserInput) => {
* // 此处 logger.info 会自动带 requestId
* return handleAction(...)
* }
* )
* ```
*/
export function withRequestContext<TArgs extends unknown[], TResult>(
fn: (...args: TArgs) => Promise<TResult>
): (...args: TArgs) => Promise<TResult> {
return async (...args: TArgs) => {
const headersList = await headers()
const requestId =
headersList.get("x-request-id") ?? randomUUID()
const ctx: RequestContext = { requestId }
return requestContextStorage.run(ctx, () => fn(...args))
}
}
```
### 4.4 `src/proxy.ts` 改造
```ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { getToken } from "next-auth/jwt"
// ... 原有 imports
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// 生成或复用 requestIdWeb Crypto APIEdge 兼容)
const requestId =
request.headers.get("x-request-id") ?? crypto.randomUUID()
// 跳过静态资源和登录页
if (
pathname.startsWith("/_next") ||
pathname.startsWith("/api/auth") ||
pathname === "/login" ||
pathname === "/register" ||
pathname === "/favicon.ico"
) {
return NextResponse.next({
request: { headers: injectRequestId(request, requestId) },
})
}
// ... 原有 token / onboarding / 权限检查逻辑
// 所有 NextResponse.next() / NextResponse.redirect() 调用保留,
// 但 NextResponse.next() 调用统一传入 request.headers
const response = NextResponse.next({
request: { headers: injectRequestId(request, requestId) },
})
response.headers.set("x-request-id", requestId)
return response
}
/**
* 创建包含 x-request-id 的新 Headers 对象。
* 通过 NextResponse.next({ request: { headers } }) 注入到下游 RSC 请求。
*/
function injectRequestId(request: NextRequest, requestId: string): Headers {
const headers = new Headers(request.headers)
headers.set("x-request-id", requestId)
return headers
}
```
> **说明**proxy.ts 不导入 `request-context.ts`,避免在 Edge Runtime 中加载 `node:async_hooks` 导致构建错误。
### 4.5 `src/shared/hooks/use-error-report.ts`
```ts
"use client"
import { useEffect } from "react"
interface ClientErrorPayload {
message: string
stack?: string
digest?: string
url: string
userAgent: string
timestamp: string
}
/**
* 客户端错误上报 Hook。
*
* 用于 error.tsx 接收 error prop 后上报到 /api/client-error。
*
* 节流策略:
* - 同一 digest或 message在 sessionStorage 中标记,避免 React 重渲染或快速刷新时多次上报
* - 上报失败时静默忽略,避免无限循环
*/
export function useErrorReport(error: Error & { digest?: string }): void {
useEffect(() => {
if (!error) return
const digest = error.digest ?? error.message
const storageKey = `error-reported:${digest}`
if (sessionStorage.getItem(storageKey)) return
sessionStorage.setItem(storageKey, "1")
const payload: ClientErrorPayload = {
message: error.message,
stack: error.stack,
digest: error.digest,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
}
fetch("/api/client-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
keepalive: true, // 即使页面卸载也尝试发送
}).catch(() => {
// 上报失败时不再上报,避免无限循环
})
}, [error])
}
```
### 4.6 `src/app/api/client-error/route.ts`
```ts
import { NextResponse } from "next/server"
import { createModuleLogger } from "@/shared/lib/logger"
import { withRequestContext } from "@/shared/lib/with-request-context"
const log = createModuleLogger("client-error")
interface ClientErrorPayload {
message: string
stack?: string
digest?: string
url: string
userAgent: string
timestamp: string
}
/**
* 接收客户端 error.tsx 上报的错误。
*
* 注意:客户端错误的 requestId 与原始请求不同(来自 /api/client-error 的 HTTP 请求),
* 但 digest 字段可用于关联原始错误。
*/
export const POST = withRequestContext(async (request: Request) => {
try {
const body = (await request.json()) as ClientErrorPayload
log.error(
{
clientMessage: body.message,
stack: body.stack,
digest: body.digest,
url: body.url,
userAgent: body.userAgent,
clientTimestamp: body.timestamp,
},
"Client error reported"
)
return NextResponse.json({ ok: true })
} catch (error) {
log.error({ err: error }, "Failed to parse client error payload")
return NextResponse.json({ ok: false }, { status: 400 })
}
})
```
### 4.7 `src/shared/lib/audit-logger.ts` 改造(静默失败 → 告警)
```ts
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("audit-logger")
export async function logAudit(params: LogAuditParams): Promise<void> {
try {
// ... 原有写入逻辑
} catch (error) {
// 旧catch { /* Silently fail */ }
// 新:记录到 logger运维可感知
log.warn(
{ err: error, action: params.action, module: params.module },
"Audit log write failed"
)
}
}
```
### 4.8 `src/shared/lib/action-utils.ts` 改造
```ts
import { createModuleLogger } from "@/shared/lib/logger"
import type { ActionState } from "@/shared/types/action-state"
import { PermissionDeniedError } from "@/shared/lib/errors"
const log = createModuleLogger("action")
export function handleActionError(e: unknown): ActionState<never> {
if (e instanceof PermissionDeniedError) {
return { success: false, message: e.message }
}
if (e instanceof BusinessError) {
return { success: false, message: e.message, errorCode: e.code }
}
if (e instanceof Error) {
// 旧console.error("[ActionError]", e.name, e.message, e.stack)
log.error({ err: e }, "Action failed")
return { success: false, message: "操作失败,请稍后重试", errorCode: "unexpected" }
}
log.error({ err: e }, "Unknown action error")
return { success: false, message: "操作失败,请稍后重试", errorCode: "unexpected" }
}
export async function safeActionCall<T>(
action: () => Promise<ActionState<T>>,
options?: {
onError?: (error: unknown) => void
onFinally?: () => void
}
): Promise<ActionState<T> | null> {
try {
return await action()
} catch (e) {
options?.onError?.(e)
// 旧console.error("[SafeActionCall]", e)
log.error({ err: e }, "Safe action call threw")
return null
} finally {
options?.onFinally?.()
}
}
```
### 4.9 `src/shared/lib/track-event.ts` 改造
```ts
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("track")
/**
* 业务埋点接口。
*
* 不再是 no-op stub通过 logger.info 输出结构化事件,
* 后续可扩展为接入外部 analytics 服务。
*/
export function trackEvent(
name: string,
props?: Record<string, unknown>
): void {
log.info({ event: name, ...props }, "track event")
}
export function trackExamEvent(
name: string,
props?: Record<string, unknown>
): void {
trackEvent(`exam.${name}`, props)
}
export function trackAuthEvent(
name: string,
props?: Record<string, unknown>
): void {
trackEvent(`auth.${name}`, props)
}
```
### 4.10 `src/env.mjs` 改造
```ts
// 在 server schema 中添加:
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
```
### 4.11 ESLint 规则
```js
// eslint.config.mjs
{
rules: {
"no-console": ["error", { allow: [], allowWithImplicit: false }]
},
// logger.ts 豁免pino 内部需用 console
overrides: [
{
files: ["src/shared/lib/logger.ts"],
rules: { "no-console": "off" }
}
]
}
```
---
## 5. 改造范围与策略
### 5.1 console.* 替换映射
| 类型 | 数量 | 替换为 | 调用方式 |
|------|------|--------|----------|
| `console.log` | 2 | `log.debug` | `const log = createModuleLogger("questions")` |
| `console.error` | 67 | `log.error` | 按文件所属模块创建子 logger |
| `console.warn` | 5 | `log.warn` | 同上 |
| `console.info` | 10 | `log.info` | 同上 |
| `console.debug` | 4 | `log.debug` | 同上 |
### 5.2 模块前缀规范化
当前散乱前缀 → 统一通过 `module` 字段:
| 当前前缀 | 新 module 字段 |
|---------|----------------|
| `[ExamAction]` | `exams` |
| `[ActionError]` | `action` |
| `[ApiError]` | `api` |
| `[AuditLogger]` | `audit-logger` |
| `[SafeActionCall]` | `action` |
| `[Files]` / `[files]` | `files` |
| 无前缀 | 按文件所属模块 |
### 5.3 Server Action 包装策略
由于 Server Action 通过 `"use server"` 自动成为 RPC不能简单用 `withRequestContext` 包装导出函数(会丢失 Next.js 类型推断)。且 `handleActionError` 是同步函数,无法 `await headers()` 读取请求头。
**最终策略**:在每个 Server Action 模块的入口点调用 `withRequestContext` 包装:
```ts
// src/modules/audit/actions.ts
"use server"
import { withRequestContext } from "@/shared/lib/with-request-context"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("audit")
export const createAuditLogAction = withRequestContext(
async (state: ActionState<void>, input: CreateAuditInput) => {
// 此处 logger 自动带 requestId
log.info({ input }, "Creating audit log")
return handleAction(...)
}
)
```
**data-access 层无需显式包装**:因为 data-access 总是从 Server Action 调用AsyncLocalStorage 上下文会自动贯穿到调用栈下游。data-access 中的 logger 调用会自动获得 requestId。
**实施时验证项**
- Next.js 16 是否允许高阶函数包装 Server Action保留 NextServerAction 标记)
- 若不允许,回退方案:在每个 Server Action 函数体首行调用 `await initRequestContext()`,该函数内部读取 headers 并写入 AsyncLocalStorage
### 5.4 error.tsx 改造模式
130 个 error.tsx 统一改为:
```tsx
"use client"
import { useEffect } from "react"
import { useErrorReport } from "@/shared/hooks/use-error-report"
// ... 原有 imports
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useErrorReport(error)
// ... 原有 UI 渲染
}
```
每个 error.tsx 仅增加 2 行import + 调用 Hook
---
## 6. 测试策略
### 6.1 单元测试
| 测试文件 | 覆盖内容 |
|---------|---------|
| `__tests__/logger.test.ts` | level 控制、JSON 输出格式、mixin 注入 requestId、createModuleLogger |
| `__tests__/request-context.test.ts` | AsyncLocalStorage 读写、嵌套调用、空上下文 |
| `__tests__/use-error-report.test.tsx` | 节流sessionStorage 标记、fetch 调用、错误处理 |
### 6.2 集成测试
- 启动开发服务器,发起请求,验证日志包含 requestId
- 触发 Server Action 错误,验证日志包含 module + requestId + error stack
- 触发 client error.tsx验证 `/api/client-error` 收到请求并记录日志
### 6.3 回归验证
- `npx tsc --noEmit` 零错误
- `npm run lint` 零错误(含新增 `no-console` 规则)
- 现有 vitest 测试套件全部通过
---
## 7. 风险与权衡
| 风险 | 影响 | 缓解 |
|------|------|------|
| pino 在 Next.js bundling 中可能有问题 | 高 | 已配置 `serverExternalPackages`pino 仅服务端导入;开发期 `pino-pretty` 通过 transport 配置启用 |
| `AsyncLocalStorage` 在 Server Action 中可能不工作 | 中 | Server Action 运行在 Node.js runtime完全支持实施时先做最小验证 |
| Server Action 高阶函数包装可能丢失 Next.js 类型 | 中 | 实施时验证,必要时回退为显式 `headers()` 调用 |
| 88 处 console.* 替换可能遗漏 | 低 | 通过 ESLint `no-console` 规则强制,仅允许 `logger.ts` 中使用 |
| 130 个 error.tsx 改造量大 | 中 | 提取公共 `useErrorReport` Hook每个 error.tsx 仅加 2 行 |
| track-event 合并可能破坏调用方 | 低 | 删除前 grep 所有引用,统一改为从 `@/shared/lib/track-event` 导入 |
| 客户端错误风暴(无限循环上报) | 中 | sessionStorage 节流 + fetch 失败静默 + `keepalive` 选项 |
| proxy.ts 改造可能影响 Edge Runtime 构建 | 中 | 不导入任何 Node.js 模块,仅用 Web Crypto API |
---
## 8. 架构图同步(强制)
按项目规则"改码必同步图",重构后必须更新:
### 8.1 `docs/architecture/004_architecture_impact_map.md`
新增章节:
- `shared/lib/logger.ts` — pino 实例 + createModuleLogger
- `shared/lib/request-context.ts` — AsyncLocalStorage 请求上下文
- `shared/lib/with-request-context.ts` — Server Action / Route Handler 入口包装
- `shared/hooks/use-error-report.ts` — 客户端错误上报 Hook
- `app/api/client-error/route.ts` — 客户端错误接收端点
修改章节:
- `proxy.ts` — 增加 requestId 注入逻辑
- `shared/lib/audit-logger.ts` / `change-logger.ts` / `login-logger.ts` — 静默失败改为 logger.warn
- `shared/lib/track-event.ts` — 实现层从 no-op 改为 logger.info
- `shared/lib/action-utils.ts` / `api-response.ts` — 接入 logger
删除记录:
- `modules/rbac/lib/track.ts`
- `modules/course-plans/lib/track-event.ts`
- `modules/questions/utils/track-event.ts`
### 8.2 `docs/architecture/005_architecture_data.json`
- `modules.shared.lib.exports` 新增 `logger` / `createModuleLogger` / `requestContextStorage` / `getRequestContext` / `withRequestContext`
- `modules.shared.hooks.exports` 新增 `useErrorReport`
- `modules.app.api.client-error.exports` 新增 `POST`
- `dependencyMatrix` 更新proxy.ts → request-context仅注入请求头不导入
- 删除 `modules.rbac.lib.track``modules.course-plans.lib.track-event``modules.questions.utils.track-event`
### 8.3 `docs/troubleshooting/known-issues.md`
新增规则条目:
- pino 集成:`serverExternalPackages` 配置 + `pino-pretty` 仅 dev
- Edge Runtime 限制proxy.ts 不能导入 `node:async_hooks`
- Server Action 包装:`withRequestContext` 高阶函数使用模式
- error.tsx 错误上报:`useErrorReport` Hook + 节流策略
- ESLint `no-console` 规则与豁免
---
## 9. 实施顺序建议
1. **Phase 1基础底座**
- 装 pino / pino-pretty
- 创建 `logger.ts` / `request-context.ts` / `with-request-context.ts`
-`env.mjs` 添加 LOG_LEVEL
- 单元测试
2. **Phase 2核心接入**
- proxy.ts 注入 requestId
- `action-utils.ts` / `api-response.ts` 接入 logger
- 三个 audit-logger 静默失败 → logger.warn
- track-event 去重合并
3. **Phase 3批量替换**
- 88 处 console.* 替换为 logger.*
- 前缀规范化
- ESLint `no-console` 规则启用
4. **Phase 4error.tsx 上报**
- 创建 `use-error-report` Hook
- 创建 `/api/client-error` Route Handler
- 130 个 error.tsx 接入
5. **Phase 5架构同步**
- 更新 004 / 005 / known-issues.md
- 验证 tsc / lint / 测试通过