20 KiB
20 KiB
已知问题速查
索引式速查手册:场景 → 技术/规则映射 架构规则见 ../architecture/004_architecture_impact_map.md 与 .trae/rules/project_rules.md 原始版本归档于 ../architecture/audit/archive/known-issues_v1.md
一、全局经验
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 本身而非抛错 |
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 | 包裹 <Suspense> 或在 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 一致性 | 初始值必须可序列化且在服务端和客户端一致 |
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* 格式 |
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 自动管理乐观状态与回滚 |
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 |
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 -- <reason> |
| 白名单文件 | 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<T> |
| 仅类型导入 | import type |
可选链后禁止 ! |
a?.b! 禁止 |
非空断言 ! |
数组取值后判空再访问;split 结果用 ?? 兜底 |
| 空对象赋值 | 显式标注 const empty: Record<string, string> = {} |
1.9 API 路由
| 场景 | 规则 |
|---|---|
| 响应信封 | { success, message?, errorCode?, data? } 与 ActionState<T> 对齐 |
| 错误→状态码 | 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 读取 |
1.11 测试
| 场景 | 规则 |
|---|---|
| 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 |
检查 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 -- <command> |
| ESLint 零警告 | 所有修改文件必须 0 errors 0 warnings |
| 自定义规则加载 | Windows 路径用 path.join,不用 path.posix.join |
二、模块经验
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 细线 |
2.2 exams / homework / grades
| 场景 | 规则 |
|---|---|
| AI 出题 | exams/ai-pipeline.ts + createAiChatCompletion + JSON 解析 + Zod 校验 |
| 作业化考试 | sourceExamId 外键关联;homeworkSubmissions + homeworkAnswers 表 |
| 批改事务 | db.transaction 包裹多步写 |
| 成绩批量更新 | useOptimistic<GradeRecordListItem[], GradeRecordListItem> |
| FULLTEXT 检索 | questions 表 content_text STORED 生成列 + MATCH AGAINST BOOLEAN MODE |
2.3 messaging / notifications
| 场景 | 规则 |
|---|---|
| 消息星标乐观更新 | useOptimistic<boolean> + useTransition + addOptimisticStarred |
| 消息列表星标 Map | useOptimistic<Map<string, boolean>>(key=messageId) |
| 撤回不乐观更新 | 需服务端返回判断 2 分钟窗口 |
| 通知批量化 | createNotifications(items) 单次 INSERT;in_app 批量 + 其他渠道并行 |
| 通知偏好 | 已从 messaging 迁移至 notifications |
| fan-out 分页 | getAllUserIds(limit=1000, offset) 分页遍历 |
2.4 attendance
| 场景 | 规则 |
|---|---|
| 考勤提交乐观更新 | useOptimistic<boolean> 保留 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<Map<string,string>> |
| 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 权限点 |
2.7 parent
| 场景 | 规则 |
|---|---|
| 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 |
三、工作经验日志
按时间倒序,50 条上限。AI 发现更好方案时可更新本节。
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 断言 |
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 乐观更新改造 |
2026-07-04
| 时间 | 模块 | 经验 |
|---|---|---|
| 全天 | 全局 | 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 |
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 行) |