docs(cache): 同步 cacheFn 架构图与已知问题速查
This commit is contained in:
@@ -135,6 +135,157 @@ src/app/
|
||||
|
||||
---
|
||||
|
||||
## 1.1.3 Phase 4.2 + Phase 4.5 状态管理优化(2026-07-05 新增)
|
||||
|
||||
V5 状态管理统一的后续阶段,针对两类高频性能问题进行细粒度优化:**Zustand 整体订阅导致的级联 re-render** 与 **useState + 等待 Server Action 模式导致的 UI 不即时反馈**。
|
||||
|
||||
### Phase 4.2:lesson-plan-editor Zustand selector 拆分
|
||||
|
||||
`useLessonPlanEditor` 是组合了 5 个 slice 的 zustand store。原消费方使用 `const editor = useLessonPlanEditor()` 整体订阅,导致任一字段变化都触发整体 re-render。
|
||||
|
||||
| 文件 | 修改要点 |
|
||||
|------|---------|
|
||||
| `modules/lesson-preparation/components/lesson-plan-editor.tsx` | 拆分为 9 个细粒度 selector(`isDirty`/`isOnline`/`doc`/`title`/`isSaving`/`saveError`/`lastSavedAt`/`setTitle` + `canUndo`/`canRedo` 派生 boolean),移除 `editor.canUndo()` 重复调用,更新 `useEffect` 依赖与 `printablePlan` useMemo 使用拆分后的变量 |
|
||||
| `modules/lesson-preparation/components/blocks/text-study-block.tsx` | `const { updateNode } = useLessonPlanEditor()` → `useLessonPlanEditor((s) => s.updateNode)` |
|
||||
| `modules/lesson-preparation/components/blocks/exercise-block.tsx` | 拆为 `useLessonPlanEditor((s) => s.updateNode)` + `useLessonPlanEditor((s) => s.planId)` |
|
||||
| `modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx` | 拆为 7 个细粒度 selector(`toggleExpand`/`expandedNodeIds`/`updateNode`/`removeNode`/`addAnchor`/`duplicateNode`/`nodes`),特别地将 `doc` 改为 `s.doc.nodes` 更精细订阅(`addAnchor` 等仅修改 anchors 不会触发本组件 re-render) |
|
||||
|
||||
**关键决策**:
|
||||
- `canUndo`/`canRedo` 改为 `useLessonPlanEditor((s) => s.canUndo())` 订阅派生 boolean,避免每次渲染时调用函数
|
||||
- 不使用 `useShallow` 包装多字段 selector,因为单字段 selector 已经够细,且避免了 `useShallow` 自身的浅比较开销
|
||||
- 函数引用(`setTitle`/`updateNode` 等)天然稳定,无需 `useShallow`
|
||||
|
||||
### Phase 4.5:useOptimistic + useTransition 乐观更新
|
||||
|
||||
将 4 个组件从 `useState` + 等待 Server Action 模式迁移到 React 19 `useOptimistic` + `useTransition`,自动管理乐观状态与回滚。
|
||||
|
||||
| 文件 | 修改要点 |
|
||||
|------|---------|
|
||||
| `modules/messaging/components/message-detail.tsx` | 星标 `useState` → `useOptimistic<boolean>` + `useTransition`。`handleToggleStar` 在 `startStarTransition` 内 `addOptimisticStarred(nextStarred)` + `await toggleMessageStarAction` + `router.refresh()`。JSX 中所有 `isStarred` 引用改为 `optimisticIsStarred`(Button variant/aria-pressed/Star className/星标徽章) |
|
||||
| `modules/messaging/components/message-list.tsx` | 星标 `starredOverride` useState → `useOptimistic<Map<string, boolean>>`(key=messageId, value=isStarred)。`getIsStarred` 改为 `optimisticStarredMap.get(m.id)`。撤回 `recalledOverride` 仍保留 useState(不需要乐观更新) |
|
||||
| `modules/grades/components/grade-record-list.tsx` | 编辑保存 `isSaving` useState → `useOptimistic<GradeRecordListItem[], GradeRecordListItem>` + `useTransition`。`handleEditSave` 构造 `optimisticRecord` 在 `startSaveTransition` 内 `addOptimisticRecord` + `await updateGradeRecordAction` + `router.refresh()`。两处 `records.map` 改为 `optimisticRecords.map`(桌面表格 + 移动端卡片视图)即时显示乐观记录 |
|
||||
| `modules/attendance/components/attendance-sheet.tsx` | `isSubmitting` useState → `useOptimistic<boolean>`。保留 `<form action={handleSubmit}>` 模式 + `useFormStatus` SubmitButton(progressive enhancement)。`setOptimisticSubmitting(true)` 在 form action 内调用,action 完成后 `optimisticSubmitting` 自动恢复 false,移除 `finally` 块 |
|
||||
|
||||
**关键决策**:
|
||||
- attendance-sheet 保留 form action 模式而非改用 `useTransition` 包裹,因为 `useFormStatus().pending` 依赖 form action 的 promise 状态,若用 `startTransition` 包裹会导致 form action 立即返回、`useFormStatus` 不可靠
|
||||
- `useOptimistic` 的 `addOptimistic` 必须在 transition 或 action 内调用(form action 也算 action)
|
||||
- 成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值
|
||||
- 不为撤回(recall)操作使用 useOptimistic,因为撤回需要根据服务端返回判断是否成功(2 分钟窗口校验),失败时不应乐观显示已撤回
|
||||
|
||||
### 验证结果
|
||||
|
||||
- `npx tsc --noEmit`:零新增错误(pre-existing 错误:`providers.tsx` 缺少 `@tanstack/react-query-devtools`、`web-vitals-reporter.tsx` Metric 导出、`homework/data-access-write.ts` MySqlTransaction.run、`paper-context-menu.tsx` 第 58 行 `duplicateNode` 调用签名不匹配——此为 pre-existing bug,`duplicateNode(id: string)` 只接受 1 参数但调用方传 2 个、`print-view.tsx` Object.entries 类型推断)
|
||||
- `npm run lint`:零新增错误(8 个 modified 文件均未出现在 lint 输出中,pre-existing 3 errors + 13 warnings 均位于未修改文件:`scripts/check-db-state.mjs`、`scripts/seed-grade5-chinese.ts`、`practice-result-view.tsx`、`exam-rich-form.tsx`、`use-exam-preview-state.ts`、`use-exam-preview-tasks.ts`、`homework/data-access-write.ts`、`unread-message-badge.tsx`)
|
||||
|
||||
---
|
||||
|
||||
## 1.1.4 Phase 3.6 + Phase 3.7 + Phase 3.8 DB 性能优化(2026-07-05 新增)
|
||||
|
||||
针对数据库查询性能的三阶段优化:**FULLTEXT 全文检索**、**公告 fan-out 批量化**、**高 LIMIT 默认值调低**。
|
||||
|
||||
### Phase 3.6:searchQuestions 改 FULLTEXT 索引
|
||||
|
||||
将 questions 表的模糊查询从 `LIKE '%xxx%'` 升级为 MySQL FULLTEXT 索引 + `MATCH AGAINST ... IN BOOLEAN MODE`,提升大表检索性能。
|
||||
|
||||
| 文件 | 修改要点 |
|
||||
|------|---------|
|
||||
| `src/shared/db/schema.ts` | 添加 `import { sql } from "drizzle-orm"`;在 `questions` 表添加 STORED 生成列 `contentText`(`text("content_text").generatedAlwaysAs(sql\`CAST(content AS CHAR)\`, { mode: "stored" })`);补齐 5 个先前未同步的索引(typeDifficultyIdx/statusCreatedIdx/creatorIdx/examStatusIdx/submittedAtIdx) |
|
||||
| `drizzle/0001_questions_fulltext_search.sql` | drizzle-kit 生成的迁移:CREATE TABLE lesson_plan_schedules + content_text 生成列 + 8 个索引;末尾手动追加 `CREATE FULLTEXT INDEX questions_content_text_ft_idx ON questions (content_text)` |
|
||||
| `drizzle/meta/0001_snapshot.json` | questions 表 indexes 节点添加 `questions_content_text_ft_idx` 声明(type: "fulltext") |
|
||||
| `src/modules/search/data-access.ts` | `searchQuestions(kw, limit?)` → `searchQuestions(q, limit?)`;新增 `toBooleanModeQuery` 工具函数将原始查询转为 `+word1* +word2*` 格式;查询改用 `MATCH(questions.contentText) AGAINST(? IN BOOLEAN MODE)` |
|
||||
| `src/app/api/search/route.ts` | 本地 `searchQuestions` 函数同步改用 MATCH AGAINST + `toBooleanModeQuery`;调用点从 `searchQuestions(kw, pageSize)` 改为 `searchQuestions(q, pageSize)` |
|
||||
|
||||
**关键决策**:
|
||||
- 使用 STORED 生成列 `content_text` 而非 VIRTUAL,因为 FULLTEXT 索引在 MySQL 5.7+ 仅支持 STORED 生成列
|
||||
- `CAST(content AS CHAR)` 将 JSON 字段序列化为纯文本,避免 MySQL FULLTEXT 无法直接索引 JSON 列的限制
|
||||
- drizzle-kit 无法生成 FULLTEXT 索引声明,迁移文件中需手动追加 `CREATE FULLTEXT INDEX` 语句,并同步更新 snapshot 的 indexes 节点
|
||||
- `toBooleanModeQuery` 转义 BOOLEAN MODE 特殊字符(+ - < > ( ) ~ * " ')避免用户输入破坏查询语法
|
||||
- 保留 textbooks/exams/announcements 的 LIKE 模糊匹配(FULLTEXT 索引需 InnoDB + utf8mb4,且这些表查询量较小)
|
||||
|
||||
### Phase 3.7:getAllUserIds 加 LIMIT + 公告 fan-out 批量化
|
||||
|
||||
| 文件 | 修改要点 |
|
||||
|------|---------|
|
||||
| `src/modules/users/data-access.ts` | `getAllUserIds()` → `getAllUserIds(limit=1000, offset=0)`,添加默认 LIMIT 防止 OOM + 分页参数支持 |
|
||||
| `src/modules/announcements/data-access.ts` | `resolveAnnouncementTargetUserIds` school 分支改为分页循环遍历 `getAllUserIds(PAGE_SIZE, offset)`(PAGE_SIZE=1000),聚合所有用户 ID |
|
||||
| `src/modules/notifications/data-access.ts` | 新增 `createNotifications(items)` 批量 INSERT 函数(单次 `db.insert().values([...])`),返回生成的 ID 数组 |
|
||||
| `src/modules/notifications/channels/in-app-channel.ts` | `InAppChannelSender.sendBatch` 从 `Promise.all(items.map(send))` 改为调用 `createNotifications(inputs)` 单次批量 INSERT;预校验 recipient/userId 一致性;失败时整体回滚并返回失败结果 |
|
||||
| `src/modules/notifications/dispatcher.ts` | `sendBatchNotifications` 重构:并行获取所有用户偏好+联系方式 → 收集 in_app 渠道 payload 单次 `sendBatch` → 其他渠道保持 per-payload 并行 → 按 payload 聚合日志 |
|
||||
| `src/modules/notifications/index.ts` | 导出 `createNotifications` |
|
||||
|
||||
**关键决策**:
|
||||
- `getAllUserIds` 默认 LIMIT 1000 是平衡单次查询压力与 fan-out 完整性的折中值;调用方通过分页遍历获取全部用户
|
||||
- `createNotifications` 使用 drizzle 的 `db.insert().values(array)` 一次 INSERT 多行,相比 N 次单行 INSERT 显著降低网络往返
|
||||
- in_app 渠道批量化,其他渠道(sms/email/wechat)保持原并行模式,因为这些渠道的发送成本主要在网络 IO 而非 DB 写入
|
||||
- 批量 INSERT 失败时整体回滚(drizzle 默认事务行为),每条记录返回失败结果
|
||||
|
||||
### Phase 3.8:调低高 LIMIT 默认值
|
||||
|
||||
在 `src/modules/*/data-access*.ts` 中搜索 LIMIT > 100 的默认值并调低至 ≤100(保留分页场景)。
|
||||
|
||||
| 文件 | 原 LIMIT | 新 LIMIT | 函数 |
|
||||
|------|---------|---------|------|
|
||||
| `src/modules/attendance/data-access-correlation.ts` | 5000 | 100 | `getAttendanceGradeCorrelation` 调用 `getGradeRecords` |
|
||||
| `src/modules/adaptive-practice/data-access-strategy.ts` | 1000 | 100 | `getStudentAnsweredQuestionIds` |
|
||||
| `src/modules/questions/data-access.ts` | 1000 | 100 | `exportQuestions` |
|
||||
|
||||
**未修改(不在 data-access*.ts 范围内)**:
|
||||
- `src/modules/homework/stats-service.ts:276`(limit: 200)和 `:345`(limit: 5000)不在 `data-access*.ts` glob 范围内,本次未修改
|
||||
|
||||
### 验证结果
|
||||
|
||||
- `npx tsc --noEmit`:零新增错误(pre-existing 错误:`providers.tsx`、`web-vitals-reporter.tsx`、`homework/data-access-write.ts`、`paper-context-menu.tsx`、`print-view.tsx` 均为未修改文件)
|
||||
- `npm run lint`:零新增错误(16 个 pre-existing problems 均位于未修改文件:`scripts/check-db-state.mjs`、`scripts/seed-grade5-chinese.ts`、`practice-result-view.tsx`、`exam-rich-form.tsx`、`use-exam-preview-state.ts`、`use-exam-preview-tasks.ts`、`homework/data-access-write.ts`、`unread-message-badge.tsx`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 1.1.5 性能预算重构专项(2026-07-05 新增)
|
||||
|
||||
> 完整审计报告见 `docs/architecture/audit/performance-budget-audit-report.md`。
|
||||
> 本章节记录本次重构对架构产生的关键变更,以便后续维护时定位。
|
||||
|
||||
### 影响范围概览
|
||||
|
||||
| Phase | 改造内容 | 关键文件 |
|
||||
|-------|---------|---------|
|
||||
| 2.1 | tiptap 动态导入(3 处) | `shared/components/ui/rich-text-editor.tsx` + `modules/exams/editor/exam-rich-editor.tsx` + `modules/lesson-preparation/components/blocks/rich-text-block.tsx` |
|
||||
| 2.2 | ReactFlow 动态导入 | `modules/textbooks/components/knowledge-graph.tsx` 拆为 `knowledge-graph-inner.tsx` + lazy wrapper,ReactFlowProvider 留外层 |
|
||||
| 2.3 | exams/editor barrel 拆分 | 新增 `modules/exams/editor/types.ts` 作为纯类型入口,供 server-only 消费方引用 |
|
||||
| 2.4 | chart.tsx barrel + status-badge 移除 use client | chart.tsx 已是具名导出;`status-badge.tsx` 移除 `"use client"`(纯展示组件) |
|
||||
| 2.5 | 4 个角色根路由 loading.tsx | 新增 `teacher/loading.tsx` + `student/loading.tsx` + `parent/loading.tsx`(admin 已有) |
|
||||
| 3.1 | unstable_cache 评估 | 评估后决定不引入,与项目现有 React `cache()` 模式一致 |
|
||||
| 3.2 | auth 函数 cache 包装 | `shared/lib/session.ts:getSession` + `shared/lib/auth-guard.ts:resolveDataScope` + `getAuthContext` + `modules/classes/data-access.ts:getSessionTeacherId` 全部用 React `cache()` 包装,`getSessionTeacherId` 改走 `getAuthContext()` |
|
||||
| 3.3 | copyCoursePlanToClasses 批量化 | `modules/course-plans/data-access.ts:347` 改为单事务 + 2 次批量 INSERT,O(N*M) → O(1) |
|
||||
| 3.4 | gradeHomeworkAnswers 批量 UPDATE | `modules/homework/data-access-write.ts:356` 改为单次 `UPDATE ... CASE WHEN` + `sql.join`,替代 N 次串行 UPDATE |
|
||||
| 3.5 | exams 表索引补齐 | `shared/db/schema.ts` 新增 5 个索引,迁移 `drizzle/0001_questions_fulltext_search.sql` |
|
||||
| 3.6 | searchQuestions FULLTEXT | `shared/db/schema.ts` 新增 `contentText` STORED 生成列 + `questions_content_text_ft_idx` FULLTEXT 索引;`search/data-access.ts` 改用 `MATCH(...) AGAINST(... IN BOOLEAN MODE)` |
|
||||
| 3.7 | 公告 fan-out 批量化 | `getAllUserIds` 加分页参数;新增 `notifications/data-access.ts:createNotifications` 批量 INSERT;`dispatcher.sendBatchNotifications` 重构为 in_app 单次批量 + 其他渠道并行 |
|
||||
| 3.8 | 高 LIMIT 调低 | attendance 5000→100、adaptive-practice 1000→100、questions 1000→100 |
|
||||
| 4.1 | SidebarProvider memoize | `modules/layout/components/sidebar-provider.tsx` resize debounce 200ms + useMemo context + useCallback toggleSidebar |
|
||||
| 4.2 | lesson-plan-editor selector 拆分 | `lesson-plan-editor.tsx` + 3 个子组件改为单字段 selector |
|
||||
| 4.3 | 题库表 + 考试表虚拟化 | 新增 `@tanstack/react-virtual` 依赖;`question-data-table.tsx` + `exam-data-table.tsx` 引入 `useVirtualizer` |
|
||||
| 4.4 | recharts props 模块级常量 | 14 个 chart 文件提取 `CHART_MARGIN`/`AXIS_TICK`/`TOOLTIP_CURSOR` 等模块级常量 |
|
||||
| 4.5 | useOptimistic 改造 | `message-detail.tsx` + `message-list.tsx` + `grade-record-list.tsx` + `attendance-sheet.tsx` 改用 `useOptimistic` + `useTransition` |
|
||||
| 4.6 | 高频子组件 React.memo | `AssignmentCard` + `StatusBadge` + `EmptyState` 添加 React.memo(ClassCard/GraphKpNode 已有) |
|
||||
| 4.7 | AiAssistantWidget 懒加载 | 新增 `modules/ai/components/ai-assistant-widget-inner.tsx`,外层改为 lazy wrapper |
|
||||
| 4.8 | scan-image-viewer aspect-ratio | `modules/homework/components/scan-image-viewer.tsx` `<img>` 加 `aspectRatio: "4 / 3"` + width/height 属性 |
|
||||
| 4.9 | metadata 增强 | `app/layout.tsx` 补 `metadataBase`/`openGraph`/`robots`/`applicationName`/`authors`/`creator` |
|
||||
|
||||
### 关键架构决策
|
||||
|
||||
1. **动态导入模式标准化**:重 UI 库(tiptap / @xyflow/react / AI SDK)统一采用 `xxx-inner.tsx` + `xxx.tsx` lazy wrapper 模式。ReactFlowProvider 等 context provider 留在外层同步渲染,保证 lazy 子组件的 `useReactFlow()` 等 hook 可用。
|
||||
2. **请求级去重优先于跨请求缓存**:auth 相关 4 个核心函数(getSession/resolveDataScope/getAuthContext/getSessionTeacherId)全部用 React `cache()` 包装。不引入 `unstable_cache`,因权限数据易变,跨请求缓存风险高于收益。
|
||||
3. **批量 SQL 优于循环 SQL**:
|
||||
- INSERT 场景:用 `db.insert().values([...])` 批量
|
||||
- UPDATE 场景:用 `sql.join` + `CASE WHEN` 单次执行
|
||||
- 复杂事务:用 `db.transaction` 包裹多个批量操作
|
||||
4. **React 19 useOptimistic 替代手动 isPending**:消息星标/出勤/成绩录入统一改为 `useOptimistic` + `useTransition`,移除手动 `useState<boolean>` 跟踪 pending。
|
||||
5. **细粒度 Zustand selector**:单字段 selector 优先于 `useShallow` 多字段 selector,因为函数引用天然稳定。
|
||||
|
||||
---
|
||||
|
||||
## 1.2 模块依赖关系图
|
||||
|
||||
下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。
|
||||
@@ -842,7 +993,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- Data-access:`getQuestions`(✅ 审计升级:支持 `textbookId`/`chapterId` 级联筛选参数,通过 textbooks data-access 获取知识点 ID 集合)/ `createQuestionWithRelations` / `updateQuestionById` / `deleteQuestionByIdRecursive` / `getKnowledgePointOptions` / `getTextbookOptions`(✅ 审计新增)/ `getChapterOptions(textbookId)`(✅ 审计新增)/ `getKnowledgePointOptionsByChapter(chapterId)`(✅ 审计新增)/ `deleteQuestionsBatch`(✅ 审计新增:批量删除,事务原子性)/ `exportQuestions`(✅ 审计新增:导出,权限范围过滤)/ `importQuestions`(✅ 审计新增:批量导入,事务原子性)
|
||||
- Types:`Question` / `QuestionWithRelations` / `GetQuestionsParams`(✅ 审计升级:新增 `textbookId`/`chapterId` 字段)/ `CreateQuestionInput` / `KnowledgePointOption` / `TextbookOption`(✅ 审计新增)/ `ChapterOption`(✅ 审计新增)/ `QuestionContent`(✅ 审计新增:类型守卫接口)/ `QuestionOption`(✅ 审计新增)/ `QUESTION_TYPE_I18N_KEY`(✅ 审计新增:题型 i18n 键映射)/ `DIFFICULTY_I18N_KEY`(✅ 审计新增:难度 i18n 键映射)
|
||||
- Utils:`parseQuestionContent(raw)`(✅ 审计新增:类型守卫,替代 `as` 断言)/ `getQuestionPreview` / `getQuestionOptions` / `getQuestionText` / `trackQuestionEvent`(✅ 审计新增:埋点接口 no-op)/ `trackQuestionCreated` / `trackQuestionUpdated` / `trackQuestionDeleted` / `trackQuestionSearched`
|
||||
- Components:`QuestionFilters`(✅ 审计重写:组合 QuestionCascadeFilter)/ `QuestionCascadeFilter`(✅ 审计新增:三级级联筛选,nuqs 管理 URL 状态)/ `QuestionDataTable`(✅ 审计重写:i18n + 集成 BatchOperations)/ `useQuestionColumns`(✅ 审计改为 Hook:因 useTranslations 是 Hook)/ `CreateQuestionButton`(✅ 审计重写:权限感知)/ `QuestionActions`(✅ 审计重写:权限感知 + 类型守卫 + QuestionContentRenderer 结构化预览)/ `CreateQuestionDialog`(✅ 审计重写:拆分为 281 行)/ `KnowledgePointSelector`(✅ 审计新增:从 dialog 拆分)/ `OptionsEditor`(✅ 审计新增:从 dialog 拆分)/ `QuestionBankResultsClient`(✅ 审计新增:RSC 与客户端 Hook 桥接)/ `QuestionContentRenderer`(✅ 审计新增:题目结构化渲染)/ `BatchOperations`(✅ 审计新增:批量删除工具栏)/ `ImportExportButtons`(✅ 审计新增:JSON 导入导出)
|
||||
- Components:`QuestionFilters`(✅ 审计重写:组合 QuestionCascadeFilter)/ `QuestionCascadeFilter`(✅ 审计新增:三级级联筛选,nuqs 管理 URL 状态)/ `QuestionDataTable`(✅ 审计重写:i18n + 集成 BatchOperations;✅ Phase 4.3:引入 @tanstack/react-virtual 虚拟滚动替换分页)/ `useQuestionColumns`(✅ 审计改为 Hook:因 useTranslations 是 Hook)/ `CreateQuestionButton`(✅ 审计重写:权限感知)/ `QuestionActions`(✅ 审计重写:权限感知 + 类型守卫 + QuestionContentRenderer 结构化预览)/ `CreateQuestionDialog`(✅ 审计重写:拆分为 281 行)/ `KnowledgePointSelector`(✅ 审计新增:从 dialog 拆分)/ `OptionsEditor`(✅ 审计新增:从 dialog 拆分)/ `QuestionBankResultsClient`(✅ 审计新增:RSC 与客户端 Hook 桥接)/ `QuestionContentRenderer`(✅ 审计新增:题目结构化渲染)/ `BatchOperations`(✅ 审计新增:批量删除工具栏)/ `ImportExportButtons`(✅ 审计新增:JSON 导入导出)
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`textbooks`(✅ 审计升级:通过 textbooks data-access 的 `getTextbooks` / `getChaptersByTextbookId` / `getKnowledgePointsByChapterId` / `getKnowledgePointsByTextbookId` 实现级联筛选)
|
||||
@@ -877,7 +1028,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `utils/track-event.ts` | 30+ | 埋点接口(no-op 实现) |
|
||||
| `components/question-cascade-filter.tsx` | 130+ | 三级级联筛选(nuqs URL 状态) |
|
||||
| `components/question-filters.tsx` | 40+ | 筛选栏(组合 QuestionCascadeFilter) |
|
||||
| `components/question-data-table.tsx` | 155+ | 数据表格(i18n + 集成 BatchOperations) |
|
||||
| `components/question-data-table.tsx` | 180+ | 数据表格(i18n + 集成 BatchOperations + ✅ Phase 4.3:@tanstack/react-virtual 虚拟滚动,替换分页避免大数据渲染卡顿) |
|
||||
| `components/question-columns.tsx` | 100+ | 列定义 Hook(useQuestionColumns) |
|
||||
| `components/create-question-button.tsx` | 30+ | 创建按钮(权限感知) |
|
||||
| `components/question-actions.tsx` | 200+ | 行操作(权限感知 + 类型守卫 + QuestionContentRenderer) |
|
||||
@@ -1142,6 +1293,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `components/knowledge-point-mastery-chart.tsx` | 232 | **P3-3 新增(2026-06-26)**:知识点掌握度柱状图组件,集成 diagnostic 模块的 getClassMasterySummary 数据。使用 SimpleBarChart + masteryBarColor 函数着色 + isMasteryTooltipPayload 类型守卫 |
|
||||
| `hooks/use-draft-lock.ts` | 184 | **P3-6 新增(2026-06-26)**:协同录入锁管理 Hook。职责:进入页面自动获取锁、3 分钟心跳续期(TTL 5 分钟)、组件卸载/提交成功释放锁、锁冲突状态暴露。使用 ref 保存最新 token 避免闭包捕获旧值,防重复 acquire/release |
|
||||
|
||||
> 架构变更(2026-07-05,Phase 4.5 — grade-record-list.tsx 编辑保存改用 useOptimistic + useTransition):
|
||||
> - **grade-record-list.tsx**:编辑保存 `isSaving` useState → `useOptimistic<GradeRecordListItem[], GradeRecordListItem>` + `useTransition`。`handleEditSave` 构造 `optimisticRecord` 在 `startSaveTransition` 内 `addOptimisticRecord` + `await updateGradeRecordAction` + `router.refresh()`;两处 `records.map` 改为 `optimisticRecords.map`(桌面表格 + 移动端卡片视图)即时显示乐观记录
|
||||
> - **关键决策**:成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值;不为删除操作使用 useOptimistic,因为删除需要服务端确认(外键约束、关联记录检查等)
|
||||
|
||||
---
|
||||
|
||||
## 2.7 classes(班级模块)— 耦合最严重
|
||||
@@ -1441,6 +1596,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `data-access-correlation.ts` | ~130 | L-9 跨模块关联分析: 委托 classes/users/grades data-access |
|
||||
| `components/attendance-grade-correlation-card.tsx` | ~220 | L-9 散点图 + 风险表格 (recharts ScatterChart) |
|
||||
|
||||
> 架构变更(2026-07-05,Phase 4.5 — attendance-sheet.tsx 提交状态改用 useOptimistic):
|
||||
> - **attendance-sheet.tsx**:`isSubmitting` useState → `useOptimistic<boolean>`。保留 `<form action={handleSubmit}>` 模式 + `useFormStatus` SubmitButton(progressive enhancement);`setOptimisticSubmitting(true)` 在 form action 内调用,action 完成后 `optimisticSubmitting` 自动恢复 false,移除 `finally` 块
|
||||
> - **关键决策**:保留 form action 模式而非改用 `useTransition` 包裹,因为 `useFormStatus().pending` 依赖 form action 的 promise 状态,若用 `startTransition` 包裹会导致 form action 立即返回、`useFormStatus` 不可靠;`useOptimistic` 的 `addOptimistic` 必须在 transition 或 action 内调用(form action 也算 action)
|
||||
|
||||
---
|
||||
|
||||
## 2.11 users(用户模块)
|
||||
@@ -1449,7 +1608,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
**导出函数**:
|
||||
- Actions:`getUserProfileAction` / `updateUserProfileAction` / `importUsersAction` / `exportUsersAction` / `downloadUserTemplateAction` / `updateUserRoleAction` / `deleteUserAction`
|
||||
- Data-access:`getUserProfile` / `getCurrentStudentUser`(✅ P2-20 已修复:从 homework 模块迁移而来,6 个 student 页面通过此函数获取学生身份,不再依赖 homework 模块)/ `getAdminUsers`(管理员用户列表分页查询,支持搜索+角色聚合)/ `getAdminUserRoles`(角色名列表,用于筛选下拉框)
|
||||
- Data-access:`getUserProfile` / `getCurrentStudentUser`(✅ P2-20 已修复:从 homework 模块迁移而来,6 个 student 页面通过此函数获取学生身份,不再依赖 homework 模块)/ `getAdminUsers`(管理员用户列表分页查询,支持搜索+角色聚合)/ `getAdminUserRoles`(角色名列表,用于筛选下拉框)/ `getAllUserIds(limit=1000, offset=0)`(✅ P3-7:全校用户 ID 查询,默认 LIMIT 1000 防止 OOM + 分页参数支持)/ `getUserIdsByGradeId` / `getUserNamesByIds`
|
||||
- Import-export:`generateUserImportTemplate` / `parseUserImportData` / `exportUsersToExcel`(+ re-export `batchImportUsers` / `UserImportResult` 保持向后兼容)
|
||||
- User-service:`batchImportUsers`(用户创建 + 密码哈希 + 角色分配)
|
||||
- Class-registration:`registerStudentByInvitationCode`(委托 classes/data-access 完成班级注册)
|
||||
@@ -1663,6 +1822,11 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- `message-detail.tsx`:✅ 审计 V1-P1-3 客户端调用 `getMessageThreadAction(messageId)` 加载回复链
|
||||
- `message-compose.tsx`:✅ 审计 V1-P0-5 `submittedRef` 标志位阻止发送后自动保存 effect 触发新草稿
|
||||
|
||||
> 架构变更(2026-07-05,Phase 4.5 — 消息星标改用 useOptimistic + useTransition):
|
||||
> - **message-detail.tsx**:星标 `useState` → `useOptimistic<boolean>` + `useTransition`。`handleToggleStar` 在 `startStarTransition` 内 `addOptimisticStarred(nextStarred)` + `await toggleMessageStarAction` + `router.refresh()`。JSX 中所有 `isStarred` 引用改为 `optimisticIsStarred`(Button variant/aria-pressed/Star className/星标徽章)
|
||||
> - **message-list.tsx**:星标 `starredOverride` useState → `useOptimistic<Map<string, boolean>>`(key=messageId, value=isStarred)。`getIsStarred` 改为 `optimisticStarredMap.get(m.id)`;`handleToggleStar` 重写为 `startStarTransition` + `addOptimisticStar` + `await router.refresh()`。撤回 `recalledOverride` 仍保留 useState(不需要乐观更新)
|
||||
> - **关键决策**:成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值;不为撤回(recall)操作使用 useOptimistic,因为撤回需要根据服务端返回判断是否成功(2 分钟窗口校验),失败时不应乐观显示已撤回
|
||||
|
||||
---
|
||||
|
||||
## 2.14 notifications(通知分发模块)
|
||||
@@ -1671,8 +1835,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
**导出函数**:
|
||||
- Actions:`sendNotificationAction` / `sendClassNotificationAction` / `getNotificationsAction` / `getUnreadNotificationCountAction` / `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` / `archiveNotificationAction`(✅ P1-4 新增:后 4 个通知 CRUD Action 从 messaging 模块迁移;✅ V2-P2-13b 新增:archiveNotificationAction 归档 Action;✅ V3-P1-3 新增:`getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 从 messaging 模块迁移)
|
||||
- Dispatcher:`sendNotification(payload)` / `sendBatchNotifications(payloads)`
|
||||
- Data-access:`createNotification` / `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount` / `archiveNotification` / `unarchiveNotification` / `getUserContactInfo` / `logNotificationSend` / `logNotificationSendBatch`(✅ P0-4 / P1-5 修复后从 messaging 迁移;✅ V2-P2-13b 新增:archiveNotification / unarchiveNotification 归档函数)
|
||||
- Dispatcher:`sendNotification(payload)` / `sendBatchNotifications(payloads)`(✅ P3-7:`sendBatchNotifications` 重构为 in_app 单次批量 INSERT,其他渠道保持 per-payload 并行)
|
||||
- Data-access:`createNotification` / `createNotifications(items)`(✅ P3-7 新增:批量 INSERT 多行)/ `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount` / `archiveNotification` / `unarchiveNotification` / `getUserContactInfo` / `logNotificationSend` / `logNotificationSendBatch`(✅ P0-4 / P1-5 修复后从 messaging 迁移;✅ V2-P2-13b 新增:archiveNotification / unarchiveNotification 归档函数)
|
||||
- Preferences:`getNotificationPreferences` / `upsertNotificationPreferences`(✅ P0-4 / P1-5 修复后从 messaging 迁移)
|
||||
- Channels:`InAppChannelSender` / `SmsChannelSender` / `EmailChannelSender` / `WeChatChannelSender`
|
||||
- Components:`NotificationList` / `NotificationDropdown`(✅ P1-4 新增:从 messaging/components 迁移;✅ V2-P2-13b:NotificationList 支持优先级 Badge 显示和归档操作;✅ V2-P2-13c:NotificationList 支持按类型筛选)
|
||||
@@ -1705,8 +1869,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `dispatcher.ts` | ~145 | 渠道选择 + 并行分发(✅ V3-P1-4:移除 `sendBatchNotifications` 重复日志) |
|
||||
| `data-access.ts` | ~250 | 站内通知 CRUD + 用户联系方式 + 发送日志持久化(notification_logs 表;P0-4 / P1-5 修复后新增通知 CRUD;✅ V2-P2-13b:新增归档函数 + 优先级/归档筛选;✅ V3-P2-8:console 日志添加 TODO 统一日志服务标记) |
|
||||
| `dispatcher.ts` | ~225 | 渠道选择 + 并行分发(✅ V3-P1-4:移除 `sendBatchNotifications` 重复日志;✅ P3-7:`sendBatchNotifications` 重构为 in_app 批量 INSERT + 其他渠道并行) |
|
||||
| `data-access.ts` | ~290 | 站内通知 CRUD + 用户联系方式 + 发送日志持久化(notification_logs 表;P0-4 / P1-5 修复后新增通知 CRUD;✅ V2-P2-13b:新增归档函数 + 优先级/归档筛选;✅ V3-P2-8:console 日志添加 TODO 统一日志服务标记;✅ P3-7:新增 `createNotifications` 批量 INSERT 函数) |
|
||||
| `preferences.ts` | 166 | 通知偏好 CRUD(P0-4 / P1-5 修复后从 messaging 迁移) |
|
||||
| `actions.ts` | ~380 | 9 个 Server Action(✅ P1-4:新增 4 个通知 CRUD Action;✅ V2-P2-13b:新增 archiveNotificationAction;✅ V3-P1-3:新增 getNotificationPreferencesAction / updateNotificationPreferencesAction 从 messaging 迁移) |
|
||||
| `schema.ts` | 22 | ✅ V3-P1-3 新增:通知偏好校验 Schema(从 messaging/schema.ts 迁移) |
|
||||
@@ -1831,7 +1995,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
**导出函数**:
|
||||
- Actions:`getAnnouncementsAction` / `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction`(✅ P1 已修复:actions 层不再直接访问 DB,全部下沉到 data-access;✅ 发布公告时触发通知模块 `sendBatchNotifications`)/ `toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`(✅ V2-P2-13d 新增:置顶切换、已读标记、批量已读状态查询;✅ Audit-P0-3:`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` 新增资源所有权/可见性二次校验;✅ Audit-P1-1:所有 Action 返回 i18n 化 message;✅ Audit-P1-7:`handleActionError` 集成 console.error + trackEvent("announcement.action_error") + 区分 PermissionDeniedError / FORBIDDEN_ANNOUNCEMENT)
|
||||
- Data-access:`getAnnouncements`(✅ Audit-P0-2:`audience` 字段从单值升级为数组 `{ gradeIds, classIds }`,过滤逻辑改用 `inArray`)/ `getAnnouncementById` / `getAnnouncementByIdForUser`(✅ Audit-P0-1 新增:用户端详情页专用,结合 status=published + 受众过滤 + 已读状态 + readCount 一次返回,防止越权)/ `isAnnouncementVisibleToAudience`(✅ Audit-P0-1 新增:纯函数,判断公告对受众是否可见,可独立单测)/ `resolveUserAudience`(✅ Audit-P0-2:从原 `resolveAudience` 重命名,遍历所有 childrenIds/classIds/gradeIds 而非仅首个)/ `resolveAnnouncementTargetUserIds`(✅ Audit-P1-5:从 actions.ts 下沉到 data-access)/ `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`(后 5 个为 P1-2 新增)/ `toggleAnnouncementPin`(✅ Audit-P1-6:原子化 UPDATE ... SET isPinned = NOT isPinned)/ `markAnnouncementAsRead`(✅ Audit-P1-6:onDuplicateKeyUpdate 实现幂等 upsert)/ `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser`(✅ V2-P2-13d 新增:置顶切换 + 已读回执 CRUD)/ `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData`(✅ P1-5 新增:管理端列表页和编辑页编排函数)/ `getUserAnnouncementsPageData`(✅ V3-P0-2 新增:用户端列表页编排函数)/ `getAdminAnnouncementDetailPageData`(✅ Audit-P1-2 新增:管理端详情页编排函数,并行 getAnnouncementById + getAnnouncementReadCount + classes.getClassOptions)
|
||||
- Data-access:`getAnnouncements`(✅ Audit-P0-2:`audience` 字段从单值升级为数组 `{ gradeIds, classIds }`,过滤逻辑改用 `inArray`)/ `getAnnouncementById` / `getAnnouncementByIdForUser`(✅ Audit-P0-1 新增:用户端详情页专用,结合 status=published + 受众过滤 + 已读状态 + readCount 一次返回,防止越权)/ `isAnnouncementVisibleToAudience`(✅ Audit-P0-1 新增:纯函数,判断公告对受众是否可见,可独立单测)/ `resolveUserAudience`(✅ Audit-P0-2:从原 `resolveAudience` 重命名,遍历所有 childrenIds/classIds/gradeIds 而非仅首个)/ `resolveAnnouncementTargetUserIds`(✅ Audit-P1-5:从 actions.ts 下沉到 data-access;✅ P3-7:school 分支改为分页循环遍历 `getAllUserIds(PAGE_SIZE, offset)`,PAGE_SIZE=1000,避免单次查询超大学校全量用户导致 OOM)/ `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`(后 5 个为 P1-2 新增)/ `toggleAnnouncementPin`(✅ Audit-P1-6:原子化 UPDATE ... SET isPinned = NOT isPinned)/ `markAnnouncementAsRead`(✅ Audit-P1-6:onDuplicateKeyUpdate 实现幂等 upsert)/ `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser`(✅ V2-P2-13d 新增:置顶切换 + 已读回执 CRUD)/ `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData`(✅ P1-5 新增:管理端列表页和编辑页编排函数)/ `getUserAnnouncementsPageData`(✅ V3-P0-2 新增:用户端列表页编排函数)/ `getAdminAnnouncementDetailPageData`(✅ Audit-P1-2 新增:管理端详情页编排函数,并行 getAnnouncementById + getAnnouncementReadCount + classes.getClassOptions)
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`school`(获取年级列表)、`classes`(获取班级列表 + 解析受众)、`users`(获取目标用户 ID 列表)、`notifications`(发布公告时发送通知)、`shared/i18n`(✅ Audit-P1-1:Actions 通过 `getTranslations('announcements')` 返回 i18n 化 message)
|
||||
@@ -2894,6 +3058,14 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
> - **V2-2 节点级 AI 协助 4 项**:`lib/ai-node-assist.ts` 新增 4 个服务函数(`generateNodeContent`/`optimizeNodeExpression`/`suggestNodeDifferentiation`/`generateLayeredQuestions`),全部纯服务端、Zod 校验、失败返回 null;`actions-ai.ts` 新增 4 个 Server Action,每个 action 校验 `LESSON_PLAN_READ + LESSON_PLAN_CREATE + AI_CHAT` 三个权限点;`hooks/use-node-ai-assist.ts` 新增客户端 hook;`components/paper-editor/paper-editor.tsx` 注入 `onAiAction` 回调;`components/detail-panel/detail-panel.tsx` 的 4 个 `AiButton` 接入真实 action;i18n 新增 `v4.contextMenu.aiRunning`/`aiSuccess`/`aiFailed`/`aiComingSoon` 键(zh-CN + en)
|
||||
> - **V4-I18N-1 i18n 完整审查与修复**:对 lesson-preparation 模块 i18n 配置进行全量审查并修复——P0 修复运行时 MISSING_MESSAGE:`version.diff.*` 3 键(title/summary/noChanges)+ `schema.ts` 引用的 8 个 `error.*` 键(labelTooLong/versionNoInvalid/nameRequired/nameTooLong/queryTooLong/blockIdRequired/commentTooLong/reviewerRequired)+ `v4.contextMenu` 6 键(copied/copyFailed/aiComingSoon/aiRunning/aiSuccess/aiFailed)(zh-CN + en 同步);P1 修复 en locale 下大面积报错:en `calendar` 节键名对齐 zh-CN(weekView/monthView/loadFailed + prev/next/eventMeta/weekDays 子节),en `analytics` 节补齐 totalPublished/totalSubmitted/totalStandardsLinked/loadFailed;P2 修复 7 处硬编码中文(detail-props.tsx 的 stageLabel/differentiationLabel、inline-node.tsx 的 exerciseCount、inline-qa-dialog.tsx 的 expectedAnswer、qa-editor.tsx 的 turnContentPlaceholder、curriculum-map-view.tsx 的 legendTitle/noData)并新增对应 6 个 i18n 键(`v4.detail.stageLabel`/`differentiationLabel`/`exerciseCount`/`turnContentPlaceholder`、`v4.heatmap.legendTitle`/`noData`);清理 7 个 dead 节(review/comment/formative/substitute/evaluation/standards/gradeHead,analytics 因被 `app/(dashboard)/admin/curriculum-map` 引用而保留);`lib/i18n-errors.ts` 新增 `t.has(msg)` 运行时守卫,避免 `as` 断言绕过类型检查(键不存在时返回原消息而非抛出 MISSING_MESSAGE);zh-CN 与 en 顶层键集一致性验证通过(43 = 43);005_architecture_data.json 同步追加 `V4-I18N-1` auditFixes 条目;known-issues.md 追加 3 个 i18n 规则表(键缺失与中英文不同步 / dead 节清理规则 / 动态键翻译守卫)
|
||||
|
||||
> 架构变更(2026-07-05,Phase 4.2 — Zustand selector 细粒度拆分,避免整体订阅级联 re-render):
|
||||
> - **lesson-plan-editor.tsx**:`const editor = useLessonPlanEditor()` 整体订阅拆分为 9 个细粒度 selector(`isDirty`/`isOnline`/`doc`/`title`/`isSaving`/`saveError`/`lastSavedAt`/`setTitle` + `canUndo`/`canRedo` 派生 boolean);`canUndo`/`canRedo` 改为 `useLessonPlanEditor((s) => s.canUndo())` 订阅派生 boolean,移除每次渲染时调用 `editor.canUndo()` 函数;`useEffect` 依赖与 `printablePlan` useMemo 同步更新使用拆分后的变量
|
||||
> - **text-study-block.tsx**:`const { updateNode } = useLessonPlanEditor()` → `useLessonPlanEditor((s) => s.updateNode)`
|
||||
> - **exercise-block.tsx**:拆为 `useLessonPlanEditor((s) => s.updateNode)` + `useLessonPlanEditor((s) => s.planId)`
|
||||
> - **paper-context-menu.tsx**:拆为 7 个细粒度 selector(`toggleExpand`/`expandedNodeIds`/`updateNode`/`removeNode`/`addAnchor`/`duplicateNode`/`nodes`);特别地将 `doc` 改为 `s.doc.nodes` 更精细订阅(`addAnchor` 等仅修改 anchors 不会触发本组件 re-render)
|
||||
> - **关键决策**:不使用 `useShallow` 包装多字段 selector,因为单字段 selector 已经够细,且避免了 `useShallow` 自身的浅比较开销;函数引用(`setTitle`/`updateNode` 等)天然稳定,无需 `useShallow`
|
||||
> - **验证**:tsc 零新增错误;lint 零新增错误(4 个 modified 文件均未出现在 lint 输出中)
|
||||
|
||||
---
|
||||
|
||||
## 2.27b standards(课标模块,2026-07-01 新增)
|
||||
@@ -3065,7 +3237,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| **Hook** | `useDragPosition` | `modules/ai/hooks/use-drag-position.ts` | 拖拽位置 Hook(pointer 事件 + 拖拽状态)— V3 新增 |
|
||||
| **Context** | `createFullAiClientService` | `modules/ai/context/create-ai-client-service.ts` | 创建完整 AI 客户端服务工厂(含全部 9 个 Action)— V3 新增 |
|
||||
| **Context** | `createCoreAiClientService` | `modules/ai/context/create-ai-client-service.ts` | 创建核心 AI 客户端服务工厂(仅 6 个常用 Action)— V3 新增 |
|
||||
| **Component** | `AiAssistantWidget` | `modules/ai/components/ai-assistant-widget.tsx` | 全局 AI 助手悬浮按钮(上下文感知)— V2 新增 |
|
||||
| **Component** | `AiAssistantWidget` | `modules/ai/components/ai-assistant-widget.tsx` | 全局 AI 助手悬浮按钮(上下文感知)— V2 新增 / Phase 4.7 改造为懒加载 wrapper(`next/dynamic` ssr:false,仅渲染 Skeleton 占位 + LazyInner) |
|
||||
| **Component** | `AiAssistantWidgetInner` | `modules/ai/components/ai-assistant-widget-inner.tsx` | 全局 AI 助手悬浮球内部实现(悬浮球 + 抽屉 + AiChatPanel + 上下文推断)— Phase 4.7 从 ai-assistant-widget.tsx 拆出,通过 dynamic 懒加载,使 AI SDK / Markdown 渲染等重型依赖打入独立 chunk |
|
||||
| **Component** | `AiChatPanel` | `modules/ai/components/ai-chat-panel.tsx` | AI 对话面板(流式 + Markdown + 复制 + 停止 + 建议)— V2 增强 |
|
||||
| **Component** | `AiMarkdownRenderer` | `modules/ai/components/ai-markdown-renderer.tsx` | Markdown 渲染器(GFM + 复制按钮)— V2 新增 |
|
||||
| **Component** | `AiGradingAssist` | `modules/ai/components/ai-grading-assist.tsx` | AI 批改辅助(主观题预评分 + 反馈建议) |
|
||||
@@ -3143,7 +3316,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `modules/ai/services/usage-tracker.ts` | ~90 | AI 使用量埋点(V3 新增 child_summary/study_path 能力) |
|
||||
| `modules/ai/services/content-safety.ts` | ~290 | 内容安全过滤(输入/输出/每日限制/原子配额/苏格拉底校验)— V2 新增 / V3 加固 |
|
||||
| `modules/ai/context/ai-client-provider.tsx` | ~62 | React Context Provider + Hooks |
|
||||
| `modules/ai/components/ai-assistant-widget.tsx` | ~170 | 全局 AI 助手悬浮按钮 — V2 新增 / V4 i18n 修复 |
|
||||
| `modules/ai/components/ai-assistant-widget.tsx` | ~25 | 懒加载 wrapper(next/dynamic ssr:false + Skeleton 占位)— Phase 4.7 改造 / V2 新增 / V4 i18n 修复 |
|
||||
| `modules/ai/components/ai-assistant-widget-inner.tsx` | ~330 | 全局 AI 助手悬浮球内部实现(悬浮球 + 抽屉 + AiChatPanel + 上下文推断)— Phase 4.7 新增(从 ai-assistant-widget.tsx 拆出) |
|
||||
| `modules/ai/components/ai-chat-panel.tsx` | ~305 | AI 对话面板(流式 + Markdown)— V2 增强 |
|
||||
| `modules/ai/components/ai-markdown-renderer.tsx` | ~100 | Markdown 渲染器 — V2 新增 |
|
||||
| `modules/ai/components/ai-child-summary.tsx` | ~170 | 家长学情摘要 — V2 新增 |
|
||||
@@ -3544,7 +3718,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
**职责**:封装跨 4 张表(questions / textbooks / exams / announcements)的全文检索数据访问层,供 `app/api/search/route.ts` 调用。本次 API 规范化重构从 `app/api/search/route.ts` 下沉而来,消除 `app/` 层直接查询 DB 的架构违规(违反「app → modules → shared」三层架构单向依赖)。
|
||||
|
||||
**导出函数**(`data-access.ts`,server-only):
|
||||
- `searchQuestions(kw, limit?)` — 题库检索:`CAST(content AS CHAR) LIKE` 模糊匹配 JSON 字段,返回 `SearchResultItem[]`(href 指向 `/admin/questions?id=`)
|
||||
- `searchQuestions(q, limit?)` — 题库检索:P3-6 改用 `MATCH(questions.contentText) AGAINST(? IN BOOLEAN MODE)` 全文检索(基于 `content_text` STORED 生成列的 FULLTEXT 索引);`toBooleanModeQuery` 工具函数将原始查询转为 `+word1* +word2*` 格式;返回 `SearchResultItem[]`(href 指向 `/admin/questions?id=`)
|
||||
- `searchTextbooks(kw, limit?)` — 教材检索:title/subject/publisher 三字段 OR 模糊匹配
|
||||
- `searchExams(kw, limit?)` — 试卷检索:title/description 模糊匹配
|
||||
- `searchAnnouncements(kw, limit?)` — 公告检索:仅返回 `status=published` 的公告,title/content 模糊匹配;HTML 内容通过 `stripHtml` 提取纯文本摘要
|
||||
@@ -3567,7 +3741,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- 被依赖:`app/api/search/route.ts`
|
||||
|
||||
**数据库表**(只读):
|
||||
- `questions`(content JSON 字段、type、createdAt)
|
||||
- `questions`(content JSON 字段、P3-6 新增 `content_text` STORED 生成列 `CAST(content AS CHAR)` 用于 FULLTEXT 索引、type、createdAt)
|
||||
- `textbooks`(title、subject、grade、publisher、createdAt)
|
||||
- `exams`(title、description、status、createdAt)
|
||||
- `announcements`(title、content、type、status、createdAt)
|
||||
@@ -3575,7 +3749,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `data-access.ts` | 212 | 4 个 search 函数 + 3 个内部纯函数(extractTextFromJson/stripHtml/truncate)+ DEFAULT_SEARCH_PAGE_SIZE 常量 |
|
||||
| `data-access.ts` | ~230 | 4 个 search 函数(P3-6: searchQuestions 改 MATCH AGAINST)+ 4 个内部纯函数(extractTextFromJson/stripHtml/truncate/toBooleanModeQuery)+ DEFAULT_SEARCH_PAGE_SIZE 常量 |
|
||||
| `types.ts` | 31 | SearchType/SearchResultItem/SearchResponse 类型 + isSearchType 类型守卫 |
|
||||
|
||||
---
|
||||
@@ -4191,6 +4365,20 @@ formatNumber(v: number | null | undefined, digits?: number): string
|
||||
|
||||
// shared/lib/search-params.ts (re-export from utils.ts)
|
||||
getParam(params: SearchParams, key: string): string | undefined // = getSearchParam
|
||||
|
||||
// shared/lib/cache/ (服务端缓存基础设施,P1 重构新增)
|
||||
// store-factory.ts —— getCacheStore() 单例工厂:默认 MemoryCacheStore,CACHE_DRIVER=redis 时懒加载 RedisCacheStore
|
||||
// memory-store.ts / redis-store.ts —— CacheStore 两种实现
|
||||
// cache-fn.ts —— cacheFn 双层包装器:外层 react.cache 请求级 memoization,内层 cacheStore.getOrSet 跨请求缓存(按 tag 失效,TTL 可选)
|
||||
// invalidation-map.ts —— INVALIDATION_MAP 集中式失效映射表 + fillTemplate 模板填充
|
||||
// invalidate.ts —— invalidateFor 三步编排函数(Server Action 写后调用)
|
||||
getCacheStore(): Promise<CacheStore>
|
||||
cacheFn<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>, options: CacheFnOptions): (...args: TArgs) => Promise<TResult>
|
||||
fillTemplate(template: string, params: Record<string, string>): string
|
||||
INVALIDATION_MAP // 集中式失效映射表常量
|
||||
invalidateFor(actionId: string, params?: Record<string, string>): Promise<void>
|
||||
// 三步编排:1) CacheStore.invalidateTags 2) revalidateTag × N 3) revalidatePath × N
|
||||
// 未知 actionId 抛错 "[cache] Unknown actionId: <id>. Update INVALIDATION_MAP."
|
||||
```
|
||||
|
||||
### 业务模块核心 Actions
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user