From e3d132dc1bd13f65ba32a44bfd3fca0004247dc0 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:25:33 +0800 Subject: [PATCH] docs(known-issues): log enterprise architecture normalization work experience --- docs/troubleshooting/known-issues.md | 449 ++++++++++++++------------- scripts/arch-scan/arch.db | Bin 3477504 -> 3477504 bytes 2 files changed, 226 insertions(+), 223 deletions(-) diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md index 509f5e8..da1319c 100644 --- a/docs/troubleshooting/known-issues.md +++ b/docs/troubleshooting/known-issues.md @@ -10,158 +10,158 @@ ### 1.1 i18n -| 场景 | 规则 | -|------|------| -| key 命名 | 禁止包含 `.`,不同字段用驼峰,同字段多角色变体用嵌套对象 | -| 动态 key 调用 | `t()` 参数为动态变量时必须包含完整嵌套路径(`t(\`type.${type}\`)` 而非 `t(type)`) | -| ICU 参数 | 单花括号 `{level}`,多参数直接并列 `{count} 题 · {subject}` | -| Namespace 与文件名一致性 | namespace 必须与文件名严格一致 | -| 翻译文件对称性 | zh-CN 与 en 必须同步新增/删除 key | -| 硬编码中文 | 所有用户可见文本必须走 i18n(AI Prompt 中文常量允许) | -| i18n dead 节清理 | 删除组件时同步清理对应的 i18n key | -| 动态键翻译守卫 | 缺失 key 时降级返回 key 本身而非抛错 | +| 场景 | 规则 | +| ------------------------ | -------------------------------------------------------------------------------- | +| key 命名 | 禁止包含 `.`,不同字段用驼峰,同字段多角色变体用嵌套对象 | +| 动态 key 调用 | `t()` 参数为动态变量时必须包含完整嵌套路径(`t(\`type.${type}\`)`而非`t(type)`) | +| ICU 参数 | 单花括号 `{level}`,多参数直接并列 `{count} 题 · {subject}` | +| Namespace 与文件名一致性 | namespace 必须与文件名严格一致 | +| 翻译文件对称性 | zh-CN 与 en 必须同步新增/删除 key | +| 硬编码中文 | 所有用户可见文本必须走 i18n(AI Prompt 中文常量允许) | +| i18n dead 节清理 | 删除组件时同步清理对应的 i18n key | +| 动态键翻译守卫 | 缺失 key 时降级返回 key 本身而非抛错 | ### 1.2 Next.js 配置与运行时 -| 场景 | 技术/规则 | -|------|----------| -| Node.js 服务端驱动 | `serverExternalPackages: ["mysql2"]` 标记为外部包 | -| 输出模式 | `output: "standalone"` | -| Tailwind v4 排除非源码 | `globals.css` 中 `@source not "../../docs";` 防止 docs 字符串被扫描成类 | -| 可选依赖 | 动态 import + `/* webpackIgnore: true */`(如 `@upstash/redis`) | -| 环境变量驱动加载 | `if (env.DRIVER === "redis") { const { Redis } = await import(...) }` | -| next-auth useSession SSR | 客户端用 `useSession()` 时需 `SessionProvider` + 等待 `status === "authenticated"` | -| useSearchParams Hydration | 包裹 `` 或在 useEffect 中读取 | -| Edge Runtime 限制 | proxy.ts 不能导入 Node.js 模块,用 Web Crypto API | -| React StrictMode 与 Server Action | StrictMode 双调用不影响 Server Action(idempotent 设计) | +| 场景 | 技术/规则 | +| --------------------------------- | ---------------------------------------------------------------------------------- | +| Node.js 服务端驱动 | `serverExternalPackages: ["mysql2"]` 标记为外部包 | +| 输出模式 | `output: "standalone"` | +| Tailwind v4 排除非源码 | `globals.css` 中 `@source not "../../docs";` 防止 docs 字符串被扫描成类 | +| 可选依赖 | 动态 import + `/* webpackIgnore: true */`(如 `@upstash/redis`) | +| 环境变量驱动加载 | `if (env.DRIVER === "redis") { const { Redis } = await import(...) }` | +| next-auth useSession SSR | 客户端用 `useSession()` 时需 `SessionProvider` + 等待 `status === "authenticated"` | +| useSearchParams Hydration | 包裹 `` 或在 useEffect 中读取 | +| Edge Runtime 限制 | proxy.ts 不能导入 Node.js 模块,用 Web Crypto API | +| React StrictMode 与 Server Action | StrictMode 双调用不影响 Server Action(idempotent 设计) | ### 1.3 Server/Client Component 边界 -| 场景 | 规则 | -|------|------| -| "use server" 文件 | 只能导出 `async function`;常量放 `types.ts`/`constants.ts`;同步函数放 `lib/`;禁止值 re-export;类型 re-export 允许 | -| Client Component 依赖隔离 | 不得 import server-only 文件;`import { db }` 的文件不得被客户端 import | -| Server Component 传递函数 | 禁止向 Client Component 传递函数,改传纯数据 + 在客户端调用 Server Action | -| Server Component 传递纯数据 | 序列化日期用 ISO string,不用 Date 对象 | -| useState SSR/CSR 一致性 | 初始值必须可序列化且在服务端和客户端一致 | +| 场景 | 规则 | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| "use server" 文件 | 只能导出 `async function`;常量放 `types.ts`/`constants.ts`;同步函数放 `lib/`;禁止值 re-export;类型 re-export 允许 | +| Client Component 依赖隔离 | 不得 import server-only 文件;`import { db }` 的文件不得被客户端 import | +| Server Component 传递函数 | 禁止向 Client Component 传递函数,改传纯数据 + 在客户端调用 Server Action | +| Server Component 传递纯数据 | 序列化日期用 ISO string,不用 Date 对象 | +| useState SSR/CSR 一致性 | 初始值必须可序列化且在服务端和客户端一致 | ### 1.4 数据库与 Drizzle -| 场景 | 规则 | -|------|------| -| schema.ts 修改后 | 必须 `npm run db:push` 或 `npm run db:migrate` 同步数据库 | -| `Unknown column` 报错 | 先 `SHOW COLUMNS FROM 表名` 对比实际结构与 schema.ts | -| 迁移文件管理 | 必须录入 `_journal.json`;编号唯一不可复用;`__drizzle_migrations` 表必须记录 hash | -| 手动补迁移流程 | 创建 `.sql` → 更新 journal → 计算 sha256 插入 migrations 表 → 验证 `No new migrations to apply` | -| 字段映射 | TS 属性名与 DB 列名不同时,验证 SQL 用 DB 列名,ORM 查询用 TS 属性名 | -| 子查询字段 alias | Drizzle 子查询字段必须显式 alias,否则类型推断失败 | -| FULLTEXT 索引声明 | drizzle-kit 无法生成,迁移文件手动追加 `CREATE FULLTEXT INDEX` + 同步 snapshot | -| MATCH AGAINST BOOLEAN MODE | 转义特殊字符 `+ - < > ( ) ~ * " '`,转 `+word1* +word2*` 格式 | +| 场景 | 规则 | +| -------------------------- | ----------------------------------------------------------------------------------------------- | +| schema.ts 修改后 | 必须 `npm run db:push` 或 `npm run db:migrate` 同步数据库 | +| `Unknown column` 报错 | 先 `SHOW COLUMNS FROM 表名` 对比实际结构与 schema.ts | +| 迁移文件管理 | 必须录入 `_journal.json`;编号唯一不可复用;`__drizzle_migrations` 表必须记录 hash | +| 手动补迁移流程 | 创建 `.sql` → 更新 journal → 计算 sha256 插入 migrations 表 → 验证 `No new migrations to apply` | +| 字段映射 | TS 属性名与 DB 列名不同时,验证 SQL 用 DB 列名,ORM 查询用 TS 属性名 | +| 子查询字段 alias | Drizzle 子查询字段必须显式 alias,否则类型推断失败 | +| FULLTEXT 索引声明 | drizzle-kit 无法生成,迁移文件手动追加 `CREATE FULLTEXT INDEX` + 同步 snapshot | +| MATCH AGAINST BOOLEAN MODE | 转义特殊字符 `+ - < > ( ) ~ * " '`,转 `+word1* +word2*` 格式 | ### 1.5 状态管理 -| 场景 | 技术 | -|------|------| -| 5 层状态模型 | L1 URL(nuqs) · L2 Server(TanStack Query) · L3 Client(Zustand slice) · L4 Global UI(Zustand ui-store + ModalRoot) · L5 Form(react-hook-form + zodResolver) | -| Zustand selector | 单字段 selector 优于 `useShallow` 多字段包装 | -| useOptimistic | 乐观更新(消息星标、成绩编辑、考勤提交),`addOptimistic` 必须在 transition 或 action 内调用 | -| useOptimistic 不适用场景 | 需要服务端返回判断成功的操作(如撤回校验时间窗口) | -| React 19 useTransition | 配合 useOptimistic 自动管理乐观状态与回滚 | +| 场景 | 技术 | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 5 层状态模型 | L1 URL(nuqs) · L2 Server(TanStack Query) · L3 Client(Zustand slice) · L4 Global UI(Zustand ui-store + ModalRoot) · L5 Form(react-hook-form + zodResolver) | +| Zustand selector | 单字段 selector 优于 `useShallow` 多字段包装 | +| useOptimistic | 乐观更新(消息星标、成绩编辑、考勤提交),`addOptimistic` 必须在 transition 或 action 内调用 | +| useOptimistic 不适用场景 | 需要服务端返回判断成功的操作(如撤回校验时间窗口) | +| React 19 useTransition | 配合 useOptimistic 自动管理乐观状态与回滚 | ### 1.6 性能优化 -| 场景 | 技术 | -|------|------| -| 批量 INSERT | `db.insert().values([...])` 单次写入 | -| 批量 UPDATE | `sql.join` + `CASE WHEN` 单次执行 | -| 递归删除 | 批量收集后代 ID + `inArray` 单次删除,禁止递归逐个 | -| 动态导入重 UI 库 | `xxx-inner.tsx` + `xxx.tsx` lazy wrapper(tiptap / @xyflow/react / AI SDK) | -| ReactFlowProvider | 留在外层同步渲染,保证 lazy 子组件 hook 可用 | -| 请求级去重 | React `cache()` 包装(auth 相关 4 函数) | -| 跨请求缓存 | 不用 `unstable_cache`(权限数据易变) | -| 高 LIMIT 调低 | 非分页场景默认 LIMIT ≤ 100 | -| 内存 filter 改 SQL | `inArray(field, ids)` 下推 | -| 统计走 SQL 聚合 | `COUNT(*)`/`SUM(CASE WHEN ...)` 替代拉全表内存循环 | -| chart props 模块级常量 | 12 个 recharts 图表提取 `CHART_MARGIN` | -| 高频子组件 React.memo | `AssignmentCard` / `StatusBadge` / `EmptyState` | -| 虚拟化大表 | `@tanstack/react-virtual` | +| 场景 | 技术 | +| ---------------------- | --------------------------------------------------------------------------- | +| 批量 INSERT | `db.insert().values([...])` 单次写入 | +| 批量 UPDATE | `sql.join` + `CASE WHEN` 单次执行 | +| 递归删除 | 批量收集后代 ID + `inArray` 单次删除,禁止递归逐个 | +| 动态导入重 UI 库 | `xxx-inner.tsx` + `xxx.tsx` lazy wrapper(tiptap / @xyflow/react / AI SDK) | +| ReactFlowProvider | 留在外层同步渲染,保证 lazy 子组件 hook 可用 | +| 请求级去重 | React `cache()` 包装(auth 相关 4 函数) | +| 跨请求缓存 | 不用 `unstable_cache`(权限数据易变) | +| 高 LIMIT 调低 | 非分页场景默认 LIMIT ≤ 100 | +| 内存 filter 改 SQL | `inArray(field, ids)` 下推 | +| 统计走 SQL 聚合 | `COUNT(*)`/`SUM(CASE WHEN ...)` 替代拉全表内存循环 | +| chart props 模块级常量 | 12 个 recharts 图表提取 `CHART_MARGIN` | +| 高频子组件 React.memo | `AssignmentCard` / `StatusBadge` / `EmptyState` | +| 虚拟化大表 | `@tanstack/react-virtual` | ### 1.7 设计令牌 -| 场景 | 规则 | -|------|------| -| 禁止硬编码颜色 | TSX/TS/CSS 中不得出现 `#hex`,用 `hsl(var(--*))` 或 Tailwind 类 | -| 禁止硬编码字体 | 不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量 | -| 禁止硬编码字号 | 不得出现 `font-size: Npx`,用 `var(--font-size-1~9)` | -| 禁止 Tailwind 任意值 | 不得使用 `w-[Npx]`/`h-[Npx]`,映射到 `--space-*` | -| 豁免场景 | PWA manifest、邮件 HTML 内联样式、图表 SVG 固定画布、loading.tsx 骨架 | -| 豁免注释格式 | `// eslint-disable-next-line no-restricted-syntax -- ` | -| 白名单文件 | `primitive.css`、`email-channel.ts`、`manifest.ts` | +| 场景 | 规则 | +| -------------------- | --------------------------------------------------------------------- | +| 禁止硬编码颜色 | TSX/TS/CSS 中不得出现 `#hex`,用 `hsl(var(--*))` 或 Tailwind 类 | +| 禁止硬编码字体 | 不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量 | +| 禁止硬编码字号 | 不得出现 `font-size: Npx`,用 `var(--font-size-1~9)` | +| 禁止 Tailwind 任意值 | 不得使用 `w-[Npx]`/`h-[Npx]`,映射到 `--space-*` | +| 豁免场景 | PWA manifest、邮件 HTML 内联样式、图表 SVG 固定画布、loading.tsx 骨架 | +| 豁免注释格式 | `// eslint-disable-next-line no-restricted-syntax -- ` | +| 白名单文件 | `primitive.css`、`email-channel.ts`、`manifest.ts` | ### 1.8 TypeScript -| 场景 | 规则 | -|------|------| -| 禁止 `any` | 未知类型用 `unknown` + 类型守卫 | -| 禁止 `as` 断言 | 除非从 `unknown` 转换或测试中(需注释原因) | -| 类型守卫优先 | JSON.parse 结果经 `unknown` 中转 + 守卫;localStorage/URL param/HTTP 响应需守卫 | -| 模块私有类型守卫 | 放 `lib/type-guards.ts`,导出 `isX` / `toX` | -| 函数返回值 | 必须显式标注,特别是 `Promise` | -| 仅类型导入 | `import type` | -| 可选链后禁止 `!` | `a?.b!` 禁止 | -| 非空断言 `!` | 数组取值后判空再访问;`split` 结果用 `??` 兜底 | -| 空对象赋值 | 显式标注 `const empty: Record = {}` | +| 场景 | 规则 | +| ---------------- | ------------------------------------------------------------------------------- | +| 禁止 `any` | 未知类型用 `unknown` + 类型守卫 | +| 禁止 `as` 断言 | 除非从 `unknown` 转换或测试中(需注释原因) | +| 类型守卫优先 | JSON.parse 结果经 `unknown` 中转 + 守卫;localStorage/URL param/HTTP 响应需守卫 | +| 模块私有类型守卫 | 放 `lib/type-guards.ts`,导出 `isX` / `toX` | +| 函数返回值 | 必须显式标注,特别是 `Promise` | +| 仅类型导入 | `import type` | +| 可选链后禁止 `!` | `a?.b!` 禁止 | +| 非空断言 `!` | 数组取值后判空再访问;`split` 结果用 `??` 兜底 | +| 空对象赋值 | 显式标注 `const empty: Record = {}` | ### 1.9 API 路由 -| 场景 | 规则 | -|------|------| -| 响应信封 | `{ success, message?, errorCode?, data? }` 与 `ActionState` 对齐 | -| 错误→状态码 | PermissionDeniedError→403 / NotFoundError→404 / ValidationError→400 / BusinessError→400 / 其他→500 | -| 统一异常处理 | `withApiErrorHandler(handler)` HOF 消除重复 try/catch | -| Action 转 API | `apiFromAction(result, options?)` 通过 `errorCode` 驱动状态码 | -| SSE 头 | `Content-Type: text/event-stream` + `Cache-Control: no-cache, no-transform` + `Connection: keep-alive` | -| SSE 错误格式 | `{ type: "error", message }` | -| SSE 结束信号 | `[DONE]` | -| JSDoc 注释 | 禁止 `**/` 序列(破坏注释解析) | -| Permissions 类型 | `Permissions` 是常量对象,`Permission` 是联合类型,不可混用 | +| 场景 | 规则 | +| ---------------- | ------------------------------------------------------------------------------------------------------ | +| 响应信封 | `{ success, message?, errorCode?, data? }` 与 `ActionState` 对齐 | +| 错误→状态码 | PermissionDeniedError→403 / NotFoundError→404 / ValidationError→400 / BusinessError→400 / 其他→500 | +| 统一异常处理 | `withApiErrorHandler(handler)` HOF 消除重复 try/catch | +| Action 转 API | `apiFromAction(result, options?)` 通过 `errorCode` 驱动状态码 | +| SSE 头 | `Content-Type: text/event-stream` + `Cache-Control: no-cache, no-transform` + `Connection: keep-alive` | +| SSE 错误格式 | `{ type: "error", message }` | +| SSE 结束信号 | `[DONE]` | +| JSDoc 注释 | 禁止 `**/` 序列(破坏注释解析) | +| Permissions 类型 | `Permissions` 是常量对象,`Permission` 是联合类型,不可混用 | ### 1.10 日志系统 -| 场景 | 规则 | -|------|------| -| 服务端日志 | `createModuleLogger("module-name")` + pino,禁止 `console.*` | -| 日志参数顺序 | `log.error({ err: error, userId }, "操作描述")` | -| 错误对象字段名 | `err`(不是 `error`) | -| 模块级 logger 命名 | kebab-case(`audit` / `exams` / `client-error`) | -| ESLint no-console | 服务端强制,客户端 track-event 改为 no-op | -| 客户端错误上报 | 通过 track-event 上报,不导入服务端 logger | -| Request ID | proxy.ts 注入 + `withRequestContext` 包装 Server Action + `getRequestContext().requestId` 读取 | +| 场景 | 规则 | +| ------------------ | ---------------------------------------------------------------------------------------------- | +| 服务端日志 | `createModuleLogger("module-name")` + pino,禁止 `console.*` | +| 日志参数顺序 | `log.error({ err: error, userId }, "操作描述")` | +| 错误对象字段名 | `err`(不是 `error`) | +| 模块级 logger 命名 | kebab-case(`audit` / `exams` / `client-error`) | +| ESLint no-console | 服务端强制,客户端 track-event 改为 no-op | +| 客户端错误上报 | 通过 track-event 上报,不导入服务端 logger | +| Request ID | proxy.ts 注入 + `withRequestContext` 包装 Server Action + `getRequestContext().requestId` 读取 | ### 1.11 测试 -| 场景 | 规则 | -|------|------| -| Vitest + Next.js 16 缓存测试 | server-only alias 到空 stub;`cache()` 包装函数测试需 mock | -| Playwright 检测 | 检查 HTTP 状态码 + pageerror(循环外注册一次)+ 可见错误元素,不检查 HTML content | -| testTimeout | ts-morph 解析慢,arch-scan 测试设置 60000ms | +| 场景 | 规则 | +| ---------------------------- | --------------------------------------------------------------------------------- | +| Vitest + Next.js 16 缓存测试 | server-only alias 到空 stub;`cache()` 包装函数测试需 mock | +| Playwright 检测 | 检查 HTTP 状态码 + pageerror(循环外注册一次)+ 可见错误元素,不检查 HTML content | +| testTimeout | ts-morph 解析慢,arch-scan 测试设置 60000ms | ### 1.12 ESLint 与验证命令 -| 场景 | 命令/规则 | -|------|----------| -| 类型检查 | `npx tsc --noEmit` | -| Lint | `npm run lint` | -| 开发服务器 | `npm run dev` | -| 生产构建 | `npm run build` | -| 同步 schema | `npm run db:push` | -| 执行迁移 | `npm run db:migrate` | -| 验证 schema 一致性 | `node scripts/verify-modules-schema.mjs` | +| 场景 | 命令/规则 | +| -------------------- | ---------------------------------------------------------------------- | +| 类型检查 | `npx tsc --noEmit` | +| Lint | `npm run lint` | +| 开发服务器 | `npm run dev` | +| 生产构建 | `npm run build` | +| 同步 schema | `npm run db:push` | +| 执行迁移 | `npm run db:migrate` | +| 验证 schema 一致性 | `node scripts/verify-modules-schema.mjs` | | 检查 i18n key 含 `.` | Grep `^\s*"[a-zA-Z_]+\.[a-zA-Z_\.]+"\s*:` glob `**/messages/**/*.json` | -| arch.db 扫描 | `npm run arch:scan` | -| arch.db 查询 | `npm run arch:query -- ` | -| ESLint 零警告 | 所有修改文件必须 0 errors 0 warnings | -| 自定义规则加载 | Windows 路径用 `path.join`,不用 `path.posix.join` | +| arch.db 扫描 | `npm run arch:scan` | +| arch.db 查询 | `npm run arch:query -- ` | +| ESLint 零警告 | 所有修改文件必须 0 errors 0 warnings | +| 自定义规则加载 | Windows 路径用 `path.join`,不用 `path.posix.join` | --- @@ -169,97 +169,97 @@ ### 2.1 lesson-preparation(备课) -| 场景 | 规则 | -|------|------| -| Zustand store 拆 slice | mutation 方法包装 history(`pushHistory` + `set`);`updateNodePosition` 不推 history;hydrate/replaceDoc 清空历史;history 栈上限 `MAX_HISTORY` | -| 撤销/重做快捷键 | `metaKey \|\| ctrlKey` 区分 Mac;Shift+Z 或 Y 重做;`preventDefault` | -| 自动保存失败 UI | toast.error + 断网时不触发 + 监听 online/offline + saveError 显示重试按钮 | -| 发布前预览 3 步 | `select → preview → confirm` 状态机;总分 useMemo;确认页警告文案 | -| Tiptap Image 扩展 | `inline: false, allowBase64: false`;`editor.commands.setImage`;不自建上传组件 | -| Tiptap SSR | `immediatelyRender: false` 避免 hydration mismatch | -| 附件表 Date 转换 | data-access 返回 Date,service 转换为 ISO string | -| 打印视图 | `print:hidden` / `print:static` / `break-inside-avoid` / `print:fixed` | -| 素材库 picker | 复用 `use-file-upload` + 通过 service 调用 + `FileTargetType` 扩展 | -| Server Action form action 适配 | `duplicateLessonPlanFormAction(formData)` 内部调用 + `redirect()` | -| 知识点按需加载 | 对话框 Tab 内 useEffect 调用,不首屏 Promise.all | -| AI 模块纯服务端 | `import "server-only"` + Zod 验证 + 失败降级返回空 | -| 版本对比/一致性校验 | 纯函数 `diffDocuments` / `checkConsistency` / `computeCurriculumCoverage` | -| 移动端切换 | `useMediaQuery` 在 readonly-view,避免服务端 useState 默认值不一致 | -| 类型扩展 | 新增可选字段 `stage?` / `differentiation?`,不破坏旧数据 | -| chapterId null | `kp.chapterId ?? null`(`string \| undefined` → `string \| null`) | -| React Flow 移除 | 改为三列布局(sidebar + paper area + detail panel),纸感界面 | -| 师生互动节点 | 渲染为对话 + '师'/'生' 角色标签 + 虚线分隔 | -| 节点展开/收起 | 左侧栏、右侧面板、右键菜单三处入口一致 | -| 右键菜单 | 节点操作(展开/收起、移动、复制、删除)+ AI 辅助(生成内容、优化表达、差异化建议、分层提问) | -| 排版差异 | Fraunces 16px 主文(近黑)+ Inter 13.5px 节点(暖灰)+ 左 2px 细线 | +| 场景 | 规则 | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Zustand store 拆 slice | mutation 方法包装 history(`pushHistory` + `set`);`updateNodePosition` 不推 history;hydrate/replaceDoc 清空历史;history 栈上限 `MAX_HISTORY` | +| 撤销/重做快捷键 | `metaKey \|\| ctrlKey` 区分 Mac;Shift+Z 或 Y 重做;`preventDefault` | +| 自动保存失败 UI | toast.error + 断网时不触发 + 监听 online/offline + saveError 显示重试按钮 | +| 发布前预览 3 步 | `select → preview → confirm` 状态机;总分 useMemo;确认页警告文案 | +| Tiptap Image 扩展 | `inline: false, allowBase64: false`;`editor.commands.setImage`;不自建上传组件 | +| Tiptap SSR | `immediatelyRender: false` 避免 hydration mismatch | +| 附件表 Date 转换 | data-access 返回 Date,service 转换为 ISO string | +| 打印视图 | `print:hidden` / `print:static` / `break-inside-avoid` / `print:fixed` | +| 素材库 picker | 复用 `use-file-upload` + 通过 service 调用 + `FileTargetType` 扩展 | +| Server Action form action 适配 | `duplicateLessonPlanFormAction(formData)` 内部调用 + `redirect()` | +| 知识点按需加载 | 对话框 Tab 内 useEffect 调用,不首屏 Promise.all | +| AI 模块纯服务端 | `import "server-only"` + Zod 验证 + 失败降级返回空 | +| 版本对比/一致性校验 | 纯函数 `diffDocuments` / `checkConsistency` / `computeCurriculumCoverage` | +| 移动端切换 | `useMediaQuery` 在 readonly-view,避免服务端 useState 默认值不一致 | +| 类型扩展 | 新增可选字段 `stage?` / `differentiation?`,不破坏旧数据 | +| chapterId null | `kp.chapterId ?? null`(`string \| undefined` → `string \| null`) | +| React Flow 移除 | 改为三列布局(sidebar + paper area + detail panel),纸感界面 | +| 师生互动节点 | 渲染为对话 + '师'/'生' 角色标签 + 虚线分隔 | +| 节点展开/收起 | 左侧栏、右侧面板、右键菜单三处入口一致 | +| 右键菜单 | 节点操作(展开/收起、移动、复制、删除)+ AI 辅助(生成内容、优化表达、差异化建议、分层提问) | +| 排版差异 | Fraunces 16px 主文(近黑)+ Inter 13.5px 节点(暖灰)+ 左 2px 细线 | ### 2.2 exams / homework / grades -| 场景 | 规则 | -|------|------| -| AI 出题 | `exams/ai-pipeline.ts` + `createAiChatCompletion` + JSON 解析 + Zod 校验 | -| 作业化考试 | `sourceExamId` 外键关联;`homeworkSubmissions` + `homeworkAnswers` 表 | -| 批改事务 | `db.transaction` 包裹多步写 | -| 成绩批量更新 | `useOptimistic` | +| 场景 | 规则 | +| ------------- | ------------------------------------------------------------------------ | +| AI 出题 | `exams/ai-pipeline.ts` + `createAiChatCompletion` + JSON 解析 + Zod 校验 | +| 作业化考试 | `sourceExamId` 外键关联;`homeworkSubmissions` + `homeworkAnswers` 表 | +| 批改事务 | `db.transaction` 包裹多步写 | +| 成绩批量更新 | `useOptimistic` | | FULLTEXT 检索 | questions 表 `content_text` STORED 生成列 + `MATCH AGAINST BOOLEAN MODE` | ### 2.3 messaging / notifications -| 场景 | 规则 | -|------|------| -| 消息星标乐观更新 | `useOptimistic` + `useTransition` + `addOptimisticStarred` | -| 消息列表星标 Map | `useOptimistic>`(key=messageId) | -| 撤回不乐观更新 | 需服务端返回判断 2 分钟窗口 | -| 通知批量化 | `createNotifications(items)` 单次 INSERT;in_app 批量 + 其他渠道并行 | -| 通知偏好 | 已从 messaging 迁移至 notifications | -| fan-out 分页 | `getAllUserIds(limit=1000, offset)` 分页遍历 | +| 场景 | 规则 | +| ---------------- | -------------------------------------------------------------------- | +| 消息星标乐观更新 | `useOptimistic` + `useTransition` + `addOptimisticStarred` | +| 消息列表星标 Map | `useOptimistic>`(key=messageId) | +| 撤回不乐观更新 | 需服务端返回判断 2 分钟窗口 | +| 通知批量化 | `createNotifications(items)` 单次 INSERT;in_app 批量 + 其他渠道并行 | +| 通知偏好 | 已从 messaging 迁移至 notifications | +| fan-out 分页 | `getAllUserIds(limit=1000, offset)` 分页遍历 | ### 2.4 attendance -| 场景 | 规则 | -|------|------| -| 考勤提交乐观更新 | `useOptimistic` 保留 form action 模式(`useFormStatus` 依赖 form action promise) | -| 不用 useTransition 包裹 | 会导致 form action 立即返回、`useFormStatus` 不可靠 | +| 场景 | 规则 | +| ----------------------- | ------------------------------------------------------------------------------------------ | +| 考勤提交乐观更新 | `useOptimistic` 保留 form action 模式(`useFormStatus` 依赖 form action promise) | +| 不用 useTransition 包裹 | 会导致 form action 立即返回、`useFormStatus` 不可靠 | ### 2.5 school / classes / scheduling -| 场景 | 规则 | -|------|------| -| school data-access 拆分 | `data-access-{departments,grades,subjects,classrooms,semesters,schools}.ts` + barrel | -| 角色判断迁出 data-access | `lib/school-permissions.ts` 导出 `getSchoolsForUser(userId)` | -| promoteGrades 事务 | `db.transaction` 包裹 select + update 循环 | -| classSchedule 写入口 | 统一到 scheduling 模块 | -| 跨模块批量接口 | `getGradeNamesByIds(gradeIds): Promise>` | -| A-06 JOIN 改批量查询 | 移除 `.leftJoin(textbooks, ...)`,改 `getTextbookTitlesByIds(ids)` + Map 合并 | -| 单条查询用单 ID 接口 | `getClassNameById(classId)` 替代 `db.select().from(classes)` | +| 场景 | 规则 | +| ------------------------ | ------------------------------------------------------------------------------------ | +| school data-access 拆分 | `data-access-{departments,grades,subjects,classrooms,semesters,schools}.ts` + barrel | +| 角色判断迁出 data-access | `lib/school-permissions.ts` 导出 `getSchoolsForUser(userId)` | +| promoteGrades 事务 | `db.transaction` 包裹 select + update 循环 | +| classSchedule 写入口 | 统一到 scheduling 模块 | +| 跨模块批量接口 | `getGradeNamesByIds(gradeIds): Promise>` | +| A-06 JOIN 改批量查询 | 移除 `.leftJoin(textbooks, ...)`,改 `getTextbookTitlesByIds(ids)` + Map 合并 | +| 单条查询用单 ID 接口 | `getClassNameById(classId)` 替代 `db.select().from(classes)` | ### 2.6 audit / rbac -| 场景 | 规则 | -|------|------| -| 破坏性操作用专属权限 | `AUDIT_LOG_PURGE` ≠ `AUDIT_LOG_READ` | -| 权限点注册全链路 | `permissions.ts` → `ROLE_PERMISSIONS_SEED` → `permission-bitmap.ts` → `permission-catalog.ts` → i18n `rbac.json` 五处同步 | -| 审计日志保留管理 | `AUDIT_RETENTION_MANAGE` 权限点 | +| 场景 | 规则 | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| 破坏性操作用专属权限 | `AUDIT_LOG_PURGE` ≠ `AUDIT_LOG_READ` | +| 权限点注册全链路 | `permissions.ts` → `ROLE_PERMISSIONS_SEED` → `permission-bitmap.ts` → `permission-catalog.ts` → i18n `rbac.json` 五处同步 | +| 审计日志保留管理 | `AUDIT_RETENTION_MANAGE` 权限点 | ### 2.7 parent -| 场景 | 规则 | -|------|------| -| parent actions.ts | app/ 必须调用 `@/modules/parent/actions`,不直接 import data-access | -| 路由权限校验 | 同时校验 `parentId` + `studentId` 防止信息泄露 | -| dashboard 数据获取 | `Promise.all` / `Promise.allSettled` 并行 | -| dashboard 组件 | 用 Server Component,除非需要 `useMemo` | +| 场景 | 规则 | +| ------------------ | ------------------------------------------------------------------- | +| parent actions.ts | app/ 必须调用 `@/modules/parent/actions`,不直接 import data-access | +| 路由权限校验 | 同时校验 `parentId` + `studentId` 防止信息泄露 | +| dashboard 数据获取 | `Promise.all` / `Promise.allSettled` 并行 | +| dashboard 组件 | 用 Server Component,除非需要 `useMemo` | ### 2.8 shared -| 场景 | 规则 | -|------|------| -| session.ts 单一入口 | shared/lib/* 不再静态 import @/auth | -| dynamic import 打破循环 | `session.ts` 内部 `dynamic import("@/auth")` | -| 重复 helper 提取 | `serializeDate` / `downloadBase64File` / `getInitials` / `isRecord` / `extractQuestionPreview` | -| ROLE_NAMES 常量 | 替代 DB 查询中硬编码角色名 | -| api-response 工具 | `apiSuccess` / `apiError` / `apiFromAction` / `withApiErrorHandler` | -| sse 工具 | `formatSseEvent` / `createSseResponse` / `createSseError` | +| 场景 | 规则 | +| ----------------------- | ---------------------------------------------------------------------------------------------- | +| session.ts 单一入口 | shared/lib/\* 不再静态 import @/auth | +| dynamic import 打破循环 | `session.ts` 内部 `dynamic import("@/auth")` | +| 重复 helper 提取 | `serializeDate` / `downloadBase64File` / `getInitials` / `isRecord` / `extractQuestionPreview` | +| ROLE_NAMES 常量 | 替代 DB 查询中硬编码角色名 | +| api-response 工具 | `apiSuccess` / `apiError` / `apiFromAction` / `withApiErrorHandler` | +| sse 工具 | `formatSseEvent` / `createSseResponse` / `createSseError` | --- @@ -269,75 +269,78 @@ ### 2026-07-07 -| 时间 | 模块 | 经验 | -|------|------|------| -| 21:30 | 全局 | 004 架构文档 V3 重构:从"影响地图"升级为"架构设计文档",14 章节 912 行。核心 mermaid 图:系统边界图 + 三层架构图 + 模块依赖全景图(35 模块分层)+ 认证 sequenceDiagram + 权限系统架构图 + 数据访问分层图 + 5 层状态模型图 + 跨模块协作 sequenceDiagram + 考试全生命周期 sequenceDiagram(跨 4 模块)+ 令牌分层架构图 + API 路由分类图 + 错误→状态码映射图 + userId/permission 影响链。模块细节移至各模块 README,004 聚焦架构层面:技术栈选型/分层规则/依赖深度统计/循环依赖处理/权限 3 道防线/缓存策略/状态层选用规则/聚合层模式/事件驱动闭环/硬性约束/性能原则/安全原则/架构决策记录 | -| 20:56 | 全模块 | 35 个模块 README 重写为完整架构文档:每个含 mermaid 架构图(graph TB 分层)+ 2-3 个 流程图(sequenceDiagram/flowchart)+ 技术栈表格 + 文件结构(行数来自 arch.db)+ 依赖关系双向表 + 关键约束 + 架构决策表。标杆 textbooks/README.md 251 行。arch:query 提供真实数据(模块统计/文件清单/依赖关系/技术标签)。并行子代理执行时 arch:scan 可能因竞争报 FOREIGN KEY 错误,串行执行正常 | -| 12:56 | onboarding/parent/messaging | data-access 审计 Phase 0 P0 修复完成(6 项):onboarding 加 server-only + DB 操作下沉;parent 新建 actions.ts(6 个 Server Action);messaging data-access.ts(1089 行)拆分为 5 子文件 + barrel | -| 12:27 | 全局 | data-access 审计 v1 完成:86 文件 ~30 actions,5 维度 4 严重级别,生成 230 issues(17 P0/48 P1/105 P2/60 P3),5 子代理并行审查 | -| 10:45 | 全局 | cacheFn 迁移审查完成:302 cacheFn 块全部正确放置(Raw + Wrapper 配对),9 处 POS-ERR 为 JSDoc/`};` 结尾的误报 | -| 09:49 | 全局 | V5 状态管理重构 Phase 4-5 完成:5 层状态模型(URL/Server/Client/UI/Form)+ notify 替换规则 + ModalRoot 命令式 registry | -| 08:13 | lesson-preparation/messaging | `as` 断言违规修复:11 个 `isXxxBlockData` 类型守卫 + `BLOCK_DATA_GUARDS` registry,替换 14 处 `as` 断言 | +| 时间 | 模块 | 经验 | +| ----- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 22:30 | 全局 | 企业级架构规范化 P2 完成:CI 新增 Unit test 阶段;创建 /api/health 端点 + Dockerfile HEALTHCHECK;安装 @next/bundle-analyzer(`npm run analyze` 触发);noUncheckedIndexedAccess 评估 419 错误,推迟到独立迭代(记入 tech-debt TD-P1-001) | +| 22:10 | 全局 | 企业级架构规范化 P1 完成:创建 LICENSE(专有协议)/CHANGELOG/CONTRIBUTING/SECURITY 大仓文档;安装 husky + lint-staged + commitlint,pre-commit 运行 eslint+prettier,commit-msg 校验 conventional commits 格式;填充 tech-debt.md(P0/P1/P2 分级 + 已解决项) | +| 21:50 | 全局 | 企业级架构规范化 P0 完成:arch:scan @public JSDoc 标记豁免机制(12 个登录前/公开/内部工具 Action);递归 CTE 识别间接调用链(requireAiPermission 等辅助函数);修复 9 个真违规(parent 5 个 requireAuth→requirePermission + settings 1 个显式校验 + 3 个 @public 豁免);拆分 5 个超长文件(schema.ts 2245→29+27子文件、invalidation-map.ts 1195→50+6子文件、messaging/actions.ts 973→47+5子文件、textbooks/data-access.ts 907→15+6子文件、questions/data-access.ts 828→48+4子文件);违规数 32→0 | +| 21:30 | 全局 | 004 架构文档 V3 重构:从"影响地图"升级为"架构设计文档",14 章节 912 行。核心 mermaid 图:系统边界图 + 三层架构图 + 模块依赖全景图(35 模块分层)+ 认证 sequenceDiagram + 权限系统架构图 + 数据访问分层图 + 5 层状态模型图 + 跨模块协作 sequenceDiagram + 考试全生命周期 sequenceDiagram(跨 4 模块)+ 令牌分层架构图 + API 路由分类图 + 错误→状态码映射图 + userId/permission 影响链。模块细节移至各模块 README,004 聚焦架构层面:技术栈选型/分层规则/依赖深度统计/循环依赖处理/权限 3 道防线/缓存策略/状态层选用规则/聚合层模式/事件驱动闭环/硬性约束/性能原则/安全原则/架构决策记录 | +| 20:56 | 全模块 | 35 个模块 README 重写为完整架构文档:每个含 mermaid 架构图(graph TB 分层)+ 2-3 个 流程图(sequenceDiagram/flowchart)+ 技术栈表格 + 文件结构(行数来自 arch.db)+ 依赖关系双向表 + 关键约束 + 架构决策表。标杆 textbooks/README.md 251 行。arch:query 提供真实数据(模块统计/文件清单/依赖关系/技术标签)。并行子代理执行时 arch:scan 可能因竞争报 FOREIGN KEY 错误,串行执行正常 | +| 12:56 | onboarding/parent/messaging | data-access 审计 Phase 0 P0 修复完成(6 项):onboarding 加 server-only + DB 操作下沉;parent 新建 actions.ts(6 个 Server Action);messaging data-access.ts(1089 行)拆分为 5 子文件 + barrel | +| 12:27 | 全局 | data-access 审计 v1 完成:86 文件 ~30 actions,5 维度 4 严重级别,生成 230 issues(17 P0/48 P1/105 P2/60 P3),5 子代理并行审查 | +| 10:45 | 全局 | cacheFn 迁移审查完成:302 cacheFn 块全部正确放置(Raw + Wrapper 配对),9 处 POS-ERR 为 JSDoc/`};` 结尾的误报 | +| 09:49 | 全局 | V5 状态管理重构 Phase 4-5 完成:5 层状态模型(URL/Server/Client/UI/Form)+ notify 替换规则 + ModalRoot 命令式 registry | +| 08:13 | lesson-preparation/messaging | `as` 断言违规修复:11 个 `isXxxBlockData` 类型守卫 + `BLOCK_DATA_GUARDS` registry,替换 14 处 `as` 断言 | ### 2026-07-06 -| 时间 | 模块 | 经验 | -|------|------|------| +| 时间 | 模块 | 经验 | +| ----- | ---- | ------------------------------------------------------------------------------------------------- | | 23:50 | 全局 | 文档体系重设计计划制定:arch.db 扫描器 + 004 瘦身 + 005 废弃 + known-issues 精简 + 35 模块 README | ### 2026-07-05 -| 时间 | 模块 | 经验 | -|------|------|------| -| 全天 | 全局 | V5 状态管理重构 Phase 1-3 完成:notify 替换为 zustand + ModalRoot;useDialogState 评估结论(不采用) | -| 全天 | 全局 | 性能预算重构:Phase 1 配置基线 + Phase 2 Bundle 预算 + Phase 3 数据层优化 + Phase 4 组件渲染优化 | -| 全天 | questions/search | FULLTEXT 索引 + MATCH AGAINST BOOLEAN MODE 替代 LIKE '%xxx%' | -| 全天 | notifications | fan-out 批量化:`getAllUserIds` 分页 + `createNotifications` 批量 INSERT | -| 全天 | 全局 | 高 LIMIT 调低:attendance 5000→100、adaptive-practice 1000→100、questions 1000→100 | -| 全天 | messaging/grades/attendance | useOptimistic + useTransition 乐观更新改造 | +| 时间 | 模块 | 经验 | +| ---- | --------------------------- | ---------------------------------------------------------------------------------------------------- | +| 全天 | 全局 | V5 状态管理重构 Phase 1-3 完成:notify 替换为 zustand + ModalRoot;useDialogState 评估结论(不采用) | +| 全天 | 全局 | 性能预算重构:Phase 1 配置基线 + Phase 2 Bundle 预算 + Phase 3 数据层优化 + Phase 4 组件渲染优化 | +| 全天 | questions/search | FULLTEXT 索引 + MATCH AGAINST BOOLEAN MODE 替代 LIKE '%xxx%' | +| 全天 | notifications | fan-out 批量化:`getAllUserIds` 分页 + `createNotifications` 批量 INSERT | +| 全天 | 全局 | 高 LIMIT 调低:attendance 5000→100、adaptive-practice 1000→100、questions 1000→100 | +| 全天 | messaging/grades/attendance | useOptimistic + useTransition 乐观更新改造 | ### 2026-07-04 -| 时间 | 模块 | 经验 | -|------|------|------| -| 全天 | 全局 | API 规范化重构:响应信封 + 错误→状态码映射 + SSE 工具 + withApiErrorHandler HOF | -| 全天 | 全局 | 设计令牌专项重构:双层令牌架构(Primitive + Semantic)+ ESLint 强制约束 | +| 时间 | 模块 | 经验 | +| ---- | ---- | -------------------------------------------------------------------------------------------- | +| 全天 | 全局 | API 规范化重构:响应信封 + 错误→状态码映射 + SSE 工具 + withApiErrorHandler HOF | +| 全天 | 全局 | 设计令牌专项重构:双层令牌架构(Primitive + Semantic)+ ESLint 强制约束 | | 全天 | 全局 | PWA 支持:manifest.ts + viewport + 触控友好(panOnDrag/zoomOnPinch/zoomOnDoubleClick=false) | ### 2026-07-02 -| 时间 | 模块 | 经验 | -|------|------|------| +| 时间 | 模块 | 经验 | +| ---- | ------------------ | ------------------------------------------------------------------------------------- | | 全天 | lesson-preparation | V5 备课模块场景缺口修复:Zustand slice + history + 自动保存 + Tiptap Image + 打印视图 | ### 2026-06-26 -| 时间 | 模块 | 经验 | -|------|------|------| +| 时间 | 模块 | 经验 | +| ---- | ------- | ------------------------------------------------------------------------------------------ | | 全天 | student | 学生端路由权限校验修复:`homework:submit` → `CLASS_READ`;新增 `loading.tsx` + `error.tsx` | -| 全天 | classes | teacher/classes/* 路由权限校验修复:统一 `CLASS_READ` | +| 全天 | classes | teacher/classes/\* 路由权限校验修复:统一 `CLASS_READ` | ### 2026-06-24 -| 时间 | 模块 | 经验 | -|------|------|------| +| 时间 | 模块 | 经验 | +| ---- | ------ | ------------------------------------------------------------------------ | | 全天 | grades | 成绩录入支持按试卷录入:`getExamsForGradeEntry` / `getExamForGradeEntry` | | 全天 | shared | `ROLE_NAMES` 常量替代 DB 硬编码角色名;`extractQuestionPreview` 共享工具 | ### 2026-06-23 -| 时间 | 模块 | 经验 | -|------|------|------| +| 时间 | 模块 | 经验 | +| ---- | ------ | ------------------------------------------------------------------------------------------------------------------- | | 全天 | shared | Action 错误处理统一:`handleActionError` / `safeActionCall` / `BusinessError` / `NotFoundError` / `ValidationError` | ### 2026-06-22 -| 时间 | 模块 | 经验 | -|------|------|------| +| 时间 | 模块 | 经验 | +| ---- | ------- | -------------------------------- | | 全天 | classes | teacher/classes 路由权限审计修复 | ### 2026-06-17 -| 时间 | 模块 | 经验 | -|------|------|------| +| 时间 | 模块 | 经验 | +| ---- | ---- | ------------------------------------------------------------------ | | 全天 | 全局 | 004 架构影响地图首次编写(4227 行,后于 2026-07-07 瘦身至 458 行) | diff --git a/scripts/arch-scan/arch.db b/scripts/arch-scan/arch.db index 6a4972622a41d195f13d90ac54bde4b90802689e..bfaf4aa185db79ea728cd67bd7019629adca54a4 100644 GIT binary patch delta 13406 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 delta 10150 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 Y|HFMRs(!(cmfXlHbNKV~1zn~80sMG(X8-^I