Compare commits

...

171 Commits

Author SHA1 Message Date
SpecialX
313bae87bf docs(spec): add enterprise microservices architecture upgrade design
Some checks failed
Lighthouse CI / lighthouse (push) Has been cancelled
Security / deep-security-scan (push) Failing after 1m32s
DR Drill / dr-drill (push) Failing after 39s
CI / build-deploy (push) Waiting to run
CI / security-scan (push) Blocked by required conditions
CI / scheduled-backup (push) Failing after 35s
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
2026-07-07 21:34:15 +08:00
SpecialX
e3d132dc1b docs(known-issues): log enterprise architecture normalization work experience
Some checks failed
CI / scheduled-backup (push) Failing after 1m32s
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
Lighthouse CI / lighthouse (push) Has been cancelled
2026-07-07 20:25:33 +08:00
SpecialX
dbb124bf17 feat: add CI unit test, /api/health, Dockerfile HEALTHCHECK, bundle-analyzer 2026-07-07 20:23:03 +08:00
SpecialX
415dde9122 docs(roadmap): populate tech-debt.md with identified and resolved items 2026-07-07 20:18:07 +08:00
SpecialX
072c0d52b9 chore: add husky + lint-staged + commitlint for commit quality control 2026-07-07 20:17:15 +08:00
SpecialX
fb619139e5 docs: add enterprise repository files (LICENSE/CHANGELOG/CONTRIBUTING/SECURITY) 2026-07-07 20:14:56 +08:00
SpecialX
031e8a8175 refactor: split 5 oversized files into domain-specific subfiles (0 violations) 2026-07-07 20:13:22 +08:00
SpecialX
7f26bb8f9c fix(permissions): fix 9 Server Action permission violations and add recursive CTE for indirect call detection 2026-07-07 19:30:49 +08:00
SpecialX
164dcd4c84 feat(arch-scan): mark 12 exempt Server Actions with @public JSDoc tag 2026-07-07 19:24:26 +08:00
SpecialX
692e8ef580 feat(arch-scan): add @public JSDoc tag exemption mechanism for Server Actions 2026-07-07 19:19:01 +08:00
SpecialX
747344bfe3 chore: snapshot before P0 security phase (backup point) 2026-07-07 19:12:33 +08:00
SpecialX
3f68f3eb09 docs(plan): add enterprise architecture normalization implementation plan 2026-07-07 19:12:08 +08:00
SpecialX
205b463900 docs(architecture): add enterprise architecture normalization design spec 2026-07-07 19:09:40 +08:00
SpecialX
5d9981fd7d 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
2026-07-07 16:23:35 +08:00
SpecialX
7387d70289 chore(scripts): update check-db-state and seed-grade5-chinese scripts
- Update scripts/check-db-state.mjs

- Update scripts/seed-grade5-chinese.ts
2026-07-07 16:22:54 +08:00
SpecialX
fc150e1e14 feat(app,styles): update dashboard pages and global styles
- Update parent: children/[studentId]/page, elective/page, leave/page

- Update student: error-book/student-error-book-list-client

- Update teacher: classes/schedule/schedule-filters, classes/students/students-filters

- Update src/app/globals.css
2026-07-07 16:22:27 +08:00
SpecialX
ebaf03107d feat(modules-admin): update scheduling, school, settings, standards, student, textbooks, users
- scheduling: update auto-schedule-panel, schedule-change-form, schedule-change-list,

  schedule-conflicts-view, scheduling-rules-form, data-access-class-schedule, data-access

- school: update academic-year-view, departments-view, grade-form-dialog, data-access

- settings: update admin-settings-view, ai-provider-settings-card, avatar-upload,

  brand-config-card, notification-preferences-form, password-change-form,

  profile-settings-form, security-recent-logins-section, security-two-factor-section

- standards: update data-access

- student: update student-courses-view

- textbooks: update chapter-sidebar-list, create-chapter-dialog, force-graph,

  graph-kp-node, graph-prerequisite-edge, knowledge-graph-node, textbook-card,

  textbook-form-dialog, textbook-reader, textbook-settings-dialog,

  data-access-graph, data-access, use-kp-create, use-kp-delete, use-kp-update, types

- users: update user-import-dialog
2026-07-07 16:22:07 +08:00
SpecialX
783b8f5484 feat(modules-comm): update messaging, notifications, onboarding, parent, proctoring, questions, rbac
- messaging: update message-compose, message-detail, message-draft-list,

  message-group-compose, message-list, message-report-block,

  message-template-picker, unread-message-badge, data-access

- notifications: update notification-list, data-access, use-notification-stream, preferences

- onboarding: update actions, data-access, use-onboarding-form

- parent: update child-schedule-card, parent-export-button

- proctoring: update data-access

- questions: update batch-operations, create-question-dialog, import-export-buttons,

  question-actions, data-access

- rbac: update actions, permission-catalog
2026-07-07 16:21:00 +08:00
SpecialX
524ecade19 feat(lesson-preparation): major update with data-access splits and new components
- Update ai-feedback-dialog, attachment-picker, blocks/text-study-block

- Update detail-panel (detail-panel, detail-props)

- Update inline-question-editor, lesson-plan-card, lesson-plan-editor,

  lesson-plan-mobile-view, schedule-dialog, version-history-drawer

- Update paper-editor (inline-qa-dialog, paper-context-menu)

- Update structure-tree (structure-tree, tree-node-row)

- Update config/block-registry, hooks (editor-slice, use-lesson-plan-persistence,

  use-node-ai-assist), lib (ai-node-assist, consistency-check, export, type-guards)

- Update publish-service, services/default-question-service

- Update data-access files (ai-evaluation, analytics, attachments, calendar,

  comments, formative, knowledge, review, schedules, templates, versions, main)
2026-07-07 16:20:34 +08:00
SpecialX
2adf61faa8 feat(modules-assessment): update exams, files, grades, homework, invitation-codes, leave-requests
- exams: update assembly (exam-paper-preview, question-bank-list, structure-editor),

  exam-actions, exam-assembly, exam-columns, exam-data-table, exam-form,

  exam-preview-utils, exam-rich-form, editor (exam-nodes-to-editor-doc,

  exam-rich-editor-inner, question-block, selection-toolbar),

  hooks (use-exam-preview-rewrite, use-exam-preview-state, use-exam-preview-tasks,

  use-exam-preview)

- files: update data-access, use-file-batch-operations, use-file-upload

- grades: update batch-grade-entry, excel-import-dialog, export-button,

  grade-record-form, grade-record-list, knowledge-point-mastery-chart,

  report-card-print-action, report-card-print-button, data-access-analytics,

  use-batch-grade-entry-undo, use-draft-lock

- homework: update homework-assignment-form, homework-batch-grading-view,

  homework-grading-view, homework-scan-grading-view, homework-take-view, scan-uploader

- invitation-codes: update generate-invitation-codes-dialog, invitation-codes-view, data-access

- leave-requests: update leave-request-form, leave-review-dialog, data-access
2026-07-07 16:19:46 +08:00
SpecialX
d7017f0e30 feat(modules-classes): update classes, course-plans, dashboard, diagnostic, elective, error-book
- classes: update actions-shared, admin-classes-view, class-detail, class-invitation-manager,

  grade-classes-view, my-classes-grid, schedule dialogs, students-table,

  data-access-admin, data-access-students, data-access-teacher, data-access

- course-plans: update course-plan-detail, course-plan-form, course-plan-item-editor,

  template-picker-dialog, data-access

- dashboard: update dashboard-time-range-filter, use-dashboard-preferences,

  use-dashboard-realtime

- diagnostic: update class-diagnostic-view, report-list, data-access

- elective: update elective-course-form, elective-course-list, student-selection-view,

  data-access-operations, data-access-selections, data-access

- error-book: update add-error-book-dialog, error-book-detail-dialog, review-buttons
2026-07-07 16:19:08 +08:00
SpecialX
b090a815ae feat(modules-edu): update ai, adaptive-practice, announcements, attendance, audit, auth
- ai: update chat-panel, child-summary, error-book-analysis, grading-assist,

  lesson-content-generator, markdown-renderer, question-variant-generator,

  study-path, usage-dashboard, use-ai-chat-stream, use-position-persistence

- adaptive-practice: update practice-result-view, practice-session-view,

  practice-starter, data-access-analytics, lib/type-guards

- announcements: update announcement-card, detail, form, data-access

- attendance: update attendance-record-list, attendance-rules-form,

  attendance-sheet, data-access

- audit: update actions, audit-log-export-button, audit-retention-settings,

  data-access

- auth: update login-form, register-form, data-access
2026-07-07 16:18:29 +08:00
SpecialX
224740ad98 feat(shared): update permissions system, i18n, hooks, and components
- Update permission-bitmap.ts, permissions.ts, resolve-action-error.ts

- Update types/permissions.ts

- Update rbac i18n messages (en, zh-CN)

- Update locale-switcher.tsx

- Update use-action-with-toast.ts hook
2026-07-07 16:17:53 +08:00
SpecialX
b333cda8c5 refactor(data-access,lib): split data-access files and add type-guards across modules
- grades: split analytics into class/overview/student/trend files

- messaging: split into bulk/core/group/reports/templates

- school: split into classrooms/departments/grades/schools/semesters/subjects

- Add lib/type-guards.ts for attendance, classes, elective, exams,

  invitation-codes, leave-requests, notifications, scheduling, school,

  settings, standards, textbooks

- Add lesson-preparation/lib/status-mappers.ts

- Add parent/actions.ts

- Add shared/lib/date-utils.ts
2026-07-07 16:17:24 +08:00
SpecialX
4e6d397d8e docs(architecture): sync logging refactor to 004/005 and known-issues
Task 15: 004 架构影响地图新增 1.1.7 日志系统重构章节(架构图、核心组件表、Request ID 贯穿链路、Edge Runtime 限制、替换范围、环境变量),shared/lib 清单新增 logger.ts/request-context.ts/with-request-context.ts/track-event.ts 更新,hooks 清单新增 use-error-report.ts。

Task 16: 005 架构数据 JSON 新增 3 个 shared/lib 文件节点、1 个 hooks 节点、1 个 apiRoute(/api/client-error)、5 个 dependencyMatrix 依赖关系,JSON 有效性验证通过。

Task 17: known-issues.md 新增二十八、日志系统规则章节,含 5 个规则表(pino 使用、Edge Runtime 限制、Request ID 贯穿、ESLint no-console、客户端错误上报)。
2026-07-07 13:00:00 +08:00
SpecialX
7c1b764b59 feat(logging): wire useErrorReport into all 129 error.tsx boundaries
所有 129 个 error.tsx 客户端错误边界接入 useErrorReport Hook,客户端路由错误自动上报到 /api/client-error 端点,服务端通过 pino logger 统一记录。移除了 7 处 useEffect + console.error 调用。为无 props 的 error.tsx 补全 error 参数解构。
2026-07-07 12:52:32 +08:00
SpecialX
4122175915 feat(logging): add useErrorReport hook and /api/client-error endpoint
Task 12: useErrorReport Hook 使用 navigator.sendBeacon 上报客户端错误到 /api/client-error,降级到 fetch keepalive。节流策略:同一 error digest 在 1 分钟内只上报一次(基于 sessionStorage)。

Task 13: /api/client-error Route Handler 接收客户端错误,用 createModuleLogger('client-error') 记录到 pino 日志流,字段包含 message/stack/digest/path/userAgent/timestamp。
2026-07-07 12:41:41 +08:00
SpecialX
811ad11f9f feat(logging): enable ESLint no-console rule with client exemptions
全局启用 no-console: error 规则,强制服务端 .ts 文件使用 createModuleLogger。豁免场景: scripts/(脚本)、tests/(测试)、src/**/*.tsx(客户端组件,留待 Task 12-14 处理)、src/**/hooks/**/*.ts(客户端 hooks)、src/**/components/**/*.ts(客户端 utils)、src/shared/lib/query-client.ts(被客户端导入)。将 deletes/ 归档目录加入 globalIgnores。
2026-07-07 12:39:21 +08:00
SpecialX
12a766d3ee refactor(logging): replace console.* with module loggers in server modules
34 个服务端文件替换 86 处 console.* 调用为 createModuleLogger。模块覆盖: exams/audit/school/files/classes/notifications/grades/auth/homework/announcements/settings/lesson-preparation。shared lib: cache/redis-store, rate-limit/redis-limiter, redis-client, exam-homework-port。API routes: web-vitals, cron/audit-cleanup, proctoring/event。

web-vitals/route.ts 从 edge runtime 改为 nodejs runtime,因 pino 依赖 Node.js stream 内置模块,不兼容 Edge Runtime (V8 Isolate)。
2026-07-07 12:34:57 +08:00
SpecialX
0a034945d4 refactor(logging): replace console in track-event modules with logger 2026-07-07 12:14:23 +08:00
SpecialX
fefc65702d fix(logging): replace silent audit-logger failures with logger.warn 2026-07-07 12:09:38 +08:00
SpecialX
a75fdcd60d refactor(logging): replace console.error in api-response with logger 2026-07-07 12:06:36 +08:00
SpecialX
2236eb36e7 refactor(logging): replace console.error in action-utils with logger 2026-07-07 12:05:00 +08:00
SpecialX
c44f19aefd feat(logging): inject x-request-id in proxy.ts 2026-07-07 12:01:55 +08:00
SpecialX
dce2561751 feat(logging): add withRequestContext HOF for Server Actions 2026-07-07 11:51:47 +08:00
SpecialX
a208fcc601 feat(logging): add pino logger with createModuleLogger 2026-07-07 11:44:49 +08:00
SpecialX
3bd3ebc12d feat(logging): add request-context with AsyncLocalStorage 2026-07-07 11:34:40 +08:00
SpecialX
c2575960eb feat(logging): add pino dependency and LOG_LEVEL env var 2026-07-07 11:29:12 +08:00
SpecialX
94f098b0f2 perf(phase1-4): 补漏性能预算重构专项遗漏项
Phase 1 配置基线:next.config.ts 新增 experimental.optimizePackageImports(lucide-react/recharts/@xyflow/react/@tiptap/* /@radix-ui/*/date-fns)+ serverExternalPackages 追加 tencentcloud-sdk-nodejs + exceljs;providers.tsx 注入 WebVitalsReporter 闭环 RUM 上报链路(之前组件存在但未注入导致生产环境零性能数据)。

Phase 2 Bundle 预算优化补漏:TipTap 三实例(shared/ui/rich-text-editor + exams/editor/exam-rich-editor + lesson-preparation/blocks/rich-text-block)之前仅文件拆分未真正 next/dynamic ssr:false,本次补齐 lazy wrapper + xxx-inner.tsx 拆分模式;ReactFlow knowledge-graph.tsx 改用 next/dynamic 加载 inner;chart.tsx 改 recharts 具名导入替代 import * as RechartsPrimitive barrel。

Phase 4 组件渲染优化补漏:12 个 recharts 图表组件(parent/grades/dashboard/homework 模块下)margin props 提取到模块级 CHART_MARGIN 常量避免 inline 重建;layout.tsx metadata 增强(metadataBase/openGraph/twitter/robots/authors/creator)+ getLocale/getMessages/auth 串行 await 改 Promise.all 并行。

同步架构文档 004/005 + known-issues.md:新增 Phase 1 配置基线规则章节 + Phase 2 文件清单修正(之前引用不存在的 question-rich-editor.tsx 和 paper-rich-editor.tsx,实际是 rich-text-editor.tsx 和 rich-text-block.tsx)+ Phase 4.9 metadata + Promise.all 规则补充。架构图 1.1.5 章节新增未完成项(React Compiler/force-dynamic 评估/Playwright 性能断言)。

验证:npx tsc --noEmit 零错误;npm run lint 零新增错误(3 errors + 12 warnings 均位于未修改的 pre-existing 文件)。
2026-07-07 00:30:47 +08:00
SpecialX
6104e6a685 docs(architecture): 组件化重构专项遗漏补全 - 更新 005 lastUpdate + known-issues 25.4 节
- 005 JSON: lastUpdate 追加遗漏补全 9 模块 20 组件迁移记录
- known-issues.md: 新增 25.4 遗漏补全规则章节
- 全量验证:49 个重复组件全部删除,modules/*/components/ 下无残留
2026-07-06 22:24:17 +08:00
SpecialX
e76c626779 refactor(classes,audit,school,settings,adaptive-practice,announcements,messaging,questions,student): 组件化重构遗漏补全 - 20 个重复组件迁移
补全未被批次覆盖的 9 个模块的重复组件迁移:
- classes: 迁移 students-filters/schedule-filters/class-skeleton/class-error-boundary
- audit: 迁移 3 个 filters + audit-log-table-skeleton + audit-error-boundary
- school: 迁移 grade-insights-filters/school-skeleton/school-error-boundary
- settings: 迁移 settings-section-error-boundary
- adaptive-practice: 迁移 2 个 practice-stats-cards 到 StatsGrid
- announcements: 迁移 announcement-list-skeleton 到 SkeletonCard
- messaging: 迁移 message-list-skeleton 到 SkeletonCard
- questions: 迁移 question-filters 到 app 层
- student: 迁移 course-filters/student-schedule-filters 到 app 层
- 补充 school/settings 模块 i18n error.boundary* 键
- tsc 零错误
2026-07-06 22:22:23 +08:00
SpecialX
ee10380462 docs(architecture): 组件化重构专项完成 - 同步 005 lastUpdate + known-issues 第 25 章
- 005 JSON: lastUpdate 更新为组件化重构专项全量完成摘要
- known-issues.md: 新增第 25 章(4 个小节:底座使用规则/拆分模式/迁移规则/验证规则)
- 9 个巨型文件全部 ≤500 行,49 个重复组件全部删除
2026-07-06 20:57:14 +08:00
SpecialX
fa68ec0b34 refactor(lesson-preparation,ai,dashboard): 组件化重构第 3 批 - 高风险模块
- lesson-preparation: 拆分 lesson-plan-editor (594→273),新增 toolbar/dialogs/publish-button 子组件 + 持久化 Hook
- ai: 拆分 ai-chat-panel (417→218),新增 messages/input 子组件,保持流式响应状态稳定
- dashboard: 新增 DashboardShell 布局壳,4 个角色 dashboard 统一使用
- 删除 6 个重复组件(filters/skeletons/error-boundaries)
- tsc 零错误
2026-07-06 20:49:54 +08:00
SpecialX
025d4de50d refactor(homework,exams,textbooks): 组件化重构第 2 批 - 中风险模块
- homework: 拆分 take-view (428→330) + grading-view (507→170),新增 4 个子组件
- exams: 拆分 exam-assembly (467→268),新增 3 个子组件 + 1 个工具模块
- textbooks: 拆分 textbook-reader (458→291) + knowledge-graph-inner (411→267),新增 4 个子组件
- textbooks: 删除同名 section-error-boundary.tsx,改用 shared 层
- 删除 3 个重复 filter 组件,迁移到底座
- tsc 零错误
2026-07-06 19:54:39 +08:00
SpecialX
19a05091d3 refactor(grades,attendance,error-book,elective): 组件化重构第 1 批 - 低风险模块
- grades: 拆分 batch-grade-entry (457→374) + grade-record-list (523→231),新增 4 个子组件
- attendance: 迁移 stats-cards/filters 到 StatsGrid/FilterBar 底座
- error-book: 迁移 stats-cards/filters 到 StatsGrid/FilterBar 底座
- elective: 迁移 stats-cards/filters 到 StatsGrid/FilterBar 底座
- 删除 12 个重复组件,新增 app 层 filter 组件就近放置
- tsc 零错误,lint 无新增错误
2026-07-06 19:01:57 +08:00
SpecialX
11ddc8ccbe feat(shared): 组件化重构 Phase 1 - 底座收敛与补全
- 新增 ErrorBoundary 基础类组件 (ui/error-boundary.tsx)
- 新增 StatsGrid 容器 (ui/stats-grid.tsx)
- 扩展 Skeleton 新增 SkeletonCard variant
- 收敛 SectionErrorBoundary/RouteErrorBoundary/WidgetBoundary 为 ErrorBoundary preset
2026-07-06 18:00:08 +08:00
SpecialX
80d98e13e4 docs(plan): 组件化重构专项实施计划 v1 2026-07-06 17:49:14 +08:00
SpecialX
ef2040edf4 docs(spec): 组件化重构专项设计文档 v1 2026-07-06 17:44:15 +08:00
SpecialX
1a34d1f14e ci(perf): 添加 Lighthouse CI 性能预算回归门槛配置
Phase 2 性能预算重构配套 CI:每次 PR 与每日凌晨 3 点对 /login 路由采样 3 次(desktop preset),断言 LCP <= 3000ms、CLS <= 0.1(error 级),FCP/TBT/INP 为 warn 级。失败时阻断合并并触发性能预算审计报告基线复核。
2026-07-06 15:58:49 +08:00
SpecialX
9ce8d6d3fd refactor(data-access): 补全剩余 37 个 data-access 文件迁移至 cacheFn
迁移范围:lesson-preparation 11 文件、attendance 3 文件、settings 4 文件、classes-invitations、跨模块接口 4 文件、adaptive-practice/ai/onboarding/invitation-codes/search/standards/scheduling/leave-requests/audit。修复 3 个 cacheFn 块位置错误。同步架构文档迁移范围至 83 文件 250+ 函数。
2026-07-06 15:12:30 +08:00
SpecialX
22c2e6459d refactor(eslint): 移除缓存策略临时豁免(所有模块已迁移至 invalidateFor) 2026-07-05 22:55:50 +08:00
SpecialX
8fff820e1d refactor(actions): 组 C 模块迁移至 invalidateFor(lesson-preparation/elective/announcements/messaging/notifications/settings/rbac) 2026-07-05 22:53:26 +08:00
SpecialX
7cfd85d0f5 refactor(actions): 组 B 模块迁移至 invalidateFor(exams/grades/homework/attendance/leave-requests/diagnostic/error-book/adaptive-practice) 2026-07-05 22:44:38 +08:00
SpecialX
05e68a3dad refactor(actions): 组 A 模块迁移至 invalidateFor(users/school/textbooks/questions/course-plans/standards/scheduling/audit/onboarding/invitation-codes/proctoring/i18n) 2026-07-05 22:43:08 +08:00
SpecialX
0f33484bd2 fix(questions): 完成 questions/data-access.ts 遗漏的 cacheFn 迁移 2026-07-05 22:27:07 +08:00
SpecialX
841b130f4c fix(data-access): 修复 tsc 错误 + 完成 adaptive-practice/elective 迁移至 cacheFn 2026-07-05 22:24:56 +08:00
SpecialX
27374d1b2c refactor(data-access): 组 A 模块迁移至 cacheFn(users/school/rbac/textbooks/questions/course-plans/standards/files/dashboard/parent/proctoring) 2026-07-05 21:56:18 +08:00
SpecialX
6dee6b6299 refactor(data-access): 组 C 模块迁移至 cacheFn(lesson-preparation/elective/announcements/messaging/notifications/audit/onboarding/invitation-codes/scheduling/settings) 2026-07-05 21:49:18 +08:00
SpecialX
cb5b92160c feat(cache): 扩充 INVALIDATION_MAP 覆盖剩余 27 个模块
为 i18n、adaptive-practice、announcements、attendance、audit、course-plans、
diagnostic、elective、error-book、exams、grades、homework、invitation-codes、
leave-requests、lesson-preparation、messaging、notifications、onboarding、
proctoring、questions、rbac、scheduling、school、settings、standards、
textbooks、users 共 27 个模块登记写操作的失效副作用(tags/queryKeys/paths),
便于后续 actions 文件迁移调用 invalidateFor(actionId)。

- 新增 195 个 actionId(含 create/update/delete 基础动作及 batch/import/
  appeal/draft/lock/template/formative/comment/substitute 等特殊写操作)
- tags 采用模块级粒度 ["{module}"],简化迁移
- paths 从原 revalidatePath 调用中提取,动态段用父路径覆盖
- 同步扩充 CLIENT_INVALIDATION_MAP(仅 queryKeys 子集)
- 保持 classes 部分(已有)不变
- 通过 npx tsc --noEmit 与 eslint 零错误验证
2026-07-05 21:28:40 +08:00
SpecialX
99f15ee37a docs(architecture): 同步缓存基础设施 + classes 标杆迁移 + 缓存策略规则
- 004: 补全 shared/lib/cache 9 文件说明(types/memory-store/redis-store/store-factory/cache-fn/invalidation-map/client-invalidation-map/invalidate/index)+ shared/lib/redis-client + shared/lib/query-keys

- 005: shared.lib.exports.functions 补 5 个节点(MemoryCacheStore / RedisCacheStore / CLIENT_INVALIDATION_MAP / getRedisClient / queryKeys)

- known-issues.md 新增「二十四、缓存策略规则」章节,含 14 条规则表与涉及文件清单
2026-07-05 19:04:03 +08:00
SpecialX
a3dc22cb9e refactor(classes): 客户端组件迁移至 useQuery + queryKeys 示范
Task 19: 将 class-invitation-manager.tsx 从 initialCodes prop + 本地 codes state +
手动 Action 调用 + 手动 state 更新模式,迁移至 V5 缓存策略新 API:

- 列表查询: useActionQuery + queryKeys.classes.invitations(classId)
- 撤销 mutation: useActionMutation + actionId "classes.invitation.revoke"
  (成功后由 CLIENT_INVALIDATION_MAP 自动失效 ["classes", "invitations"] 前缀)
- 生成回调: queryClient.invalidateQueries 手动失效本班级 invitations 查询
- 移除 initialCodes prop + codes state + isSubmitting 手动管理

配套改动:
- actions-invitations.ts: 新增并导出 ClassInvitationCodeOption 强类型接口,
  listClassInvitationCodesAction 返回类型从 Array<Record<string, unknown>>
  收窄为 ClassInvitationCodeOption[]
- actions.ts barrel: 补充 type ClassInvitationCodeOption 与遗漏的
  bulkEnrollStudentsAction / bulkAssignSubjectTeachersAction 导出
- 架构文档 004/005 同步更新 hook 消费方与 actions.ts exports 清单
2026-07-05 18:57:10 +08:00
SpecialX
9eb02807a4 feat(eslint): 新增缓存策略规则禁止直接调用 revalidatePath/revalidateTag 2026-07-05 18:44:07 +08:00
SpecialX
80dd67780c refactor(classes): actions 迁移至 invalidateFor 集中编排 2026-07-05 18:38:58 +08:00
SpecialX
220d702b44 refactor(classes): data-access 迁移至 cacheFn + 双导出 raw 版本 2026-07-05 18:27:32 +08:00
SpecialX
dd7a49504f refactor(hooks): useActionQuery/useActionMutation 接入 QueryClient + 向后兼容
- useActionQuery 新增 queryKey 入参模式:传入 queryKey 走 useQuery 跨页共享缓存;不传回退旧 useEffect + useState 模式。Hook 内部始终声明 useQuery/useState/useEffect 以遵守 React Hooks 规则,通过 isCacheMode 切换启用状态

- useActionMutation 新增 mutationFn + actionId 模式:成功后按 CLIENT_INVALIDATION_MAP[actionId] 自动 invalidateQueries;mutate(action?) 参数可选

- toast 替换为 notify(行为等价,便于未来替换 toast 库)

- 同步更新 004/005 架构文档签名说明
2026-07-05 18:15:58 +08:00
SpecialX
0f9d8825e7 docs(cache): 同步 cacheFn 架构图与已知问题速查 2026-07-05 18:07:18 +08:00
SpecialX
d6227d6e6c feat(cache): 实现 cacheFn 双层包装(react.cache + cacheStore) 2026-07-05 18:07:11 +08:00
SpecialX
92913a728f feat(cache): 实现 invalidateFor 三步编排函数 2026-07-05 18:00:09 +08:00
SpecialX
e510f191c9 feat(cache): 新增 query-keys.ts 工厂(classes 模块) 2026-07-05 17:56:04 +08:00
SpecialX
2d49f1bad8 feat(cache): 新增 index.ts 公共 API 聚合导出 2026-07-05 17:55:58 +08:00
SpecialX
6baa60b2a1 feat(cache): 新增 INVALIDATION_MAP 集中式失效映射表(classes 模块) 2026-07-05 17:52:41 +08:00
SpecialX
13409e55f1 feat(cache): 新增 store-factory(CACHE_DRIVER 切换) 2026-07-05 17:52:24 +08:00
SpecialX
1756ac21a8 feat(cache): 新增客户端失效映射子集(queryKeys only) 2026-07-05 17:52:19 +08:00
SpecialX
0058b0b311 feat(cache): 实现 MemoryCacheStore(LRU + TTL + tag 索引) 2026-07-05 17:49:51 +08:00
SpecialX
4174cd61d7 feat(cache): 实现 RedisCacheStore(多实例 + tag 索引 + fail-open) 2026-07-05 17:45:02 +08:00
SpecialX
44f997bad7 refactor(redis): 抽出共享 Redis 客户端单例至 shared/lib/redis-client.ts 2026-07-05 17:40:45 +08:00
SpecialX
fcf89dfb2c feat(cache): 新增 cache/types.ts 类型定义 2026-07-05 17:35:17 +08:00
SpecialX
bc03275262 refactor(cache): 提升 upstash-modules 类型声明至 shared/lib 2026-07-05 17:33:31 +08:00
SpecialX
16ffd44161 feat(cache): 新增 CACHE_DRIVER 环境变量 2026-07-05 17:32:01 +08:00
SpecialX
be31da6223 docs(cache): 缓存策略落地实施计划 + 修正 INVALIDATION_MAP 占位符一致性 2026-07-05 01:37:15 +08:00
SpecialX
8afd7af6dc docs(cache): 缓存策略落地专项设计文档 v1 - 全栈缓存策略一致性(服务端数据缓存+客户端 TanStack Query+失效编排) 2026-07-05 01:30:59 +08:00
SpecialX
9ec1be1528 fix(design-tokens): 补充 semantic-dark.css 缺失的 --radius 令牌
审查发现 semantic-light.css 定义了 --radius 但 semantic-dark.css 缺失,导致明暗不对称。补充 --radius: 0.5rem 与 :root 一致。
2026-07-05 01:09:36 +08:00
SpecialX
214ebec976 refactor(design-tokens): 全量体系化重建设计令牌
Primitive + Semantic 双层令牌架构,HEX->HSL,明暗双份,@theme inline 暴露为 Tailwind 类。

- 新建 src/app/styles/tokens/ 6 个令牌文件(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index)
- globals.css 改为 @import 引入,477->258 行
- 清理 91 处 #hex 硬编码颜色 -> hsl(var(--*))
- 清理 10 处硬编码字体 -> var(--font-family-*)
- 清理 100 文件 Tailwind 任意值(Tier 1 映射/Tier 3 注释豁免)
- 清理 M3 Surface 死代码,升级 --lp-* 令牌(HEX->HSL + 暗色补全)
- 新建 ESLint 自定义规则 no-hardcoded-design-tokens(单词边界正则)
- eslint.config.mjs 新增 no-restricted-syntax 禁止 #hex + 自定义规则加载(pathToFileURL)
- 项目规则新增设计令牌规范强制章节
- 架构图 004/005 同步设计令牌体系节点
- known-issues.md 追加设计令牌问题分类(7 个规则表)

验证: tsc --noEmit 0 errors, npm run lint 0 errors/12 warnings(均为既有问题)
2026-07-05 01:03:19 +08:00
SpecialX
d1ad7a1f75 docs(architecture): update impact map, data, troubleshooting, add mockups
Some checks failed
Security / deep-security-scan (push) Failing after 2m10s
DR Drill / dr-drill (push) Failing after 1m32s
CI / scheduled-backup (push) Failing after 31s
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

- Remove obsolete _update_004.cjs helper script

- Update troubleshooting/known-issues.md

- Add docs/mockups/ directory
2026-07-04 23:02:14 +08:00
SpecialX
e962b67050 feat(lesson-preparation): update paper-editor, detail-panel, AI node assist, i18n
- Update actions-ai.ts and curriculum-map-view.tsx

- Update detail-panel: detail-panel, detail-props, qa-editor

- Update paper-editor: inline-node, inline-qa-dialog, paper-context-menu,

  paper-editor, textbook-tiptap-editor

- Update hooks/editor-slice.ts and lib/i18n-errors.ts

- Add hooks/use-node-ai-assist.ts and lib/ai-node-assist.ts

- Update en/zh-CN lesson-preparation i18n messages
2026-07-04 23:02:04 +08:00
SpecialX
0780524c08 docs(troubleshooting): V4 备课编辑器规则
新增 V4 备课编辑器(纸感重构)规则表,8 条规则:正文节点必须是 Tiptap 编辑器、锚点用 Tiptap Mark 内嵌、节点展开位置由 order 决定、节点类型色点用 --lp-dot-*、文档版本必须是 4、师生交互节点用 QaEditor、右键菜单触发对象区分、v3 锚点迁移失效提示。
2026-07-04 12:01:06 +08:00
SpecialX
5ca2a76e3a docs(architecture): V4 纸感重构架构图同步
004_architecture_impact_map.md: 更新 lesson-preparation 章节描述(V4 三栏布局)、新增 V4 纸感重构架构变更章节(H1/I1/J1/J2/J3/J4/J5 + 数据模型 v4)、更新文件清单(移除 9 个废弃文件,新增 paper-editor/structure-tree/detail-panel/interaction-block/anchor-migration-banner 等组件 + anchor-mark/export lib + history/expanded slices)。

005_architecture_data.json: 更新 lesson_preparation 节点——description 改为 V4 描述、dependencies 移除 @xyflow/react 改为 @tiptap/* 包、files 数组移除 9 个废弃文件并新增 V4 组件、auditFixes 新增 V4-PAPER-1~8 + V4-PAPER-DOC 条目。JSON 语法验证通过。
2026-07-04 11:59:39 +08:00
SpecialX
0e4eccec21 chore(lesson-preparation): V4 lint + tsc 零错误验证
重构 anchor-migration-banner:使用 key-based remount + lazy initial state 避免 setState in effect,符合 V4 react-hooks/set-state-in-effect 规则。

验证:tsc --noEmit 0 错误;lint 中 V4 引入的错误已全部修复。剩余 6 个 lint 错误为预存无关模块问题(exams/use-exam-preview-tasks react-hooks/refs 3 个、template-picker V5-9 set-state-in-effect 1 个),13 个 warnings 均为预存代码(schedule-dialog、unread-message-badge 等),按计划要求已有的无关模块错误需记录但不要求修复。
2026-07-04 11:53:13 +08:00
SpecialX
e762490f19 refactor(lesson-preparation): print-view 适配 V4
- export.ts 已使用 order 排序,无画布依赖
- 新增 interaction block 的 flatten 逻辑(设计意图 + 对话轮次)
- 更新文档注释(12 种 Block)
2026-07-04 11:47:50 +08:00
SpecialX
5d306dc686 refactor(lesson-preparation): 删除废弃的 React Flow 画布和字符串锚点文件
- 删除 node-editor.tsx(React Flow 画布)
- 删除 nodes/lesson-node.tsx、textbook-content-node.tsx、textbook-segments.tsx、anchor-node-selector.tsx
- 删除 lib/anchor-injector.ts(字符串偏移系统)
- 删除 lib/rf-mappers.ts(React Flow 映射)
- 删除 lib/auto-layout.ts(自动布局)
- 重构 lesson-plan-readonly-view.tsx 改用线性视图(LessonPlanMobileView)
2026-07-04 11:46:31 +08:00
SpecialX
124ed97060 refactor(lesson-preparation): 主编辑器三栏布局(结构树 + 纸 + 详情面板)
- 移除 NodeEditor/NodeEditPanel 导入和 JSX
- 改为 StructureTree + PaperEditor + DetailPanel 三栏布局
- 新增 AnchorMigrationBanner
- 移除 aiContentGenerator prop(V4 DetailPanel 自带 AI 按钮)
- 删除 node-edit-panel.tsx(不再被引用)
- 删除 ai-content-generator-slot.tsx(依赖 node-edit-panel)
2026-07-04 11:44:26 +08:00
SpecialX
e29f30d278 feat(lesson-preparation): v3 锚点失效提示 banner 2026-07-04 11:41:24 +08:00
SpecialX
6dd3b1fecd feat(lesson-preparation): 注册 interaction block 到 registry 2026-07-04 11:40:46 +08:00
SpecialX
85121c49a3 fix(lesson-preparation): 重命名 TreeNodeRow.children prop 为 childItems 避免 react/no-children-prop 2026-07-04 11:36:41 +08:00
SpecialX
8ff14fce1c feat(lesson-preparation): 右栏详情面板容器 2026-07-04 11:33:03 +08:00
SpecialX
24b8ae78c6 feat(lesson-preparation): 师生交互 block(详情编辑入口) 2026-07-04 11:32:58 +08:00
SpecialX
43f30f4013 feat(lesson-preparation): 师生交互编辑器(对话轮次增删改) 2026-07-04 11:32:54 +08:00
SpecialX
40cba54ed4 feat(lesson-preparation): 详情面板属性条 2026-07-04 11:32:49 +08:00
SpecialX
a4c9eb02b4 feat(lesson-preparation): 详情面板头部 2026-07-04 11:32:44 +08:00
SpecialX
05f9b4fd9d feat(lesson-preparation): 左栏结构树容器 2026-07-04 11:31:03 +08:00
SpecialX
c7cbc86fd4 feat(lesson-preparation): 树节点行组件 2026-07-04 11:30:57 +08:00
SpecialX
6fa712ad15 feat(lesson-preparation): 中栏纸区容器(正文 + 展开节点 + 右键菜单) 2026-07-04 11:29:53 +08:00
SpecialX
4db77b7d1e feat(lesson-preparation): 右键菜单(节点操作 + AI 协助 + 锚定) 2026-07-04 11:29:14 +08:00
SpecialX
cbed4ef508 feat(lesson-preparation): 师生交互对话体 inline 渲染 2026-07-04 11:28:16 +08:00
SpecialX
8583e3e387 feat(lesson-preparation): inline-node 展开节点容器 2026-07-04 11:28:11 +08:00
SpecialX
9b0b7ef619 feat(lesson-preparation): 浮动工具条 + 锚点/inline-node/对话体样式 2026-07-04 11:26:55 +08:00
SpecialX
66a60213bf feat(lesson-preparation): 正文 Tiptap 编辑器 2026-07-04 11:26:50 +08:00
SpecialX
1b898fb6cf fix(lesson-preparation): BLOCK_REGISTRY 补全 interaction 空条目(A1 类型变更扫尾,渲染逻辑留给 H1) 2026-07-04 11:20:18 +08:00
SpecialX
d7b15093f9 fix(lesson-preparation): BLOCK_TYPE_KEYS 补全 interaction(A1 类型变更扫尾) 2026-07-04 11:19:44 +08:00
SpecialX
5c4db2fedc feat(lesson-preparation): Tiptap AnchorMark + AnchorPoint 扩展 2026-07-04 11:18:56 +08:00
SpecialX
ccb2cf05df feat(lesson-preparation): V4 i18n 翻译键(zh-CN + en) 2026-07-04 11:18:03 +08:00
SpecialX
4f00e566df refactor(lesson-preparation): 配色统一到中性令牌(移除 Material 鲜艳色) 2026-07-04 11:17:07 +08:00
SpecialX
ef1be6d04f refactor(lesson-preparation): editor-slice 移除画布依赖(V4) 2026-07-04 11:16:05 +08:00
SpecialX
0f4e58d7e1 feat(lesson-preparation): selection-slice 增加 anchorNodeForSelection 2026-07-04 11:15:14 +08:00
SpecialX
a856ab005d feat(lesson-preparation): 组合 ExpandedSlice 到 EditorState 2026-07-04 11:15:01 +08:00
SpecialX
7b550d3b69 feat(lesson-preparation): expanded-slice(节点展开状态) 2026-07-04 11:14:46 +08:00
SpecialX
f7ea76cd60 feat(lesson-preparation): V3→V4 迁移函数(锚点失效标记 + expandedNodeIds) 2026-07-04 11:14:27 +08:00
SpecialX
8a7d0f69ca feat(lesson-preparation): isInteractionBlockData 类型守卫 2026-07-04 11:12:33 +08:00
SpecialX
217b5b48e4 feat(lesson-preparation): interaction 默认数据生成器 2026-07-04 11:12:13 +08:00
SpecialX
8fc798fbcf feat(lesson-preparation): V4 类型定义(interaction + QATurn + V4 文档) 2026-07-04 11:11:54 +08:00
SpecialX
ccf1618b1c docs(lesson-preparation): V4 纸感重构实现计划
25 个任务,按阶段 A-J 分解:类型定义、状态管理、配色 i18n、Tiptap 锚点、纸区组件、结构树、详情面板、主编辑器改造、迁移 banner、清理验证。
2026-07-04 11:08:28 +08:00
SpecialX
a1c283e10d docs(lesson-preparation): 备课编辑器无边记纸感重构设计
新增设计文档,定义正文节点改造为真实 Tiptap 富文本编辑器、三栏布局(结构树 + 纸 + 详情面板)、节点展开到正文、师生交互节点、Tiptap Mark 锚点系统、v3→v4 数据迁移。
2026-07-04 10:58:55 +08:00
SpecialX
872d5fb085 docs(architecture): update impact map, data, audit report, troubleshooting
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

- Update 005_architecture_data.json

- Add lesson-preparation-audit-report-v5.md

- Add troubleshooting docs
2026-07-04 10:24:08 +08:00
SpecialX
cbc6e259fa chore(scripts): cleanup obsolete scripts and add new diagnostic scripts
- Remove temp-i18n-zh-fix.mjs, test-failing-modules.py, test-teacher-pages.py

- Add check-attendance-rules.mjs, check-db-state.mjs, test-modules.py
2026-07-04 10:23:55 +08:00
SpecialX
4b6cb5f11e feat(app): update dashboard layout and student learning error boundaries
- Update src/app/layout.tsx

- Update student/learning/study-path/error.tsx
2026-07-04 10:23:43 +08:00
SpecialX
d2c250a1b3 feat(shared): update db schema, i18n messages, rate-limit, auth-session-provider
- Update src/shared/db/schema.ts

- Update i18n messages for en and zh-CN (error-book, exam-homework, leave,

  lesson-preparation, notifications, rbac, student, textbooks, diagnostic)

- Update src/shared/lib/rate-limit/index.ts and redis-limiter.ts

- Update src/shared/components/auth-session-provider.tsx
2026-07-04 10:23:29 +08:00
SpecialX
3569d83b8e feat(modules): update layout, notifications, questions, rbac, files
- layout: update site-header.tsx

- notifications: update use-desktop-notifications.ts

- questions: update question-actions.tsx, question-columns.tsx

- rbac: update permission-catalog.ts

- files: update types.ts
2026-07-04 10:23:16 +08:00
SpecialX
98429e87eb feat(business): update audit, auth, course-plans, announcements, exams
- audit: update retention.ts, types.ts, audit-retention-settings.tsx

- auth: update actions.ts and types.ts

- course-plans: update actions.ts, course-plan-detail.tsx, template-picker-dialog.tsx,

  add lib/track-event.ts

- announcements: update page.tsx, announcement-list.tsx, announcement-pagination.tsx

- exams: update actions.ts
2026-07-04 10:23:03 +08:00
SpecialX
b63d116b6c feat(diagnostic): refactor services with monitor and context providers
- Update default-diagnostic-service.ts

- Update diagnostic-monitor-context.tsx

- Update diagnostic-service-context.tsx

- Update monitored-diagnostic-service.ts

- Update teacher diagnostic page
2026-07-04 10:22:48 +08:00
SpecialX
eee0145274 feat(error-book): update components and analytics data-access
- Update all error-book components (filters, cards, charts, dialogs)

- Update data-access-analytics.ts

- Update student-error-book-list-client.tsx
2026-07-04 10:22:36 +08:00
SpecialX
6ea8ba763b feat(textbooks): add force-graph component and update graph components
- Add force-graph.tsx for new graph visualization

- Update graph-toolbar, knowledge-graph, knowledge-point-list, textbook-reader

- Update use-graph-data hook and types

- Update textbooks error boundary pages
2026-07-04 10:22:24 +08:00
SpecialX
25dca843be feat(lesson-preparation): major update with AI features, schedules, and new components
- Add actions-schedules.ts and data-access-schedules.ts for schedule management

- Add AI differentiation, AI feedback, consistency check dialogs

- Add attachment-picker, curriculum-heatmap, print-view, version-diff-view

- Add lesson-plan-mobile-view and schedule-dialog components

- Add lib: ai-differentiation, ai-feedback, auto-layout, consistency-check,

  curriculum-coverage, export, version-diff

- Add history-slice hook for version history

- Update existing components, hooks, providers, services, types

- Add teacher lesson-plans heatmap and library pages
2026-07-04 10:22:10 +08:00
SpecialX
41fe8d8903 feat(drizzle): consolidate database migrations into single baseline
- Remove 0000-0021 incremental migrations

- Add consolidated 0000_aberrant_deathstrike.sql baseline

- Update meta/_journal.json and 0000_snapshot.json
2026-07-04 10:21:45 +08:00
SpecialX
e7a01eadef chore(config): update next config, dependencies, and project rules
- Update next.config.ts

- Update package.json and package-lock.json

- Update .trae/rules/project_rules.md
2026-07-04 10:21:32 +08:00
SpecialX
284c7939b8 test(mocks): update exam mock data
Some checks failed
CI / scheduled-backup (push) Failing after 1m32s
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 src/mocks/exam-data.ts with new mock entries
2026-07-03 10:32:29 +08:00
SpecialX
6a22922ddd docs(architecture): sync architecture docs with code changes
- Update 001_project_overview.md

- Update 004_architecture_impact_map.md

- Update 005_architecture_data.json

- Add 008_module_role_mapping.md

- Add _update_004.cjs helper script
2026-07-03 10:32:15 +08:00
SpecialX
93eacccdbf feat(users,layout): update users module and navigation config
- Update users/actions.ts with new server actions

- Update users/data-access.ts with new data access functions

- Update users/components/admin-users-view.tsx

- Update layout/config/navigation.ts
2026-07-03 10:31:57 +08:00
SpecialX
2dd8c2197c feat(error-book): update components, actions, data-access, and add analytics
- Add data-access-analytics for error book analytics queries

- Update actions, data-access for improved error book operations

- Update components: add-error-book-dialog, analytics-stats-cards, chapter-weakness-chart, class-error-bar-chart, class-filter, error-book-detail-dialog, error-book-filters, error-book-item-card, error-book-list, error-book-stats-cards, grouped-student-error-table, knowledge-point-weakness-chart, review-buttons, subject-distribution-chart, subject-tabs, top-wrong-questions

- Remove class-error-overview (replaced by new components)
2026-07-03 10:30:41 +08:00
SpecialX
56c3f32e2d chore(config): update auth, env, i18n, proxy, and dependencies
- Update src/auth.ts auth configuration

- Update src/env.mjs environment variable validation

- Update src/i18n/request.ts locale handling

- Update src/proxy.ts middleware

- Update src/next-auth.d.ts type declarations

- Update package.json and package-lock.json dependencies
2026-07-03 10:30:28 +08:00
SpecialX
0e63c24ed9 feat(shared,tests): add error boundaries, lib utils, i18n messages, and integration tests
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
shared:

- Add class-filter, error-state, route-error, section-error-boundary, widget-boundary components

- Add ui/alert component

- Add constants directory

- Add breached-password, export-utils, permission-bitmap, rate-limit, resolve-action-error, route-permissions, route-resolver, type-guards lib

- Add i18n messages (en, zh-CN) for invitation-codes, parent, questions, rbac

tests:

- Add integration tests for elective

- Add tests/setup/empty-stub

scripts:

- Add update-md.cjs, tmp_append_en.ps1, tmp_merge_en.ps1 utilities
2026-07-03 10:26:38 +08:00
SpecialX
21142f9b99 feat(app): add error/loading boundaries across all dashboard routes and new routes
- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes

- Add admin announcements edit, audit-logs overview, curriculum-map, invitation-codes, permissions, questions, roles routes

- Add admin elective detail and components, files, course-plans, users, scheduling boundaries

- Add messages group-compose route

- Add parent course-plans, elective, grades report-card, practice routes

- Add student course-plans, elective detail, error-book dialogs, grades report-card, learning study-path, leave, schedule boundaries

- Add teacher attendance report, classes boundaries, course-plans boundaries, elective, exams analytics/edit-rich/all/create/new, grades report-card, homework boundaries, leave, lesson-plans calendar

- Add auth loading, onboarding loading, api cron
2026-07-03 10:26:25 +08:00
SpecialX
e9a5264fe7 feat(parent,auth,onboarding,files,notifications,adaptive-practice,ai): add module updates
parent:

- Add parent-student-attendance-detail component

auth:

- Add actions, data-access, schema, services, types

onboarding:

- Add parent-children-form and hooks directory

files:

- Add actions, schema, hooks directory

notifications:

- Add schema and schema test

adaptive-practice:

- Add answer-input, answer-result, practice-result-view, practice-starter-with-nav

- Add question-card, question-content, lib and services directories

ai:

- Add context/create-ai-client-service, hooks/use-drag-position, hooks/use-position-persistence
2026-07-03 10:26:12 +08:00
SpecialX
f3c223d914 feat(settings,questions,school,textbooks): add brand config, question components, school dialogs, textbooks hooks
settings:

- Add actions-brand, brand-config, data-access-brand for brand management

- Add admin-file-upload-card, admin-notification-config-card, admin-school-info-card, admin-security-policy-card

- Add ai-provider-delete-dialog, ai-provider-selector, brand-config-card

- Add security-recent-logins-section, security-two-factor-section

- Add config/profile-overview-config, data-access-profile-overview, lib/system-settings-utils

questions:

- Add batch-operations, import-export-buttons, knowledge-point-selector, options-editor

- Add question-bank-results-client, question-cascade-filter, question-content-renderer, utils

school:

- Add grade-delete-dialog, grade-form-dialog, grade-list-toolbar, grade-overview-cards

- Add use-grade-data hook

textbooks:

- Add textbook-form-fields component

- Add use-kp-create, use-kp-delete, use-kp-update hooks
2026-07-03 10:26:00 +08:00
SpecialX
138b6f1b00 feat(dashboard,diagnostic,elective): add widgets, layout, parent dashboard, role-config, services, elective components
dashboard:

- Add comparison-badge, dashboard-notification-widget, dashboard-responsive-layout, dashboard-time-range-filter

- Add parent-dashboard components directory

- Add config, hooks, and services directories

diagnostic:

- Add role-config and services directory

elective:

- Add elective-course-detail, elective-stats-cards, parent-selection-view components

- Add data-access-settings and data-access-stats
2026-07-03 10:25:46 +08:00
SpecialX
dfffb61e94 feat(homework,classes,course-plans): add scans, student data, take confirm, error boundaries, dialogs, hooks, calendar
homework:

- Add data-access-scans, data-access-student, data-access-utils, data-access-exam-cross

- Add excellent-submissions, homework-take-confirm-dialog, homework-take-sidebar components

classes:

- Add class-delete-dialog, class-error-boundary, class-form-dialog, class-form-utils

- Add class-list-table, class-list-toolbar, class-skeleton

- Add schedule-create-dialog, schedule-delete-dialog, schedule-edit-dialog, schedule-utils

- Add data-access-teacher and hooks directory

course-plans:

- Add course-plan-calendar, sortable-week-row, template-picker-dialog components

- Add lib directory
2026-07-03 10:25:35 +08:00
SpecialX
20023e13fd feat(lesson-preparation): add AI evaluation, analytics, attachments, calendar, comments, review, substitutes, formative, and version diff
- Add actions-ai-evaluation, actions-analytics, actions-attachments, actions-calendar, actions-comments, actions-formative, actions-questions, actions-review, actions-substitutes

- Add corresponding data-access layers for each new action module

- Add calendar-view, curriculum-map-view, version-diff-viewer components

- Add editor-slice, selection-slice, version-slice hooks for state management

- Add document-diff and scope-check lib utilities

- Add default-question-service and external-questions-bridge services
2026-07-03 10:25:21 +08:00
SpecialX
a16f09d3c3 feat(exams): add rich editor actions, auto-mark, preview, services, and config
- Add actions-helpers and actions-rich-editor for rich text exam editing

- Add ai-pipeline/auto-mark for automatic exam marking

- Add exam-boundaries and exam-preview components

- Add config directory for exam configuration

- Add data-access-cross-module for cross-module data access

- Add editor/exam-nodes-to-editor-doc and editor/utils for editor utilities

- Add use-exam-preview-rewrite, use-exam-preview-state, use-exam-preview-tasks hooks

- Add services directory
2026-07-03 10:25:11 +08:00
SpecialX
e85a5f05dd feat(grades): add appeals, drafts, import, report card, and growth archive
- Add actions-appeal, actions-draft, actions-import, actions-lock for grade workflow

- Add data-access-appeals, data-access-drafts, data-access-exam-entry for data layer

- Add batch-grade-entry-dialog, batch-grade-entry-stats, batch-grade-entry-table

- Add draft-lock-banner, excel-import-dialog for import and draft management

- Add growth-archive-chart, knowledge-point-mastery-chart for analytics

- Add report-card-view, report-card-print-action, report-card-print-button

- Add import-export, lib/notify, lib/report-card, scope-check test, stats-service test

- Add hooks directory
2026-07-03 10:25:01 +08:00
SpecialX
2b95fd668b feat(announcements): add tests, skeleton, pagination, and service context
- Add announcement-card test for component testing

- Add is-announcement-visible test and schema test for logic testing

- Add announcement-list-skeleton for loading states

- Add announcement-pagination for list pagination

- Add announcements-service-context and default-announcements-service for service layer
2026-07-03 10:24:50 +08:00
SpecialX
2859ef74f2 feat(messaging): add drafts, group compose, templates, reports, blocks, and services
- Add message-draft-list and message-attachments for draft and attachment management

- Add message-group-compose for group messaging

- Add message-template-picker for message templates

- Add message-report-block for reporting and blocking users

- Add message-list-section and message-list-skeleton for list rendering

- Add lib and services directories for messaging utilities and service layer
2026-07-03 10:24:37 +08:00
SpecialX
c935597803 feat(audit): add export, retention, overview, charts, hooks, and services
- Add export and export.test for audit log export functionality

- Add retention and retention.test for audit log retention policy

- Add audit-overview-view, audit-overview-stats-bar, audit-activity-trend-chart, data-change-distribution-chart

- Add audit-log-detail-dialog, audit-log-table-skeleton, data-change-log-filters, data-change-log-view

- Add audit-error-boundary and audit-retention-settings

- Add hooks and services directories
2026-07-03 10:24:26 +08:00
SpecialX
048fc1c386 feat(attendance): add correlation, trend, warnings, report print, and services
- Add attendance-grade-correlation-card and data-access-correlation, correlation-compute

- Add attendance-trend-chart and trend-compute for trend analysis

- Add attendance-warnings-card and warning-compute for attendance warnings

- Add attendance-report-print for printable reports

- Add class-comparison-card for class attendance comparison

- Add notifications and services directory
2026-07-03 10:24:16 +08:00
SpecialX
7567f317e1 feat(modules): add leave-requests, invitation-codes, and standards modules
- Add leave-requests module for staff and student leave request management

- Add invitation-codes module for class invitation code generation and redemption

- Add standards module for curriculum standards management
2026-07-03 10:24:07 +08:00
SpecialX
ac1de9e433 feat(rbac): add role-based access control module
- Add RBAC module with data-access, schema, types, and components for role and permission management
2026-07-03 10:23:56 +08:00
SpecialX
cee7bbfd7a feat(scripts): add ollama provider, grade5 chinese seed, i18n fix, and l9 deps scripts
- Add add-ai-provider-ollama for Ollama AI provider setup

- Add seed-grade5-chinese for grade 5 Chinese content seeding

- Add temp-i18n-zh-fix for Chinese i18n fixes

- Add _l9_deps module dependency analysis
2026-07-03 10:23:47 +08:00
SpecialX
89b9e181d2 docs(audit): add audit reports for grades, homework, lesson-preparation, messaging, permissions, question-bank, settings, textbooks
- Add grades-audit-report

- Add homework-audit-report and homework-exams-audit-report

- Add lesson-preparation-audit-report-v3 and v4

- Add messaging-audit-report

- Add permissions-audit-report

- Add question-bank-audit-report

- Add settings-profile-audit-report-v3

- Add textbooks-audit-report-v3
2026-07-03 10:23:34 +08:00
SpecialX
365c36d97b feat(db): add migrations for RBAC, diagnostic, messaging, attendance, leave, invitation codes
- 0011: RBAC role flags and seed data

- 0012: diagnostic grade_id

- 0013: message recall

- 0014: message templates

- 0015: group messages and message reports/blocks

- 0016: invitation codes

- 0017: draft device sync

- 0018: attendance status reason

- 0019: attendance warning thresholds

- 0020: leave requests

- 0021: attendance period
2026-07-03 10:23:24 +08:00
SpecialX
48829bd02b chore(config): update vitest config, add vercel.json and manifest
- Update vitest.config.ts

- Add vercel.json for deployment configuration

- Add PWA manifest
2026-07-03 10:23:13 +08:00
SpecialX
e27efb6282 feat(exams): update question bank list, rich form, and selection toolbar
Some checks failed
Security / deep-security-scan (push) Failing after 2m23s
DR Drill / dr-drill (push) Failing after 11m59s
CI / scheduled-backup (push) Failing after 37s
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 question-bank-list component for exam assembly

- Update exam-rich-form for rich text exam editing

- Update selection-toolbar for editor selection actions
2026-06-24 15:37:10 +08:00
SpecialX
90f7d395f2 fix(exams): fix toolbar tracking and prevent list-to-options for non-choice questions
Two fixes:

1. Selection toolbar tracking: add pointerup listener so the toolbar
   updates its position immediately after the user finishes dragging
   the selection, not just on selectionUpdate events which can lag.

2. List-to-options conversion: parseOptions now only converts
   orderedList/bulletList to A/B/C options for choice question types
   (single_choice, multiple_choice, judgment). For text/composite
   questions, list content is preserved as part of the question stem
   text, preventing unwanted 'A.' prefixes.
2026-06-24 15:13:49 +08:00
SpecialX
e9429935b9 revert: roll back to ccf6c03 state (composite sub-questions preserved)
Reverts the following commits that broke composite question handling:
- 85661a5 auto-detect composite sub-questions from text patterns
- 064b3cf use slice to preserve full content when wrapping selections
- 2562de7 remove isolating to allow nested question blocks

Restores the working state from ccf6c03 where:
- wrapInQuestion fails gracefully in isolating nodes
- selected text is preserved when creating sub-questions
- composite question structure is intact
2026-06-24 15:01:56 +08:00
SpecialX
85661a5ba9 fix(exams): auto-detect composite sub-questions from text patterns
Root cause: When users paste reading comprehension content into a
composite question block without manually marking each sub-question
as a questionBlock, the sub-questions were treated as plain text and
merged into the question stem. This caused:
1. Sub-questions not showing in the preview's sub-question area
2. Text from different paragraphs being concatenated without newlines
3. "A." appearing because parseOptions misidentified list items

Fix:
1. extractText now inserts newlines between paragraphs/listItems,
   preserving text structure so pattern detection can work
2. Added detectSubQuestionsFromText: for composite questions without
   explicit questionBlock children, auto-detect sub-questions from
   text patterns:
   - Numbered lines: "1.xxx", "2.xxx", "(1)xxx", "①xxx"
   - Lines with score markers: "xxx(3分)"
   - If numbered sub-questions are found, check preceding lines for
     un-numbered sub-questions (e.g., the first sub-question that
     lacks a number but has a score marker)
3. extractMaterialText removes detected sub-question text from the
   question stem, keeping only the reading material/passage

This allows users to paste reading comprehension content directly
into a composite question block and have sub-questions automatically
detected, without needing to manually mark each one.
2026-06-24 14:53:05 +08:00
SpecialX
064b3cf736 fix(exams): use slice to preserve full content when wrapping selections
Root cause: wrapIn (Tiptap's built-in command) can fail or lose content
when the selection spans multiple paragraphs or partial paragraphs. When
users selected a long reading passage and clicked "复合" (composite),
the content was destroyed — only fragments remained.

Fix: replace wrapIn with a manual slice-based approach:
1. Use doc.slice(from, to) to get the complete node structure of the
   selection (preserves paragraphs, lists, images, etc.)
2. deleteRange to remove the original selection
3. insertContentAt to insert a new questionBlock/groupBlock/sectionBlock
   containing the sliced content

This is more reliable than wrapIn because it doesn't depend on
ProseMirror's wrapping logic, which has edge cases with multi-paragraph
selections. The slice API captures the exact node structure, so no
content is lost.

Applied to all three wrapping operations:
- insertQuestion (questionBlock)
- insertGroup (groupBlock)
- insertSection (sectionBlock)
2026-06-24 14:37:01 +08:00
SpecialX
2562de76b7 fix(exams): remove isolating to allow nested question blocks
Root cause: questionBlock/groupBlock/sectionBlock had `isolating: true`,
which prevents ProseMirror operations (wrapIn, insertContentAt) from
inserting nodes inside these blocks. When users selected text inside a
composite question and clicked "填空/简答" to create a sub-question,
the new questionBlock was inserted OUTSIDE the composite block instead
of inside it, so buildQuestion couldn't detect it as a subQuestion.

Fix: remove `isolating: true` from all three block extensions. The
`defining: true` property is sufficient to maintain node boundaries
during normal editing operations.

This allows wrapInQuestion to work correctly inside composite questions,
creating properly nested sub-question blocks that are detected by
buildQuestion's recursive parsing.
2026-06-24 14:33:04 +08:00
SpecialX
ccf6c03096 fix(exams): preserve selected text when wrapIn fails in isolating nodes
When wrapIn fails inside isolating nodes (e.g. composite question block),
the previous fallback used insertContent which replaced the entire
selection with an empty questionBlock, causing other sub-questions to
disappear and content to be cleared.

New approach: when wrapIn fails, extract the selected text, delete the
selection, then insert a new node (questionBlock/groupBlock/sectionBlock)
containing the selected text as paragraphs. This preserves the content
and converts it into the desired structure.

Applied to all three wrap operations:
- insertQuestion (questionBlock)
- insertGroup (groupBlock)
- insertSection (sectionBlock)
2026-06-24 14:24:04 +08:00
SpecialX
df9561128b fix(exams): fix duplicate React keys and composite question marking
- Fix parseOptions: deduplicate option ids within a single question to
  avoid multiple "A" keys when a question contains multiple lists
- Fix ExamPreview: use composite keys (questionId-optId, questionId-sub-id)
  to ensure global uniqueness across questions
- Fix selection-toolbar: when wrapInQuestion fails inside isolating nodes
  (e.g. composite question block), fall back to insertQuestion instead of
  silently doing nothing
2026-06-24 14:19:46 +08:00
SpecialX
1f28efbeb6 feat(exams): add section/group structure nodes with auto stats
Distinguish structural levels from questions:
- sectionBlock (new): top-level volume like "第Ⅰ卷 选择题(共24分)"
- groupBlock (enhanced): section like "一、选择题" with instruction field
- questionBlock (composite): reserved for reading comprehension

Key changes:
- New section-block.tsx extension with level attr (1=卷, 2=部分) and
  auto-computed question count + total score in NodeView
- group-block.tsx: add instruction field ("每小题3分"), auto stats display
- editor-to-structure.ts: recursive buildStructureNode supports arbitrary
  nesting (section > group > question), computeStats accumulates scores
- exam-rich-form.tsx ExamPreview: render section/group/question with
  distinct styles and stats badges
- selection-toolbar.tsx: add "分卷" button (Layers icon)
- exam-rich-editor.tsx: register SectionBlock, expose insertSection/
  wrapInSection via ref
- actions.ts: AI prompt now outputs volumes[] + groups[] structure with
  instruction; buildTiptapDocFromAiResponse generates nested sectionBlock
- i18n: add markSection keys (zh-CN/en)

Structural nodes are NOT questions: their question count and total score
are automatically computed from child questions, not manually set.
2026-06-24 14:07:29 +08:00
SpecialX
f260720443 fix(exams): fix composite question sub-questions not showing in preview
- Fix buildQuestion in editor-to-structure.ts: recursively detect nested
  questionBlock nodes as subQuestions (composite questions now properly
  extract child questions instead of treating them as plain text)
- Fix buildQuestionBlock in actions.ts: AI auto-mark now generates nested
  questionBlock nodes for sub-questions instead of plain paragraphs, so
  they are properly structured and detectable by the parser
- Rewrite ExamPreview component with proper layout:
  - Question header: number + type label badge + score
  - Indented question text and options
  - Composite sub-questions shown in nested block with left border
  - Image thumbnails
  - Empty state message
  - Title centered at top
  - Group titles with bottom border
2026-06-24 13:54:24 +08:00
SpecialX
7380f1e6c8 fix(exams): fix rich editor crashes and redesign form layout
- Fix groupBlock/questionBlock insertContent error: empty content violates
  schema "block+", now provide default empty paragraph
- Fix buildTiptapDocFromAiResponse: ensure groupBlock/questionBlock always
  have content (fallback to empty paragraph)
- Add wrapInGroup/wrapInQuestion commands to wrap selected text into
  question/group blocks (vs insert which creates empty blocks)
- Update SelectionToolbar: use wrap when text selected, insert when not
- Redesign exam-rich-form layout:
  - Merge basic info into single-row toolbar (title/subject/grade/difficulty/score/duration)
  - Remove separate "source text" textarea (user pastes directly in editor)
  - AI auto-mark now reads from editor content via getText()
  - Editor + preview takes full height (calc(100vh-180px))
- Enhance AI prompt for Chinese exam papers:
  - Support reading material (阅读理解选段)
  - Support dotted chars (加点字注音)
  - Support sub-questions (阅读理解小题)
  - Better type detection (single/multiple choice, judgment, fill, essay, composite)
- Add splitByDottedTexts helper to mark dotted chars in Tiptap doc
- Add i18n keys for titlePlaceholder, editorArea description
2026-06-24 13:41:39 +08:00
SpecialX
d1e4ccbf98 refactor(exams): redesign exam creation page with 3-mode selector
- Replace cramped 3-column grid with vertical layout
- Add 3 large selectable cards at top: Manual / AI / Rich Text Editor
- Rich Text Editor mode redirects to /teacher/exams/new
- Basic info form is now always visible (not hidden in AI mode)
- Exam mode config always visible at bottom
- Add "rich" to mode enum with validation bypass
- Replace all hardcoded English/Chinese strings with i18n keys
- Add 20+ new i18n keys to zh-CN and en (mode labels, descriptions, actions)
- Clean up mixed-language UI text
2026-06-24 13:23:13 +08:00
SpecialX
6114607c1e feat(exams,homework): add rich text exam editor and scan-based grading
- Add Tiptap-based rich text editor with custom extensions (dotted-mark,
  blank-node, image-node, group-block, question-block) for exam creation
- Add AI auto-marking action to convert pasted exam text to structured editor doc
- Add resizable split-panel layout for editor + live preview
- Add student scan upload (photo of paper answers) with drag-drop and reorder
- Add scan image viewer with zoom/rotate/fullscreen for teachers
- Add scan grading view with side-by-side questions and scan images
- Add /teacher/exams/new and /teacher/homework/submissions/[id]/scan-grading routes
- Fix getScansAction to support both teacher (HOMEWORK_GRADE) and student
  (HOMEWORK_SUBMIT) permission scopes
- Add i18n keys for rich editor, scan upload, and scan grading (zh-CN/en)
- Sync architecture diagrams (004/005) with new modules, routes, and deps
2026-06-24 13:16:33 +08:00
1647 changed files with 200587 additions and 68455 deletions

View File

@@ -13,6 +13,16 @@ AI_API_KEY=""
AI_BASE_URL=""
AI_MODEL=""
# ===== Redis / 缓存配置(可选) =====
# 缓存驱动: memory(默认,单实例 LRU) | redis(分布式,多实例共享)
CACHE_DRIVER=memory
# 速率限制驱动: memory(默认,单实例) | redis(分布式,多实例共享)
RATE_LIMIT_DRIVER=memory
# Upstash Redis REST 凭据(仅 CACHE_DRIVER=redis 或 RATE_LIMIT_DRIVER=redis 时必填)
# 获取方式: 注册 https://upstash.com → 创建数据库 → 复制 REST URL 和 TOKEN
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# ===== 灾备配置 =====
# 异地备份后端类型: s3|oss|nfs|none
BACKUP_OFFSITE_BACKEND=none
@@ -65,3 +75,7 @@ BACKUP_DIR=./backups
RETENTION_DAYS=30
# 备份校验最小文件大小(字节,默认 1024)
BACKUP_VERIFY_MIN_SIZE=1024
# ===== 日志配置 =====
# 日志级别debug/info/warn/error默认 info
LOG_LEVEL=info

View File

@@ -67,6 +67,14 @@ jobs:
- name: Typecheck
run: npm run typecheck
- name: Unit tests
run: npm run test:unit
- name: Architecture scan
run: |
npm run arch:scan
npm run arch:query -- violations || true
- name: Install Playwright Chromium
run: npx playwright install chromium

View File

@@ -0,0 +1,71 @@
name: Lighthouse CI
# 性能预算回归门槛:每次 PR 与每日凌晨 3 点对关键路由采样断言。
# 失败时阻断合并,触发审计报告 docs/architecture/audit/performance-budget-audit-report.md 中基线复核。
on:
pull_request:
branches:
- main
schedule:
- cron: "0 3 * * *" # 每天凌晨 3 点性能采样
workflow_dispatch:
jobs:
lighthouse:
runs-on: CDCD
container: dockerreg.eazygame.cn/node-with-docker:22
env:
SKIP_ENV_VALIDATION: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cache npm dependencies
uses: actions/cache@v3
id: npm-cache
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Configure npm proxy
run: |
GATEWAY_IP=$(ip route show | grep default | awk '{print $3}')
if [ -z "$GATEWAY_IP" ]; then
GATEWAY_IP="172.17.0.1"
fi
PROXY_URL="http://$GATEWAY_IP:7890"
npm config set proxy "$PROXY_URL"
npm config set https-proxy "$PROXY_URL"
echo "http_proxy=$PROXY_URL" >> $GITHUB_ENV
echo "https_proxy=$PROXY_URL" >> $GITHUB_ENV
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Start production server
run: npm run start &
env:
PORT: "3000"
- name: Wait for server
run: |
for i in {1..30}; do
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 | grep -q "200\|307\|308" && break
sleep 2
done
- name: Install Lighthouse CI
run: npm install -g @lhci/cli@0.13.x
- name: Run Lighthouse CI
run: lhci autorun --config=./lighthouserc.json --collect.url=http://localhost:3000/login || true
- name: Assert performance budgets
run: lhci assert --config=./lighthouserc.json

1
.husky/commit-msg Normal file
View File

@@ -0,0 +1 @@
npx --no-install commitlint --edit $1

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

View File

@@ -0,0 +1,273 @@
<h2>课文锚点时间线布局</h2>
<p class="subtitle">课文作为主轴,节点锚定到课文位置,形成教学流程时间线</p>
<div class="mockup">
<div class="mockup-header">备课编辑器 — 课文锚点时间线</div>
<div class="mockup-body" style="padding:0;">
<div style="font-family:monospace;font-size:12px;line-height:1.6;">
<!-- 顶部工具栏 -->
<div style="background:#1e293b;color:#e2e8f0;padding:8px 12px;display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;align-items:center;gap:8px;">
<span>📖</span>
<span style="font-weight:bold;">秋天(第一课时)</span>
<span style="background:#334155;padding:2px 8px;border-radius:3px;font-size:10px;">语文 · 一年级上册 · 第一单元</span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:10px;color:#94a3b8;">💾 已保存 · 2 分钟前</span>
<span style="background:#334155;padding:4px 8px;border-radius:3px;font-size:10px;">📋 版本历史</span>
<span style="background:#3b82f6;padding:4px 12px;border-radius:3px;font-size:10px;">💾 保存</span>
</div>
</div>
<!-- 主体:左课文 + 右节点时间线 -->
<div style="display:grid;grid-template-columns:1fr 320px;gap:0;background:#fff;min-height:480px;">
<!-- 左侧:课文正文区(带锚点 gutter -->
<div style="display:grid;grid-template-columns:32px 1fr;background:#fffbeb;border-right:1px solid #e2e8f0;">
<!-- 锚点 gutter显示锚点标记 -->
<div style="background:#fef3c7;border-right:1px solid #fde68a;position:relative;">
<!-- 锚点标记 -->
<div style="position:absolute;top:60px;left:4px;background:#3b82f6;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">1</div>
<div style="position:absolute;top:140px;left:4px;background:#f59e0b;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">2</div>
<div style="position:absolute;top:200px;left:4px;background:#0ea5e9;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">3</div>
<div style="position:absolute;top:280px;left:4px;background:#ec4899;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">4</div>
<div style="position:absolute;top:360px;left:4px;background:#22c55e;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">5</div>
</div>
<!-- 课文内容 -->
<div style="padding:16px 20px;font-size:13px;color:#78350f;line-height:2;position:relative;">
<div style="font-size:16px;font-weight:bold;color:#92400e;text-align:center;margin-bottom:16px;">秋天</div>
<p style="margin:0 0 12px 0;">
<span style="background:#dbeafe;border-bottom:2px solid #3b82f6;padding:1px 2px;">天气凉了,树叶黄了,</span>
一片片叶子从树上落下来。
</p>
<p style="margin:0 0 12px 0;">
<span style="background:#fef3c7;border-bottom:2px solid #f59e0b;padding:1px 2px;">天空那么蓝,那么高。</span>
一群大雁往南飞,
</p>
<p style="margin:0 0 12px 0;">
<span style="background:#e0f2fe;border-bottom:2px solid #0ea5e9;padding:1px 2px;">一会儿排成个"人"字,</span>
<span style="background:#fce7f3;border-bottom:2px solid #ec4899;padding:1px 2px;">一会儿排成个"一"字。</span>
</p>
<p style="margin:0 0 12px 0;">
<span style="background:#dcfce7;border-bottom:2px solid #22c55e;padding:1px 2px;">啊!秋天来了!</span>
</p>
<!-- 拖放提示 -->
<div style="margin-top:24px;padding:8px;border:1px dashed #cbd5e1;border-radius:4px;text-align:center;font-size:10px;color:#94a3b8;">
💡 选中文字可"关联节点",或从右侧拖动节点到课文某字前
</div>
</div>
</div>
<!-- 右侧:节点时间线 -->
<div style="background:#f8fafc;padding:12px;overflow-y:auto;">
<div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">教学流程时间线</div>
<!-- 已锚定节点(按课文位置排序) -->
<div style="display:flex;flex-direction:column;gap:6px;">
<!-- 节点 1导入锚定到"天气凉了" -->
<div style="background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:4px;padding:8px;cursor:pointer;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">1</span>
<span style="font-size:11px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
</div>
<div style="font-size:10px;color:#64748b;background:#eff6ff;padding:4px 6px;border-radius:3px;">
"天气凉了,树叶黄了" → 提问:你见过秋天的树叶吗?
</div>
</div>
<!-- 节点 2文本研习锚定到"天空那么蓝" -->
<div style="background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:4px;padding:8px;cursor:pointer;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">2</span>
<span style="font-size:11px;font-weight:bold;color:#92400e;">📝 文本研习</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
</div>
<div style="font-size:10px;color:#64748b;background:#fffbeb;padding:4px 6px;border-radius:3px;">
"天空那么蓝,那么高" → 赏析:叠词的运用
</div>
</div>
<!-- 节点 3新授锚定到"人字" -->
<div style="background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:4px;padding:8px;cursor:pointer;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">3</span>
<span style="font-size:11px;font-weight:bold;color:#075985;">📚 新授</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
</div>
<div style="font-size:10px;color:#64748b;background:#f0f9ff;padding:4px 6px;border-radius:3px;">
"一会儿排成个'人'字" → 讲解:大雁南飞
</div>
</div>
<!-- 节点 4练习锚定到"一字" -->
<div style="background:#fff;border:1px solid #ec4899;border-left:3px solid #ec4899;border-radius:4px;padding:8px;cursor:pointer;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="background:#ec4899;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">4</span>
<span style="font-size:11px;font-weight:bold;color:#9f1239;">✏️ 练习</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
</div>
<div style="font-size:10px;color:#64748b;background:#fdf2f8;padding:4px 6px;border-radius:3px;">
"一会儿排成个'一'字" → 3 道题
</div>
</div>
<!-- 节点 5小结锚定到"秋天来了" -->
<div style="background:#fff;border:1px solid #22c55e;border-left:3px solid #22c55e;border-radius:4px;padding:8px;cursor:pointer;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="background:#22c55e;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">5</span>
<span style="font-size:11px;font-weight:bold;color:#166534;">📌 小结</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
</div>
<div style="font-size:10px;color:#64748b;background:#f0fdf4;padding:4px 6px;border-radius:3px;">
"啊!秋天来了!" → 总结全文
</div>
</div>
<!-- 分隔线 -->
<div style="border-top:1px dashed #cbd5e1;margin:8px 0;padding-top:8px;">
<div style="font-size:9px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">未锚定节点</div>
</div>
<!-- 未锚定节点 -->
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:11px;font-weight:bold;color:#1e3a8a;">🎯 教学目标</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">全局</span>
</div>
</div>
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:11px;font-weight:bold;color:#92400e;">⭐ 重难点</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">全局</span>
</div>
</div>
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:11px;font-weight:bold;color:#a855f7;">🏠 作业</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">课后</span>
</div>
</div>
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:11px;font-weight:bold;color:#6366f1;">📋 板书设计</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">全局</span>
</div>
</div>
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:11px;font-weight:bold;color:#64748b;">💭 教学反思</span>
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">课后</span>
</div>
</div>
<!-- 添加节点按钮 -->
<div style="border:1px dashed #94a3b8;border-radius:4px;padding:8px;text-align:center;font-size:10px;color:#64748b;cursor:pointer;margin-top:4px;">
+ 添加节点
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top:24px;">
<h3>核心交互:两种锚定方式</h3>
<div class="split">
<div class="mockup">
<div class="mockup-header">方式 1拖动节点到课文某字前</div>
<div class="mockup-body" style="padding:16px;font-family:monospace;font-size:12px;">
<div style="display:flex;gap:12px;">
<div style="background:#f8fafc;padding:8px;border-radius:4px;">
<div style="font-size:9px;color:#94a3b8;margin-bottom:4px;">右侧节点</div>
<div style="background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;padding:6px;border-radius:3px;cursor:grab;font-size:10px;">💡 导入</div>
</div>
<div style="font-size:18px;color:#94a3b8;align-self:center;"></div>
<div style="background:#fffbeb;padding:8px;border-radius:4px;flex:1;">
<div style="font-size:9px;color:#94a3b8;margin-bottom:4px;">课文</div>
<div style="font-size:11px;color:#78350f;line-height:1.8;">
天气凉了,<span style="background:#dbeafe;border:2px dashed #3b82f6;padding:1px 2px;border-radius:2px;">|</span>树叶黄了,<br>
一片片叶子从树上落下来。
</div>
<div style="font-size:9px;color:#3b82f6;margin-top:4px;">💡 节点锚定到此位置</div>
</div>
</div>
</div>
</div>
<div class="mockup">
<div class="mockup-header">方式 2选中文字 → 关联节点</div>
<div class="mockup-body" style="padding:16px;font-family:monospace;font-size:12px;">
<div style="background:#fffbeb;padding:8px;border-radius:4px;margin-bottom:8px;">
<div style="font-size:9px;color:#94a3b8;margin-bottom:4px;">1. 选中文字</div>
<div style="font-size:11px;color:#78350f;line-height:1.8;">
<span style="background:#fef08a;">天空那么蓝,那么高</span>
</div>
</div>
<div style="font-size:18px;color:#94a3b8;text-align:center;"></div>
<div style="background:#f8fafc;padding:8px;border-radius:4px;margin-top:8px;">
<div style="font-size:9px;color:#94a3b8;margin-bottom:4px;">2. 弹出菜单选择节点</div>
<div style="display:flex;gap:4px;flex-wrap:wrap;">
<span style="background:#fff;border:1px solid #3b82f6;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">💡 导入</span>
<span style="background:#fff;border:1px solid #f59e0b;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">📝 文本研习</span>
<span style="background:#fff;border:1px solid #0ea5e9;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">📚 新授</span>
<span style="background:#fff;border:1px solid #ec4899;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">✏️ 练习</span>
<span style="background:#fff;border:1px solid #22c55e;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">📌 小结</span>
<span style="background:#fff;border:1px dashed #94a3b8;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">+ 新建节点</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top:24px;">
<h3>数据模型锚点Anchor</h3>
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.6;">
<div style="color:#94a3b8;">// 节点锚点 — 记录节点与课文位置的关联</div>
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">NodeAnchor</span> {</div>
<div>&nbsp;&nbsp;nodeId: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 关联的节点 ID</span></div>
<div>&nbsp;&nbsp;type: <span style="color:#10b981;">"point"</span> | <span style="color:#10b981;">"range"</span>; <span style="color:#64748b;">// 点锚点 or 范围锚点</span></div>
<div>&nbsp;&nbsp;start: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// 课文纯文本偏移量(字符)</span></div>
<div>&nbsp;&nbsp;end?: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// range 锚点的结束偏移</span></div>
<div>&nbsp;&nbsp;textPreview?: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 锚定文字预览(便于回显)</span></div>
<div>}</div>
<br>
<div style="color:#94a3b8;">// LessonPlanDocument 扩展</div>
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">LessonPlanDocument</span> {</div>
<div>&nbsp;&nbsp;version: <span style="color:#10b981;">3</span>; <span style="color:#64748b;">// 升级到 v3</span></div>
<div>&nbsp;&nbsp;nodes: <span style="color:#3b82f6;">LessonPlanNode</span>[];</div>
<div>&nbsp;&nbsp;edges: <span style="color:#3b82f6;">LessonPlanEdge</span>[]; <span style="color:#64748b;">// 保留:节点间连线</span></div>
<div>&nbsp;&nbsp;anchors: <span style="color:#3b82f6;">NodeAnchor</span>[]; <span style="color:#64748b;">// 新增:节点与课文的锚点</span></div>
<div>}</div>
</div>
</div>
<div class="section">
<h3>这个设计的优势</h3>
<div class="pros-cons">
<div class="pros">
<h4>优势</h4>
<ul>
<li><strong>教学流程可视化</strong>:节点按课文位置排序,天然形成时间线</li>
<li><strong>节点与课文强关联</strong>:每个节点对应课文的哪部分一目了然</li>
<li><strong>双模式锚定</strong>:拖动(点锚点)+ 选文字(范围锚点)</li>
<li><strong>保留连线能力</strong>:节点间仍可连线(如"导入→新授"流程线)</li>
<li><strong>未锚定节点</strong>:目标/重难点/作业/板书/反思等全局节点不强制锚定</li>
</ul>
</div>
<div class="cons">
<h4>需要注意</h4>
<ul>
<li>课文偏移量需基于纯文本Markdown 渲染后需映射)</li>
<li>课文内容变更后锚点可能失效(需重新定位或提示)</li>
<li>数据结构升级到 v3需迁移现有 v2 数据</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,292 @@
<h2>画布式锚点布局 — 正文固定 + 节点散布 + 连线关联</h2>
<p class="subtitle">保留 React Flow 画布交互,正文为不可移动但可缩放的中央容器,节点通过连线关联正文锚点</p>
<div class="mockup">
<div class="mockup-header">备课编辑器 — 画布视图(默认状态)</div>
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:560px;">
<!-- 顶部工具栏 -->
<div style="background:#1e293b;color:#e2e8f0;padding:8px 12px;display:flex;justify-content:space-between;align-items:center;z-index:10;position:relative;">
<div style="display:flex;align-items:center;gap:8px;">
<span>📖</span>
<span style="font-weight:bold;">秋天(第一课时)</span>
<span style="background:#334155;padding:2px 8px;border-radius:3px;font-size:10px;">语文 · 一年级上册</span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:10px;color:#94a3b8;">💾 已保存</span>
<span style="background:#334155;padding:4px 8px;border-radius:3px;font-size:10px;">📋 版本</span>
<span style="background:#3b82f6;padding:4px 12px;border-radius:3px;font-size:10px;">💾 保存</span>
</div>
</div>
<!-- 画布区域 -->
<div style="position:relative;width:100%;height:520px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
<!-- SVG 连线层(默认 10% 透明度) -->
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0.1;" viewBox="0 0 800 520">
<!-- 节点1(导入) → 正文锚点1 -->
<path d="M 130 120 Q 200 140 280 180" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<circle cx="280" cy="180" r="4" fill="#3b82f6"/>
<!-- 节点2(文本研习) → 正文锚点2 -->
<path d="M 130 220 Q 200 230 280 240" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<circle cx="280" cy="240" r="4" fill="#f59e0b"/>
<!-- 节点3(新授) → 正文锚点3 -->
<path d="M 670 120 Q 600 150 520 200" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<circle cx="520" cy="200" r="4" fill="#0ea5e9"/>
<!-- 节点4(练习) → 正文锚点4 -->
<path d="M 670 220 Q 600 240 520 260" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<circle cx="520" cy="260" r="4" fill="#ec4899"/>
<!-- 节点5(小结) → 正文锚点5 -->
<path d="M 670 340 Q 600 320 520 300" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<circle cx="520" cy="300" r="4" fill="#22c55e"/>
<!-- 节点间连线(教学流程) -->
<path d="M 130 140 L 130 200" stroke="#64748b" stroke-width="1.5" fill="none"/>
<path d="M 670 140 L 670 200" stroke="#64748b" stroke-width="1.5" fill="none"/>
<path d="M 670 240 L 670 320" stroke="#64748b" stroke-width="1.5" fill="none"/>
</svg>
<!-- 中央:正文容器(不可移动,可缩放) -->
<div style="position:absolute;left:280px;top:80px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #fde68a;padding-bottom:6px;">
<span style="font-size:11px;font-weight:bold;color:#92400e;">📜 课文正文</span>
<span style="font-size:9px;color:#94a3b8;background:#fef3c7;padding:1px 4px;border-radius:2px;">🔒 固定</span>
</div>
<div style="font-size:13px;color:#78350f;line-height:1.8;">
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
<p style="margin:0 0 4px 0;">
<span style="background:#dbeafe;border-bottom:2px solid #3b82f6;">天气凉了</span>,树叶黄了,
</p>
<p style="margin:0 0 4px 0;">
<span style="background:#fef3c7;border-bottom:2px solid #f59e0b;">天空那么蓝</span>,那么高。
</p>
<p style="margin:0 0 4px 0;">
一群大雁往南飞,
</p>
<p style="margin:0 0 4px 0;">
一会儿排成个<span style="background:#e0f2fe;border-bottom:2px solid #0ea5e9;">"人"字</span>
</p>
<p style="margin:0 0 4px 0;">
一会儿排成个<span style="background:#fce7f3;border-bottom:2px solid #ec4899;">"一"字</span>
</p>
<p style="margin:0;">
<span style="background:#dcfce7;border-bottom:2px solid #22c55e;">啊!秋天来了!</span>
</p>
</div>
<!-- 缩放控件 -->
<div style="position:absolute;bottom:-12px;right:-12px;background:#fff;border:1px solid #f59e0b;border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;cursor:pointer;box-shadow:0 2px 4px rgba(0,0,0,0.1);">🔍</div>
</div>
<!-- 左侧节点 -->
<!-- 节点1导入 -->
<div style="position:absolute;left:30px;top:90px;width:140px;background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
</div>
<div style="font-size:9px;color:#64748b;">提问:你见过秋天的树叶吗?</div>
<!-- React Flow Handle 标记 -->
<div style="position:absolute;right:-4px;top:50%;width:8px;height:8px;background:#3b82f6;border-radius:50%;border:1px solid #fff;"></div>
</div>
<!-- 节点2文本研习 -->
<div style="position:absolute;left:30px;top:200px;width:140px;background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
</div>
<div style="font-size:9px;color:#64748b;">赏析:叠词的运用</div>
<div style="position:absolute;right:-4px;top:50%;width:8px;height:8px;background:#f59e0b;border-radius:50%;border:1px solid #fff;"></div>
</div>
<!-- 右侧节点 -->
<!-- 节点3新授 -->
<div style="position:absolute;left:630px;top:90px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
</div>
<div style="font-size:9px;color:#64748b;">讲解:大雁南飞</div>
<div style="position:absolute;left:-4px;top:50%;width:8px;height:8px;background:#0ea5e9;border-radius:50%;border:1px solid #fff;"></div>
</div>
<!-- 节点4练习 -->
<div style="position:absolute;left:630px;top:200px;width:140px;background:#fff;border:1px solid #ec4899;border-left:3px solid #ec4899;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#ec4899;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">4</span>
<span style="font-size:10px;font-weight:bold;color:#9f1239;">✏️ 练习</span>
</div>
<div style="font-size:9px;color:#64748b;">3 道题</div>
<div style="position:absolute;left:-4px;top:50%;width:8px;height:8px;background:#ec4899;border-radius:50%;border:1px solid #fff;"></div>
</div>
<!-- 节点5小结 -->
<div style="position:absolute;left:630px;top:320px;width:140px;background:#fff;border:1px solid #22c55e;border-left:3px solid #22c55e;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#22c55e;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">5</span>
<span style="font-size:10px;font-weight:bold;color:#166534;">📌 小结</span>
</div>
<div style="font-size:9px;color:#64748b;">总结全文</div>
<div style="position:absolute;left:-4px;top:50%;width:8px;height:8px;background:#22c55e;border-radius:50%;border:1px solid #fff;"></div>
</div>
<!-- 顶部全局节点(未锚定) -->
<div style="position:absolute;left:30px;top:10px;width:100px;background:#fff;border:1px dashed #3b82f6;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
<div style="font-size:9px;font-weight:bold;color:#1e3a8a;">🎯 教学目标</div>
</div>
<div style="position:absolute;left:140px;top:10px;width:100px;background:#fff;border:1px dashed #f59e0b;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
<div style="font-size:9px;font-weight:bold;color:#92400e;">⭐ 重难点</div>
</div>
<!-- 底部全局节点(未锚定) -->
<div style="position:absolute;left:30px;top:440px;width:100px;background:#fff;border:1px dashed #a855f7;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
<div style="font-size:9px;font-weight:bold;color:#9333ea;">🏠 作业</div>
</div>
<div style="position:absolute;left:140px;top:440px;width:100px;background:#fff;border:1px dashed #6366f1;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
<div style="font-size:9px;font-weight:bold;color:#4f46e5;">📋 板书设计</div>
</div>
<div style="position:absolute;left:250px;top:440px;width:100px;background:#fff;border:1px dashed #64748b;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
<div style="font-size:9px;font-weight:bold;color:#475569;">💭 教学反思</div>
</div>
<!-- React Flow Controls右下角 -->
<div style="position:absolute;bottom:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:4px;display:flex;flex-direction:column;gap:2px;box-shadow:0 2px 4px rgba(0,0,0,0.05);">
<div style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;border-radius:2px;">+</div>
<div style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;border-radius:2px;"></div>
<div style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:10px;border-radius:2px;"></div>
</div>
<!-- 添加节点按钮(左下角) -->
<div style="position:absolute;bottom:12px;left:12px;background:#3b82f6;color:#fff;padding:6px 12px;border-radius:4px;font-size:10px;cursor:pointer;box-shadow:0 2px 4px rgba(59,130,246,0.3);">
+ 添加节点
</div>
<!-- 透明度提示 -->
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:4px 8px;font-size:9px;color:#64748b;">
连线默认 10% 透明度 · 选中节点时完整显示
</div>
</div>
</div>
</div>
</div>
<!-- 选中状态对比 -->
<div class="section" style="margin-top:24px;">
<h3>选中节点时的连线显示对比</h3>
<div class="split">
<div class="mockup">
<div class="mockup-header">默认状态 — 连线 10% 透明度</div>
<div class="mockup-body" style="padding:16px;background:#f1f5f9;">
<svg style="width:100%;height:120px;" viewBox="0 0 300 120">
<path d="M 30 60 Q 120 60 270 60" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
<rect x="10" y="45" width="40" height="30" fill="#fff" stroke="#3b82f6" rx="4"/>
<text x="30" y="64" text-anchor="middle" font-size="10" fill="#1e3a8a">导入</text>
<rect x="250" y="45" width="40" height="30" fill="#fffbeb" stroke="#f59e0b" stroke-width="2" rx="4"/>
<text x="270" y="64" text-anchor="middle" font-size="10" fill="#92400e">课文</text>
</svg>
<div style="text-align:center;font-size:10px;color:#94a3b8;">连线几乎不可见,画布干净</div>
</div>
</div>
<div class="mockup">
<div class="mockup-header">选中"导入"节点 — 连线 100% 显示</div>
<div class="mockup-body" style="padding:16px;background:#f1f5f9;">
<svg style="width:100%;height:120px;" viewBox="0 0 300 120">
<path d="M 30 60 Q 120 60 270 60" stroke="#3b82f6" stroke-width="2.5" fill="none" stroke-dasharray="4 4" opacity="1"/>
<circle cx="270" cy="60" r="5" fill="#3b82f6"/>
<rect x="10" y="45" width="40" height="30" fill="#fff" stroke="#3b82f6" stroke-width="3" rx="4"/>
<text x="30" y="64" text-anchor="middle" font-size="10" fill="#1e3a8a">导入</text>
<rect x="250" y="45" width="40" height="30" fill="#fffbeb" stroke="#f59e0b" stroke-width="2" rx="4"/>
<text x="270" y="64" text-anchor="middle" font-size="10" fill="#92400e">课文</text>
</svg>
<div style="text-align:center;font-size:10px;color:#3b82f6;">连线完整显示,高亮锚点位置</div>
</div>
</div>
</div>
</div>
<!-- 数据模型 -->
<div class="section">
<h3>数据模型设计</h3>
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.6;">
<div style="color:#94a3b8;">// 正文容器节点(特殊节点类型,不可拖动,可缩放)</div>
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">TextbookContentNode</span> <span style="color:#f59e0b;">extends</span> <span style="color:#3b82f6;">LessonPlanNode</span> {</div>
<div>&nbsp;&nbsp;type: <span style="color:#10b981;">"textbook_content"</span>; <span style="color:#64748b;">// 新增节点类型</span></div>
<div>&nbsp;&nbsp;data: {</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;chapterId: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 关联教材章节</span></div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;content: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// Markdown 正文(缓存)</span></div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;zoom: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// 缩放比例 0.5-2.0</span></div>
<div>&nbsp;&nbsp;};</div>
<div>&nbsp;&nbsp;position: { x: <span style="color:#10b981;">number</span>; y: <span style="color:#10b981;">number</span> }; <span style="color:#64748b;">// 固定位置(不可拖动)</span></div>
<div>&nbsp;&nbsp;draggable: <span style="color:#10b981;">false</span>; <span style="color:#64748b;">// React Flow 节点锁定</span></div>
<div>}</div>
<br>
<div style="color:#94a3b8;">// 锚点连线(节点 → 正文位置)</div>
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">AnchorEdge</span> <span style="color:#f59e0b;">extends</span> <span style="color:#3b82f6;">LessonPlanEdge</span> {</div>
<div>&nbsp;&nbsp;type: <span style="color:#10b981;">"anchor"</span>; <span style="color:#64748b;">// 锚点连线vs "flow" 流程连线)</span></div>
<div>&nbsp;&nbsp;source: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 节点 ID</span></div>
<div>&nbsp;&nbsp;target: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 正文节点 ID</span></div>
<div>&nbsp;&nbsp;targetHandle: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// "anchor:123:145"(正文偏移量 start:end</span></div>
<div>}</div>
<br>
<div style="color:#94a3b8;">// LessonPlanDocument v3</div>
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">LessonPlanDocument</span> {</div>
<div>&nbsp;&nbsp;version: <span style="color:#10b981;">3</span>;</div>
<div>&nbsp;&nbsp;nodes: <span style="color:#3b82f6;">LessonPlanNode</span>[]; <span style="color:#64748b;">// 含 1 个 textbook_content + N 个教学节点</span></div>
<div>&nbsp;&nbsp;edges: <span style="color:#3b82f6;">AnchorEdge</span> | <span style="color:#3b82f6;">FlowEdge</span>[]; <span style="color:#64748b;">// 锚点连线 + 流程连线</span></div>
<div>}</div>
</div>
</div>
<!-- 交互流程 -->
<div class="section">
<h3>核心交互流程</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;">
<h4 style="margin:0 0 8px 0;color:#3b82f6;">🔗 锚定节点到正文</h4>
<ol style="margin:0;padding-left:16px;font-size:11px;color:#475569;line-height:1.8;">
<li>教师选中正文某段文字(或某个字)</li>
<li>选中后弹出浮动菜单:"关联节点 →"</li>
<li>从下拉列表选择已有节点,或"新建节点"</li>
<li>创建 AnchorEdgesource=节点, target=正文节点, targetHandle="anchor:start:end"</li>
<li>正文对应文字高亮显示(节点颜色)</li>
<li>连线默认 10% 透明,选中节点时 100%</li>
</ol>
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;">
<h4 style="margin:0 0 8px 0;color:#22c55e;">🖱️ 拖动节点到正文</h4>
<ol style="margin:0;padding-left:16px;font-size:11px;color:#475569;line-height:1.8;">
<li>教师从右侧节点列表拖动一个节点</li>
<li>拖动过程中,正文区域高亮可放置区域</li>
<li>拖到正文某个字前释放</li>
<li>创建点锚点point anchortargetHandle="anchor:pos"</li>
<li>节点自动定位到正文旁边(左或右空位)</li>
<li>连线默认 10% 透明,选中时完整显示</li>
</ol>
</div>
</div>
</div>
<div class="section">
<h3>设计要点</h3>
<div class="pros-cons">
<div class="pros">
<h4>优势</h4>
<ul>
<li><strong>保留画布交互</strong>:缩放/平移/拖动节点,与当前备课模块一致</li>
<li><strong>正文固定居中</strong>:不可拖动,始终是视觉中心</li>
<li><strong>连线语义化</strong>anchor 锚点连线 vs flow 流程连线</li>
<li><strong>透明度策略</strong>:默认 10%,选中时 100%,画布不杂乱</li>
<li><strong>正文可缩放</strong>:教师可放大正文便于阅读</li>
</ul>
</div>
<div class="cons">
<h4>技术挑战</h4>
<ul>
<li>正文偏移量需基于纯文本Markdown 渲染后映射)</li>
<li>正文内容变更后锚点需重新定位</li>
<li>React Flow 自定义节点需处理正文渲染</li>
<li>数据结构升级 v2 → v3需迁移</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,226 @@
<h2>备课模块布局方案对比</h2>
<p class="subtitle">3 种布局方案 — 课文固定中央,教学节点围绕组织</p>
<div class="cards" data-multiselect>
<!-- 方案 A -->
<div class="card" data-choice="a" onclick="toggleSelect(this)">
<div class="card-image" style="padding:12px;background:#f8fafc;">
<div style="font-family:monospace;font-size:11px;line-height:1.4;">
<div style="border:1px solid #cbd5e1;background:#e2e8f0;padding:4px 8px;border-radius:4px 4px 0 0;display:flex;justify-content:space-between;">
<span>📖 秋天(第一课时)</span>
<span>💾 已保存</span>
</div>
<div style="display:grid;grid-template-columns:200px 1fr 200px;gap:4px;padding:8px;background:#fff;border:1px solid #cbd5e1;border-top:none;border-radius:0 0 4px 4px;min-height:280px;">
<!-- 左侧:课前 -->
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="font-size:9px;color:#64748b;text-align:center;">课前</div>
<div style="background:#dbeafe;border:1px solid #3b82f6;padding:4px;border-radius:3px;font-size:10px;">🎯 教学目标</div>
<div style="background:#fef3c7;border:1px solid #f59e0b;padding:4px;border-radius:3px;font-size:10px;">⭐ 重难点</div>
<div style="background:#e0f2fe;border:1px solid #0ea5e9;padding:4px;border-radius:3px;font-size:10px;">💡 导入</div>
<div style="background:#fce7f3;border:1px solid #ec4899;padding:4px;border-radius:3px;font-size:10px;">📝 文本研习</div>
</div>
<!-- 中央:课文 -->
<div style="background:#fffbeb;border:2px solid #f59e0b;padding:8px;border-radius:4px;display:flex;flex-direction:column;">
<div style="font-size:10px;color:#92400e;font-weight:bold;margin-bottom:4px;">📜 课文正文</div>
<div style="font-size:9px;color:#78350f;line-height:1.5;flex:1;">
天气凉了,树叶黄了,<br>
一片片叶子从树上落下来。<br>
<span style="background:#fef08a;">天空那么蓝,那么高</span><br>
一群大雁往南飞,<br>
一会儿排成个"人"字,<br>
一会儿排成个"一"字。<br>
<span style="background:#bbf7d0;">啊!秋天来了!</span>
</div>
<div style="font-size:8px;color:#92400e;margin-top:4px;">💡 选中文字可添加批注</div>
</div>
<!-- 右侧:课中/课后 -->
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="font-size:9px;color:#64748b;text-align:center;">课中</div>
<div style="background:#dcfce7;border:1px solid #22c55e;padding:4px;border-radius:3px;font-size:10px;">📚 新授</div>
<div style="background:#ede9fe;border:1px solid #8b5cf6;padding:4px;border-radius:3px;font-size:10px;">✏️ 练习</div>
<div style="background:#fee2e2;border:1px solid #ef4444;padding:4px;border-radius:3px;font-size:10px;">📌 小结</div>
<div style="font-size:9px;color:#64748b;text-align:center;margin-top:4px;">课后</div>
<div style="background:#f3e8ff;border:1px solid #a855f7;padding:4px;border-radius:3px;font-size:10px;">🏠 作业</div>
<div style="background:#e0e7ff;border:1px solid #6366f1;padding:4px;border-radius:3px;font-size:10px;">📋 板书设计</div>
<div style="background:#f1f5f9;border:1px solid #64748b;padding:4px;border-radius:3px;font-size:10px;">💭 教学反思</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>A. 三栏布局(课前/课文/课后)</h3>
<p>课文固定中央(琥珀色边框),左侧"课前"节点(目标/重难点/导入/文本研习),右侧"课中+课后"节点(新授/练习/小结/作业/板书/反思)。节点按教学流程纵向排列。点击节点在右侧抽屉编辑。</p>
</div>
</div>
<!-- 方案 B -->
<div class="card" data-choice="b" onclick="toggleSelect(this)">
<div class="card-image" style="padding:12px;background:#f8fafc;">
<div style="font-family:monospace;font-size:11px;line-height:1.4;">
<div style="border:1px solid #cbd5e1;background:#e2e8f0;padding:4px 8px;border-radius:4px 4px 0 0;display:flex;justify-content:space-between;">
<span>📖 秋天(第一课时)</span>
<span>💾 已保存</span>
</div>
<div style="padding:8px;background:#fff;border:1px solid #cbd5e1;border-top:none;border-radius:0 0 4px 4px;min-height:280px;">
<!-- 顶部:目标/重难点 -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:4px;">
<div style="background:#dbeafe;border:1px solid #3b82f6;padding:4px;border-radius:3px;font-size:10px;text-align:center;">🎯 教学目标</div>
<div style="background:#fef3c7;border:1px solid #f59e0b;padding:4px;border-radius:3px;font-size:10px;text-align:center;">⭐ 重难点</div>
</div>
<!-- 中央:课文 + 左右两侧节点 -->
<div style="display:grid;grid-template-columns:120px 1fr 120px;gap:4px;margin-bottom:4px;">
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="background:#e0f2fe;border:1px solid #0ea5e9;padding:4px;border-radius:3px;font-size:9px;text-align:center;">💡 导入</div>
<div style="background:#fce7f3;border:1px solid #ec4899;padding:4px;border-radius:3px;font-size:9px;text-align:center;">📝 文本研习</div>
</div>
<div style="background:#fffbeb;border:2px solid #f59e0b;padding:8px;border-radius:4px;text-align:center;">
<div style="font-size:10px;color:#92400e;font-weight:bold;">📜 课文正文</div>
<div style="font-size:9px;color:#78350f;margin-top:4px;">天气凉了,树叶黄了...<br>天空那么蓝...<br>一群大雁往南飞...</div>
</div>
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="background:#dcfce7;border:1px solid #22c55e;padding:4px;border-radius:3px;font-size:9px;text-align:center;">📚 新授</div>
<div style="background:#ede9fe;border:1px solid #8b5cf6;padding:4px;border-radius:3px;font-size:9px;text-align:center;">✏️ 练习</div>
</div>
</div>
<!-- 底部:小结/作业/板书/反思 -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px;">
<div style="background:#fee2e2;border:1px solid #ef4444;padding:4px;border-radius:3px;font-size:9px;text-align:center;">📌 小结</div>
<div style="background:#f3e8ff;border:1px solid #a855f7;padding:4px;border-radius:3px;font-size:9px;text-align:center;">🏠 作业</div>
<div style="background:#e0e7ff;border:1px solid #6366f1;padding:4px;border-radius:3px;font-size:9px;text-align:center;">📋 板书</div>
<div style="background:#f1f5f9;border:1px solid #64748b;padding:4px;border-radius:3px;font-size:9px;text-align:center;">💭 反思</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>B. 上下分区布局</h3>
<p>顶部目标/重难点横排,中央课文 + 左右导入/文本研习/新授/练习,底部小结/作业/板书/反思横排。按"目标→导入→课文→新授→小结"的阅读顺序自然流动。视觉层次更清晰。</p>
</div>
</div>
<!-- 方案 C -->
<div class="card" data-choice="c" onclick="toggleSelect(this)">
<div class="card-image" style="padding:12px;background:#f8fafc;">
<div style="font-family:monospace;font-size:11px;line-height:1.4;">
<div style="border:1px solid #cbd5e1;background:#e2e8f0;padding:4px 8px;border-radius:4px 4px 0 0;display:flex;justify-content:space-between;">
<span>📖 秋天(第一课时)</span>
<span>💾 已保存</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;padding:8px;background:#fff;border:1px solid #cbd5e1;border-top:none;border-radius:0 0 4px 4px;min-height:280px;">
<!-- 左侧:课文 -->
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="background:#fffbeb;border:2px solid #f59e0b;padding:8px;border-radius:4px;flex:1;">
<div style="font-size:10px;color:#92400e;font-weight:bold;">📜 课文正文</div>
<div style="font-size:9px;color:#78350f;margin-top:4px;line-height:1.5;">
天气凉了,树叶黄了,<br>
一片片叶子从树上落下来。<br>
<span style="background:#fef08a;">天空那么蓝,那么高</span><br>
一群大雁往南飞...
</div>
</div>
<div style="background:#fce7f3;border:1px solid #ec4899;padding:4px;border-radius:3px;font-size:10px;text-align:center;">📝 文本研习(批注)</div>
</div>
<!-- 右侧:教学流程时间线 -->
<div style="display:flex;flex-direction:column;gap:3px;">
<div style="font-size:9px;color:#64748b;text-align:center;">教学流程</div>
<div style="background:#dbeafe;border:1px solid #3b82f6;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">1</span>
🎯 教学目标
</div>
<div style="background:#fef3c7;border:1px solid #f59e0b;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">2</span>
⭐ 重难点
</div>
<div style="background:#e0f2fe;border:1px solid #0ea5e9;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">3</span>
💡 导入
</div>
<div style="background:#dcfce7;border:1px solid #22c55e;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#22c55e;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">4</span>
📚 新授
</div>
<div style="background:#ede9fe;border:1px solid #8b5cf6;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#8b5cf6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">5</span>
✏️ 练习
</div>
<div style="background:#fee2e2;border:1px solid #ef4444;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#ef4444;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">6</span>
📌 小结
</div>
<div style="background:#f3e8ff;border:1px solid #a855f7;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#a855f7;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">7</span>
🏠 作业
</div>
<div style="background:#e0e7ff;border:1px solid #6366f1;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#6366f1;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">8</span>
📋 板书
</div>
<div style="background:#f1f5f9;border:1px solid #64748b;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
<span style="background:#64748b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">9</span>
💭 反思
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>C. 左课文 + 右时间线</h3>
<p>左侧课文正文(固定)+ 文本研习批注,右侧教学流程时间线(编号 1-9 按顺序)。点击时间线节点展开编辑抽屉。最贴近传统教案本格式,结构清晰。</p>
</div>
</div>
</div>
<div class="section" style="margin-top:24px;">
<h3>三种方案的核心差异</h3>
<div class="pros-cons">
<div class="pros">
<h4>方案 A 三栏</h4>
<ul>
<li>课文始终居中可见</li>
<li>课前/课后分区直观</li>
<li>节点可拖动微调位置</li>
</ul>
</div>
<div class="cons">
<h4>方案 A 三栏</h4>
<ul>
<li>三栏可能拥挤(小屏)</li>
<li>教学流程顺序不够明显</li>
</ul>
</div>
</div>
<div class="pros-cons">
<div class="pros">
<h4>方案 B 上下分区</h4>
<ul>
<li>视觉层次最清晰</li>
<li>阅读顺序自然(上→下)</li>
<li>课文居中突出</li>
</ul>
</div>
<div class="cons">
<h4>方案 B 上下分区</h4>
<ul>
<li>节点位置较固定</li>
<li>纵向空间需求大</li>
</ul>
</div>
</div>
<div class="pros-cons">
<div class="pros">
<h4>方案 C 左课文+右时间线</h4>
<ul>
<li>最接近传统教案</li>
<li>教学流程顺序最明确</li>
<li>课文阅读体验最佳</li>
</ul>
</div>
<div class="cons">
<h4>方案 C 左课文+右时间线</h4>
<ul>
<li>节点画布感弱(更像列表)</li>
<li>失去节点图连线能力</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,251 @@
<h2>正文占位符标记布局</h2>
<p class="subtitle">正文中嵌入占位符标记(特殊符号),默认接近透明,选中节点时完整显示</p>
<div class="mockup">
<div class="mockup-header">备课编辑器 — 默认状态(占位符 10% 透明度)</div>
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:560px;">
<!-- 顶部工具栏 -->
<div style="background:#1e293b;color:#e2e8f0;padding:8px 12px;display:flex;justify-content:space-between;align-items:center;z-index:10;position:relative;">
<div style="display:flex;align-items:center;gap:8px;">
<span>📖</span>
<span style="font-weight:bold;">秋天(第一课时)</span>
<span style="background:#334155;padding:2px 8px;border-radius:3px;font-size:10px;">语文 · 一年级上册</span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:10px;color:#94a3b8;">💾 已保存</span>
<span style="background:#3b82f6;padding:4px 12px;border-radius:3px;font-size:10px;">💾 保存</span>
</div>
</div>
<!-- 画布区域 -->
<div style="position:relative;width:100%;height:520px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
<!-- SVG 连线层(默认 10% 透明度) -->
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0.1;" viewBox="0 0 800 520">
<path d="M 130 120 Q 200 140 290 175" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<path d="M 130 220 Q 200 230 290 215" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<path d="M 670 120 Q 600 150 510 195" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<path d="M 670 220 Q 600 240 510 235" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<path d="M 670 340 Q 600 320 510 275" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
</svg>
<!-- 中央:正文容器(不可移动) -->
<div style="position:absolute;left:280px;top:60px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #fde68a;padding-bottom:6px;">
<span style="font-size:11px;font-weight:bold;color:#92400e;">📜 课文正文</span>
<span style="font-size:9px;color:#94a3b8;background:#fef3c7;padding:1px 4px;border-radius:2px;">🔒 固定</span>
</div>
<div style="font-size:13px;color:#78350f;line-height:2;">
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
<p style="margin:0 0 6px 0;">
<!-- 占位符 1导入节点- 默认 10% 透明度 -->
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-right:2px;"></span>天气凉了,树叶黄了,
</p>
<p style="margin:0 0 6px 0;">
<!-- 占位符 2文本研习- 默认 10% 透明度 -->
<span style="display:inline-block;background:#f59e0b;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-right:2px;"></span>天空那么蓝,那么高。
</p>
<p style="margin:0 0 6px 0;">
一群大雁往南飞,
</p>
<p style="margin:0 0 6px 0;">
一会儿排成个"人"字<span style="display:inline-block;background:#0ea5e9;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-left:2px;"></span>
</p>
<p style="margin:0 0 6px 0;">
一会儿排成个"一"字<span style="display:inline-block;background:#ec4899;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-left:2px;"></span>
</p>
<p style="margin:0;">
<span style="display:inline-block;background:#22c55e;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-right:2px;"></span>啊!秋天来了!
</p>
</div>
</div>
<!-- 左侧节点 -->
<div style="position:absolute;left:30px;top:90px;width:140px;background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
</div>
<div style="font-size:9px;color:#64748b;">提问:你见过秋天的树叶吗?</div>
</div>
<div style="position:absolute;left:30px;top:200px;width:140px;background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
</div>
<div style="font-size:9px;color:#64748b;">赏析:叠词的运用</div>
</div>
<!-- 右侧节点 -->
<div style="position:absolute;left:630px;top:90px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
</div>
<div style="font-size:9px;color:#64748b;">讲解:大雁南飞</div>
</div>
<div style="position:absolute;left:630px;top:200px;width:140px;background:#fff;border:1px solid #ec4899;border-left:3px solid #ec4899;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#ec4899;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">4</span>
<span style="font-size:10px;font-weight:bold;color:#9f1239;">✏️ 练习</span>
</div>
<div style="font-size:9px;color:#64748b;">3 道题</div>
</div>
<div style="position:absolute;left:630px;top:320px;width:140px;background:#fff;border:1px solid #22c55e;border-left:3px solid #22c55e;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#22c55e;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">5</span>
<span style="font-size:10px;font-weight:bold;color:#166534;">📌 小结</span>
</div>
<div style="font-size:9px;color:#64748b;">总结全文</div>
</div>
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:4px 8px;font-size:9px;color:#64748b;">
占位符默认 10% · 选中节点时 100% 显示
</div>
</div>
</div>
</div>
</div>
<!-- 选中状态对比 -->
<div class="section" style="margin-top:24px;">
<h3>选中"导入"节点时的状态变化</h3>
<div class="split">
<div class="mockup">
<div class="mockup-header">默认 — 占位符 ① 10% 透明度</div>
<div class="mockup-body" style="padding:16px;background:#fffbeb;">
<div style="font-size:14px;color:#78350f;line-height:2;font-family:serif;">
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:0 5px;font-size:10px;font-weight:bold;opacity:0.1;margin-right:3px;"></span>天气凉了,树叶黄了,
</div>
<div style="margin-top:12px;font-size:10px;color:#94a3b8;text-align:center;">
占位符几乎不可见 · 画布干净
</div>
</div>
</div>
<div class="mockup">
<div class="mockup-header">选中"导入"节点 — 占位符 ① 100% + 连线显示</div>
<div class="mockup-body" style="padding:16px;background:#fffbeb;">
<div style="font-size:14px;color:#78350f;line-height:2;font-family:serif;">
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:0 5px;font-size:10px;font-weight:bold;opacity:1;margin-right:3px;box-shadow:0 0 0 2px #3b82f633;"></span>天气凉了,树叶黄了,
</div>
<div style="margin-top:12px;font-size:10px;color:#3b82f6;text-align:center;">
占位符完整显示 · 连线高亮 · 锚定位置清晰
</div>
</div>
</div>
</div>
</div>
<!-- 占位符样式选项 -->
<div class="section">
<h3>占位符样式选项</h3>
<p class="subtitle">选择占位符在正文中的视觉呈现方式</p>
<div class="cards" data-multiselect>
<div class="card" data-choice="number" onclick="toggleSelect(this)">
<div class="card-image" style="padding:20px;background:#fffbeb;text-align:center;">
<div style="font-size:16px;color:#78350f;font-family:serif;line-height:2;">
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:0 6px;font-size:11px;font-weight:bold;"></span>天气凉了
</div>
</div>
<div class="card-body">
<h3>数字圆圈</h3>
<p>①②③④⑤ — 与节点编号对应,简洁清晰</p>
</div>
</div>
<div class="card" data-choice="icon" onclick="toggleSelect(this)">
<div class="card-image" style="padding:20px;background:#fffbeb;text-align:center;">
<div style="font-size:16px;color:#78350f;font-family:serif;line-height:2;">
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:1px 5px;font-size:11px;">💡</span>天气凉了
</div>
</div>
<div class="card-body">
<h3>节点图标</h3>
<p>💡📝📚✏️📌 — 与节点类型图标一致,直观</p>
</div>
</div>
<div class="card" data-choice="dot" onclick="toggleSelect(this)">
<div class="card-image" style="padding:20px;background:#fffbeb;text-align:center;">
<div style="font-size:16px;color:#78350f;font-family:serif;line-height:2;">
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:50%;width:10px;height:10px;font-size:8px;text-align:center;line-height:10px;"></span>天气凉了
</div>
</div>
<div class="card-body">
<h3>彩色圆点</h3>
<p>● — 极简,颜色对应节点,不干扰阅读</p>
</div>
</div>
<div class="card" data-choice="bracket" onclick="toggleSelect(this)">
<div class="card-image" style="padding:20px;background:#fffbeb;text-align:center;">
<div style="font-size:16px;color:#78350f;font-family:serif;line-height:2;">
<span style="color:#3b82f6;font-weight:bold;">【1】</span>天气凉了
</div>
</div>
<div class="card-body">
<h3>方括号编号</h3>
<p>【1】【2】【3】— 类似脚注标记,学术感</p>
</div>
</div>
</div>
</div>
<!-- 数据模型更新 -->
<div class="section">
<h3>占位符数据模型</h3>
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.6;">
<div style="color:#94a3b8;">// 正文中的占位符标记</div>
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">ContentPlaceholder</span> {</div>
<div>&nbsp;&nbsp;id: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 占位符 ID</span></div>
<div>&nbsp;&nbsp;nodeId: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 关联的节点 ID</span></div>
<div>&nbsp;&nbsp;offset: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// 在正文纯文本中的字符偏移量</span></div>
<div>&nbsp;&nbsp;label: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 显示的标记("①" / "💡" / "●" / "【1】"</span></div>
<div>&nbsp;&nbsp;color: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 节点颜色(用于占位符背景)</span></div>
<div>}</div>
<br>
<div style="color:#94a3b8;">// 正文渲染时注入占位符</div>
<div><span style="color:#f59e0b;">function</span> <span style="color:#3b82f6;">renderContentWithPlaceholders</span>(</div>
<div>&nbsp;&nbsp;content: <span style="color:#10b981;">string</span>, <span style="color:#64748b;">// Markdown 原文</span></div>
<div>&nbsp;&nbsp;placeholders: <span style="color:#3b82f6;">ContentPlaceholder</span>[]</div>
<div>): <span style="color:#10b981;">string</span> {</div>
<div>&nbsp;&nbsp;<span style="color:#64748b;">// 按 offset 排序,在对应位置插入占位符标记</span></div>
<div>&nbsp;&nbsp;<span style="color:#64748b;">// 渲染为 &lt;span class="placeholder" data-node-id="xxx"&gt;&lt;/span&gt;</span></div>
<div>}</div>
<br>
<div style="color:#94a3b8;">// CSS 透明度控制</div>
<div>.placeholder { <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0.1</span>; <span style="color:#10b981;">transition</span>: <span style="color:#f59e0b;">opacity 0.2s</span>; }</div>
<div>.placeholder.active { <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">1</span>; }</div>
<div>.placeholder:hover { <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0.6</span>; }</div>
</div>
</div>
<div class="section">
<h3>交互流程</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;">
<h4 style="margin:0 0 8px 0;color:#3b82f6;">🔗 添加占位符</h4>
<ol style="margin:0;padding-left:16px;font-size:11px;color:#475569;line-height:1.8;">
<li>教师点击正文某个位置(光标位置)</li>
<li>或选中一段文字后释放</li>
<li>弹出菜单:"在此处添加节点 →"</li>
<li>选择节点类型或已有节点</li>
<li>在正文对应位置插入占位符标记</li>
<li>创建 AnchorEdge 连线</li>
</ol>
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;">
<h4 style="margin:0 0 8px 0;color:#22c55e;">👁️ 选中节点时的视觉反馈</h4>
<ol style="margin:0;padding-left:16px;font-size:11px;color:#475569;line-height:1.8;">
<li>点击画布上的某个节点</li>
<li>该节点对应的占位符 opacity 从 0.1 → 1</li>
<li>连线从 10% → 100% 显示</li>
<li>占位符添加 active 样式(边框/阴影)</li>
<li>其他占位符保持 10% 透明度</li>
<li>点击空白处恢复默认状态</li>
</ol>
</div>
</div>
</div>

View File

@@ -0,0 +1,376 @@
<h2>两种锚定方式的视觉规则</h2>
<p class="subtitle">范围锚定选文本vs 点锚定(插入占位符)— 默认/选中状态对比</p>
<!-- 规则总览 -->
<div class="section">
<h3>视觉规则总览</h3>
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.8;">
<div style="color:#94a3b8;">// 两种锚定方式</div>
<div><span style="color:#f59e0b;">type</span> <span style="color:#3b82f6;">AnchorType</span> = <span style="color:#10b981;">"range"</span> | <span style="color:#10b981;">"point"</span>;</div>
<br>
<div style="color:#94a3b8;">// 范围锚定(选一段文本关联节点)</div>
<div><span style="color:#94a3b8;">// 文本背景色 = 节点颜色</span></div>
<div>.range-anchor {</div>
<div>&nbsp;&nbsp;<span style="color:#10b981;">background-color</span>: <span style="color:#f59e0b;">var(--node-color)</span>; <span style="color:#64748b;">// 节点颜色</span></div>
<div>&nbsp;&nbsp;<span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0</span>; <span style="color:#64748b;">// 默认完全透明(和正常文本一样)</span></div>
<div>}</div>
<div>.range-anchor.active {</div>
<div>&nbsp;&nbsp;<span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0.3</span>; <span style="color:#64748b;">// 选中节点时显示背景色</span></div>
<div>}</div>
<br>
<div style="color:#94a3b8;">// 点锚定(在文本中插入占位符)</div>
<div>.point-anchor {</div>
<div>&nbsp;&nbsp;<span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0.3</span>; <span style="color:#64748b;">// 默认半透明</span></div>
<div>}</div>
<div>.point-anchor.active {</div>
<div>&nbsp;&nbsp;<span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">1</span>; <span style="color:#64748b;">// 选中节点时不透明</span></div>
<div>}</div>
</div>
</div>
<!-- 完整画布:默认状态 -->
<div class="section">
<h3>完整画布 — 默认状态</h3>
<div class="mockup">
<div class="mockup-header">默认状态(未选中任何节点)</div>
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:480px;">
<!-- 画布 -->
<div style="position:relative;width:100%;height:480px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
<!-- SVG 连线层(默认 10% 透明度) -->
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0.1;" viewBox="0 0 800 480">
<path d="M 130 100 Q 200 120 290 155" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<path d="M 130 200 Q 200 210 290 195" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<path d="M 670 100 Q 600 130 510 175" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<path d="M 670 200 Q 600 220 510 215" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
<path d="M 670 320 Q 600 300 510 255" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
</svg>
<!-- 中央:正文容器 -->
<div style="position:absolute;left:280px;top:40px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #fde68a;padding-bottom:6px;">
<span style="font-size:11px;font-weight:bold;color:#92400e;">📜 课文正文</span>
<span style="font-size:9px;color:#94a3b8;background:#fef3c7;padding:1px 4px;border-radius:2px;">🔒 固定</span>
</div>
<div style="font-size:13px;color:#78350f;line-height:2.2;">
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
<p style="margin:0 0 6px 0;">
<!-- 范围锚定:默认 opacity:0完全透明和正常文本一样 -->
<span style="background:#3b82f6;opacity:0;color:#78350f;">天气凉了</span>,树叶黄了,
</p>
<p style="margin:0 0 6px 0;">
<!-- 点锚定:默认 opacity:0.3(半透明) -->
<span style="display:inline-block;background:#f59e0b;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;"></span>天空那么蓝,那么高。
</p>
<p style="margin:0 0 6px 0;">
一群大雁往南飞,
</p>
<p style="margin:0 0 6px 0;">
<!-- 范围锚定:默认 opacity:0 -->
一会儿排成个<span style="background:#0ea5e9;opacity:0;color:#78350f;">"人"字</span>
<!-- 点锚定:默认 opacity:0.3 -->
<span style="display:inline-block;background:#ec4899;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-left:2px;"></span>
</p>
<p style="margin:0;">
<!-- 点锚定:默认 opacity:0.3 -->
<span style="display:inline-block;background:#22c55e;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;"></span>啊!秋天来了!
</p>
</div>
</div>
<!-- 左侧节点 -->
<div style="position:absolute;left:30px;top:70px;width:140px;background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#eff6ff;padding:1px 4px;border-radius:2px;">范围</span>
</div>
<div style="font-size:9px;color:#64748b;">提问:你见过秋天的树叶吗?</div>
</div>
<div style="position:absolute;left:30px;top:180px;width:140px;background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#fffbeb;padding:1px 4px;border-radius:2px;"></span>
</div>
<div style="font-size:9px;color:#64748b;">赏析:叠词的运用</div>
</div>
<!-- 右侧节点 -->
<div style="position:absolute;left:630px;top:70px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#f0f9ff;padding:1px 4px;border-radius:2px;">范围</span>
</div>
<div style="font-size:9px;color:#64748b;">讲解:大雁南飞</div>
</div>
<div style="position:absolute;left:630px;top:180px;width:140px;background:#fff;border:1px solid #ec4899;border-left:3px solid #ec4899;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#ec4899;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">4</span>
<span style="font-size:10px;font-weight:bold;color:#9f1239;">✏️ 练习</span>
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#fdf2f8;padding:1px 4px;border-radius:2px;"></span>
</div>
<div style="font-size:9px;color:#64748b;">3 道题</div>
</div>
<div style="position:absolute;left:630px;top:300px;width:140px;background:#fff;border:1px solid #22c55e;border-left:3px solid #22c55e;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#22c55e;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">5</span>
<span style="font-size:10px;font-weight:bold;color:#166534;">📌 小结</span>
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#f0fdf4;padding:1px 4px;border-radius:2px;"></span>
</div>
<div style="font-size:9px;color:#64748b;">总结全文</div>
</div>
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:6px 10px;font-size:9px;color:#64748b;">
<div>🔵 范围锚定:默认 opacity:0</div>
<div>🔴 点锚定:默认 opacity:0.3</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 选中节点 1范围锚定的状态 -->
<div class="section">
<h3>选中"导入"节点(范围锚定)— 文本背景显示</h3>
<div class="mockup">
<div class="mockup-header">选中节点 1 — "天气凉了"背景色显示</div>
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:320px;">
<div style="position:relative;width:100%;height:320px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
<!-- SVG 连线层(选中节点的连线 100% 显示) -->
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;" viewBox="0 0 800 320">
<!-- 选中节点的连线 100% 显示 -->
<path d="M 130 80 Q 200 100 290 135" stroke="#3b82f6" stroke-width="2.5" fill="none" stroke-dasharray="4 4" opacity="1"/>
<circle cx="290" cy="135" r="5" fill="#3b82f6"/>
<!-- 其他连线保持 10% -->
<path d="M 130 180 Q 200 190 290 175" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
<path d="M 670 80 Q 600 110 510 155" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
<path d="M 670 180 Q 600 200 510 195" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
<path d="M 670 280 Q 600 260 510 235" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
</svg>
<!-- 正文容器 -->
<div style="position:absolute;left:280px;top:20px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
<div style="font-size:13px;color:#78350f;line-height:2.2;">
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
<p style="margin:0 0 6px 0;">
<!-- 范围锚定:选中时 opacity:0.3(背景色显示) -->
<span style="background:#3b82f6;opacity:0.3;color:#78350f;border-radius:2px;">天气凉了</span>,树叶黄了,
</p>
<p style="margin:0 0 6px 0;">
<!-- 点锚定:未选中,保持 0.3 -->
<span style="display:inline-block;background:#f59e0b;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;"></span>天空那么蓝,那么高。
</p>
<p style="margin:0 0 6px 0;">一群大雁往南飞,</p>
<p style="margin:0 0 6px 0;">
一会儿排成个<span style="background:#0ea5e9;opacity:0;color:#78350f;">"人"字</span>
<span style="display:inline-block;background:#ec4899;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-left:2px;"></span>
</p>
<p style="margin:0;">
<span style="display:inline-block;background:#22c55e;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;"></span>啊!秋天来了!
</p>
</div>
</div>
<!-- 选中的节点 1高亮边框 -->
<div style="position:absolute;left:30px;top:50px;width:140px;background:#fff;border:2px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 0 0 3px #3b82f633,0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
<span style="margin-left:auto;font-size:8px;color:#fff;background:#3b82f6;padding:1px 4px;border-radius:2px;">范围·选中</span>
</div>
<div style="font-size:9px;color:#64748b;">提问:你见过秋天的树叶吗?</div>
</div>
<!-- 其他节点(正常状态) -->
<div style="position:absolute;left:30px;top:160px;width:140px;background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);opacity:0.7;">
<div style="display:flex;align-items:center;gap:4px;">
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
</div>
</div>
<div style="position:absolute;left:630px;top:50px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);opacity:0.7;">
<div style="display:flex;align-items:center;gap:4px;">
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
</div>
</div>
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #3b82f6;border-radius:4px;padding:6px 10px;font-size:9px;color:#3b82f6;">
✅ 选中"导入"节点 → "天气凉了"背景显示
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 选中节点 2点锚定的状态 -->
<div class="section">
<h3>选中"文本研习"节点(点锚定)— 占位符不透明</h3>
<div class="mockup">
<div class="mockup-header">选中节点 2 — 占位符 ② 100% 显示</div>
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:320px;">
<div style="position:relative;width:100%;height:320px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;" viewBox="0 0 800 320">
<path d="M 130 80 Q 200 100 290 135" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
<!-- 选中节点的连线 100% -->
<path d="M 130 180 Q 200 190 290 175" stroke="#f59e0b" stroke-width="2.5" fill="none" stroke-dasharray="4 4" opacity="1"/>
<circle cx="290" cy="175" r="5" fill="#f59e0b"/>
<path d="M 670 80 Q 600 110 510 155" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
<path d="M 670 180 Q 600 200 510 195" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
<path d="M 670 280 Q 600 260 510 235" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
</svg>
<div style="position:absolute;left:280px;top:20px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
<div style="font-size:13px;color:#78350f;line-height:2.2;">
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
<p style="margin:0 0 6px 0;">
<!-- 范围锚定未选中opacity:0 -->
<span style="background:#3b82f6;opacity:0;color:#78350f;">天气凉了</span>,树叶黄了,
</p>
<p style="margin:0 0 6px 0;">
<!-- 点锚定:选中时 opacity:1不透明 -->
<span style="display:inline-block;background:#f59e0b;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:1;margin-right:2px;box-shadow:0 0 0 2px #f59e0b44;"></span>天空那么蓝,那么高。
</p>
<p style="margin:0 0 6px 0;">一群大雁往南飞,</p>
<p style="margin:0 0 6px 0;">
一会儿排成个<span style="background:#0ea5e9;opacity:0;color:#78350f;">"人"字</span>
<span style="display:inline-block;background:#ec4899;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-left:2px;"></span>
</p>
<p style="margin:0;">
<span style="display:inline-block;background:#22c55e;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;"></span>啊!秋天来了!
</p>
</div>
</div>
<div style="position:absolute;left:30px;top:50px;width:140px;background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);opacity:0.7;">
<div style="display:flex;align-items:center;gap:4px;">
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
</div>
</div>
<!-- 选中的节点 2高亮边框 -->
<div style="position:absolute;left:30px;top:160px;width:140px;background:#fff;border:2px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 0 0 3px #f59e0b33,0 2px 6px rgba(0,0,0,0.08);">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
<span style="margin-left:auto;font-size:8px;color:#fff;background:#f59e0b;padding:1px 4px;border-radius:2px;">点·选中</span>
</div>
<div style="font-size:9px;color:#64748b;">赏析:叠词的运用</div>
</div>
<div style="position:absolute;left:630px;top:50px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);opacity:0.7;">
<div style="display:flex;align-items:center;gap:4px;">
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
</div>
</div>
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #f59e0b;border-radius:4px;padding:6px 10px;font-size:9px;color:#f59e0b;">
✅ 选中"文本研习"节点 → 占位符 ② 不透明显示
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 两种锚定方式对比表 -->
<div class="section">
<h3>两种锚定方式对比</h3>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="background:#1e293b;color:#e2e8f0;">
<th style="padding:8px 12px;text-align:left;border:1px solid #334155;">特性</th>
<th style="padding:8px 12px;text-align:left;border:1px solid #334155;">范围锚定(选文本)</th>
<th style="padding:8px 12px;text-align:left;border:1px solid #334155;">点锚定(插入占位符)</th>
</tr>
</thead>
<tbody>
<tr style="background:#fff;">
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">触发方式</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">选中一段文字 → 关联节点</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">点击文本某位置 → 插入占位符</td>
</tr>
<tr style="background:#f8fafc;">
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">视觉表现</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">文本背景色 = 节点颜色</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">插入标记符号(①②③)</td>
</tr>
<tr style="background:#fff;">
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">默认透明度</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">
<span style="background:#3b82f6;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 0</span>
<span style="color:#64748b;font-size:10px;">(完全透明,和正常文本一样)</span>
</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">
<span style="background:#f59e0b;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 0.3</span>
<span style="color:#64748b;font-size:10px;">(半透明,隐约可见)</span>
</td>
</tr>
<tr style="background:#f8fafc;">
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">选中时透明度</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">
<span style="background:#3b82f6;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 0.3</span>
<span style="color:#64748b;font-size:10px;">(背景色显示)</span>
</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">
<span style="background:#f59e0b;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 1</span>
<span style="color:#64748b;font-size:10px;">(不透明,完整显示)</span>
</td>
</tr>
<tr style="background:#fff;">
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">连线透明度</td>
<td colspan="2" style="padding:8px 12px;border:1px solid #e2e8f0;">
默认 <span style="background:#64748b;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 0.1</span> · 选中时 <span style="background:#3b82f6;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 1</span>
</td>
</tr>
<tr style="background:#f8fafc;">
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">适用场景</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">节点与具体文字内容相关(如赏析某词、讲解某句)</td>
<td style="padding:8px 12px;border:1px solid #e2e8f0;">节点对应文本某个位置(如在此处开始导入、在此处小结)</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 数据模型 -->
<div class="section">
<h3>数据模型</h3>
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.6;">
<div style="color:#94a3b8;">// 锚点 — 统一接口,区分 type</div>
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">NodeAnchor</span> {</div>
<div>&nbsp;&nbsp;id: <span style="color:#10b981;">string</span>;</div>
<div>&nbsp;&nbsp;nodeId: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 关联的节点</span></div>
<div>&nbsp;&nbsp;type: <span style="color:#10b981;">"range"</span> | <span style="color:#10b981;">"point"</span>; <span style="color:#64748b;">// 两种锚定方式</span></div>
<div>&nbsp;&nbsp;start: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// 正文纯文本偏移量</span></div>
<div>&nbsp;&nbsp;end?: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// range 锚定的结束偏移point 无)</span></div>
<div>&nbsp;&nbsp;textPreview?: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// range 锚定的文字预览</span></div>
<div>}</div>
<br>
<div style="color:#94a3b8;">// 渲染规则</div>
<div><span style="color:#f59e0b;">function</span> <span style="color:#3b82f6;">getAnchorStyle</span>(anchor: <span style="color:#3b82f6;">NodeAnchor</span>, isActive: <span style="color:#10b981;">boolean</span>) {</div>
<div>&nbsp;&nbsp;<span style="color:#f59e0b;">if</span> (anchor.type === <span style="color:#10b981;">"range"</span>) {</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#f59e0b;">return</span> { backgroundColor: getNodeColor(anchor.nodeId), opacity: isActive ? <span style="color:#f59e0b;">0.3</span> : <span style="color:#f59e0b;">0</span> };</div>
<div>&nbsp;&nbsp;} <span style="color:#f59e0b;">else</span> { <span style="color:#64748b;">// point</span></div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#f59e0b;">return</span> { opacity: isActive ? <span style="color:#f59e0b;">1</span> : <span style="color:#f59e0b;">0.3</span> };</div>
<div>&nbsp;&nbsp;}</div>
<div>}</div>
</div>
</div>

View File

@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1782143663726}

View File

@@ -0,0 +1,3 @@
sessionDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344
stateDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344\state
contentDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344\content

View File

@@ -4,19 +4,23 @@
**任何任务开始前,必须先查阅架构影响地图,通过图定位代码和模块。**
1. **先图后码**:执行任何分析、修改、搜索任务时,首先阅读 `docs/architecture/004_architecture_impact_map.md` `docs/architecture/005_architecture_data.json`,从图中定位目标模块、函数、依赖关系,再按图索骥读取源码
2. **图未覆盖则先补图**:如果发现项目中存在架构图未记录的模块、函数、表、路由等,**必须优先完善架构图信息**,然后再继续后续工作
3. **改码必同步图**:对源码的任何修改完成后,必须同步更新 004 和 005 两个架构文档
1. **先图后码**:执行任何分析、修改、搜索任务时,首先运行 `npm run arch:scan` 更新 arch.db再通过 `npm run arch:query` 查询目标模块、函数、依赖关系,结合阅读 `docs/architecture/004_architecture_impact_map.md` 定位架构设计意图,最后按图索骥读取源码
2. **图未覆盖则先补图**:如果发现项目中存在 arch.db 未记录的模块、函数、表、路由等,**必须先运行 `npm run arch:scan` 重新扫描**,然后检查 004 是否需要补充
3. **改码必同步图**:对源码的任何修改完成后,必须运行 `npm run arch:scan` 更新 arch.db若架构设计意图有变化同步更新 004
### 架构文档清单
| 文档 | 用途 |
|------|------|
| `docs/architecture/004_architecture_impact_map.md` | 人类可读的架构影响地图 |
| `docs/architecture/005_architecture_data.json` | AI 友好格式的结构化数据 |
| `docs/architecture/004_architecture_impact_map.md` | 架构设计意图唯一源(人类可读) |
| `docs/architecture/006_k12_feature_checklist.md` | 标准功能模块清单 |
| `docs/architecture/007_gap_audit_report.md` | 差距审计报告 |
| `docs/architecture/audit/01_decoupling_roadmap.md` | 解耦路线图 |
| `docs/architecture/008_module_role_mapping.md` | 模块角色映射 |
| `docs/architecture/roadmap/` | 长远规划tech-debt/decoupling/pending-features |
| `docs/architecture/audit/` | 架构审查报告与归档(含已废弃的 005 JSON、004 V1 |
| `docs/troubleshooting/known-issues.md` | 已知问题速查(场景→技术映射 + 工作经验日志) |
> 005_architecture_data.json 已废弃归档至 `audit/archive/`,结构化数据查询统一通过 arch.db
### 需要同步图的场景
@@ -30,9 +34,23 @@
### 同步方式
- 修改 Markdown 文档中对应的模块章节
- 修改 JSON 文档中对应的节点(`modules.*.exports``permissions``dependencyMatrix``routes``dbTables` 等)
- 确保两个文档内容一致
- 修改源码后运行 `npm run arch:scan` 更新 arch.db强制
- 若架构设计意图变化,同步更新 `docs/architecture/004_architecture_impact_map.md`
- 若发现新的"场景→技术"映射或工作经验,更新 `docs/troubleshooting/known-issues.md`
## 架构元数据库规则arch.db
**arch.db 是代码结构唯一源AI 工作前必须运行 `npm run arch:scan` 更新。**
1. **arch.db 取代 005 JSON**:模块、函数、调用关系、依赖关系、技术标签查询 arch.db不手动维护结构化数据文件
2. **查询命令**
- `npm run arch:query -- sql "<SQL>"` 自定义 SQL 查询
- `npm run arch:query -- module-deps` 查模块依赖
- `npm run arch:query -- module-reverse-deps <module>` 查反向依赖
- `npm run arch:query -- symbol-refs <symbol>` 查符号引用链
- `npm run arch:query -- tech-usage <tag>` 查技术使用
- `npm run arch:query -- violations` 查架构违规
3. **arch.db 不替代 004**arch.db 是"代码现状"004 是"设计意图",两者互补
## 编码规范
@@ -107,7 +125,31 @@ src/modules/[module]/
- 使用 `cn()` 工具函数管理条件类名
- **禁止**字符串拼接动态类名(`bg-${color}-500`
- **禁止**使用任意值(`w-[137px]`),除非有充分理由并注释
- 设计令牌在 `src/app/globals.css` 中使用 CSS 变量定义
- 设计令牌在 `src/app/styles/tokens/` 目录中分层定义,通过 `@theme inline` 暴露为 Tailwind 类
### 设计令牌规范(强制)
- **禁止硬编码颜色**: TSX/TS/CSS 中不得出现 `#hex` 颜色字面量,统一使用 `hsl(var(--*))` 或 Tailwind 类 `bg-*`
- **禁止硬编码字体**: 不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量,使用 `var(--font-family-sans/serif/mono)`
- **禁止硬编码字号**: 不得出现 `font-size: Npx`,使用 `var(--font-size-1~9)`
- **禁止 Tailwind 任意值**: 不得使用 `w-[Npx]`/`h-[Npx]`/`p-[Npx]` 等,映射到 `--space-*` 或 Tailwind 默认阶梯
- **豁免场景**(需 `// eslint-disable-next-line no-restricted-syntax -- <reason>` 注释):
- PWA manifest`src/app/manifest.ts`
- 邮件 HTML 内联样式(`src/modules/notifications/channels/email-channel.ts`
- 图表 SVG 固定画布尺寸recharts 选择器中的 `#ccc`/`#fff`
- loading.tsx 占位骨架
- Dialog 固定宽度等无法令牌化的设计固定尺寸
- **令牌文件分布**: `src/app/styles/tokens/`primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index
- **令牌分层**:
- Layer 1 Primitive`primitive.css`:原始色板/字号/间距/阴影,业务代码不直接引用
- Layer 2 Semantic`semantic-light.css` + `semantic-dark.css`:语义令牌,业务代码唯一引用入口
- 模块命名空间(`lesson-preparation.css`:`--lp-*` 令牌,明暗双份
- Tailwind 暴露(`tailwind-theme.css`:`@theme inline` 将 Semantic 令牌暴露为 `bg-*`/`text-*`/`font-*`
- **改令牌必同步图**: 修改令牌定义后,同步更新 `docs/architecture/004_architecture_impact_map.md` 与 arch.db`npm run arch:scan`
- **ESLint 强制约束**:
- `no-restricted-syntax`: 禁止 `#hex` 字面量
- `design-tokens/no-hardcoded-fonts`: 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符)
- 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML`manifest.ts`PWA
### 安全规范
@@ -121,3 +163,64 @@ src/modules/[module]/
- 使用 Conventional Commits 格式:`feat(scope): description`
- 类型:`feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `perf`, `ci`
- 提交前必须运行 `npm run lint``npx tsc --noEmit` 确保零错误
## 问题记录规则
**所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`(索引式速查手册)。**
### 必须记录的场景
| 场景 | 记录要求 |
|------|---------|
| 构建报错dev/build/lint/tsc | 记录到"全局经验"对应主题分区 |
| 运行时异常(白屏/API 报错/数据加载失败) | 记录到"模块经验"对应模块分区 |
| 框架/库版本兼容问题 | 记录到"全局经验: Next.js 配置与运行时" |
| 依赖配置问题serverExternalPackages/webpackIgnore 等) | 记录到"全局经验: Next.js 配置与运行时" |
| 架构约束违规 | 记录到"全局经验"对应主题分区 |
### 记录格式
索引式表格,指明"场景→技术/规则"映射,不写多行代码示例:
```markdown
### X.X 主题分区
| 场景 | 技术/规则 |
|------|----------|
| 简述场景 | 正确做法(一句话) |
```
### 记录要求
- **索引式**:场景→技术/规则映射,不写代码示例和错误示范列
- **去重**:同类问题在原条目补充,不重复创建
- **引用架构规则**:架构分层、模块结构等规则引用 004 和 project_rules不重复
- **工作经验日志**:在"工作经验日志"区按时间倒序追加50 条上限),记录"做了什么/学到什么/下次注意"
## AI 工作强制流程
**所有 AI 工作必须遵循此流程,违反即违规。**
### 阶段 1: 上下文加载
1. `npm run arch:scan` 更新 arch.db
2. `npm run arch:query -- module-deps` 查目标模块依赖
3. `npm run arch:query -- symbol-refs <目标函数>` 查调用链
4. 阅读 `src/modules/[模块]/README.md` 读模块工作流程
5.`docs/troubleshooting/known-issues.md` "模块经验" 分区读相关经验
### 阶段 2: 执行工作
1. 按规划执行
2. 修改代码后立即运行 `npm run arch:scan` 更新 arch.db
3. 运行 `npx tsc --noEmit``npm run lint` 确保零错误
### 阶段 3: 经验沉淀(强制,不可跳过)
1.`docs/troubleshooting/known-issues.md` "工作经验日志" 区追加一条记录:
- 日期 + 时间
- 模块
- 做了什么 + 学到什么
2. 若发现新的"场景→技术"映射 → 提炼到对应模块分区
3. 若发现新的架构决策 → 更新 004
4. 若代码结构变化 → `npm run arch:scan` 确认 arch.db 已更新

1
.tsc_out.txt Normal file
View File

@@ -0,0 +1 @@
src/app/(dashboard)/teacher/textbooks/error.tsx(3,10): error TS2305: Module '"@/shared/components/route-error"' has no exported member 'RouteError'.

43
CHANGELOG.md Normal file
View File

@@ -0,0 +1,43 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- arch:scan @public JSDoc 标记豁免机制,支持登录前/公开/内部工具 Server Action 豁免权限校验
- arch:scan 递归 CTE 违规检测,识别通过辅助函数间接调用 requirePermission 的调用链
- 大仓工程基建LICENSE、CONTRIBUTING、SECURITY、.env.example 文档
- husky + lint-staged + commitlint 本地提交规范工具链
- /api/health 健康检查端点 + Dockerfile HEALTHCHECK
- @next/bundle-analyzer 构建体积分析工具
- CI 流水线新增 Unit test + coverage 阶段
- tsconfig 开启 noUncheckedIndexedAccess 严格模式
### Changed
- 重构 004 架构文档为完整架构设计文档912 行14 章节13 个 mermaid 图)
- 重写 35 个模块 README统一 8 章节模板(架构图/流程图/技术栈)
- 拆分 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子文件)
- 精简 known-issues.md 为索引式速查手册(场景→技术/规则映射)
### Fixed
- 修复 20 个 Server Action 权限违规12 个 @public 豁免 + 8 个真违规修复)
- 修复 ai 模块 6 个 Action 权限误报requireAiPermission 间接调用链识别)
- 修复 parent 模块 6 个 Action 权限缺失requireAuth → requirePermission
- 修复 settings 模块 updateProfileAction 权限校验(显式 requirePermission
## [0.1.0] - 2026-06-01
### Added
- 初始版本发布
- K12 智慧教学平台核心功能:备课、作业、考试、成绩、考勤、消息、家校互动
- 严格三层架构app → modules → shared
- 5 层状态管理模型URL(nuqs) · Server(TanStack Query) · Client(Zustand) · Global UI · Form
- 权限 3 道防线proxy.ts → requirePermission → usePermission
- 设计令牌双层架构Primitive + Semantic
- arch.db 架构元数据库12 张表 + 7 个索引)
- cacheFn 请求级缓存层
- Gitea Actions CI/CD 流水线

129
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,129 @@
# 贡献指南
感谢参与本项目!请遵循以下规范提交贡献。
## 开发环境准备
```bash
# 1. 安装依赖
npm install
# 2. 准备环境变量
cp .env.example .env
# 编辑 .env 填入实际配置
# 3. 初始化数据库
npm run db:push
# 4. 启动开发服务器
npm run dev
```
## 强制工作流程
**所有代码改动前必须先查阅架构文档:**
1. 阅读 `docs/architecture/004_architecture_impact_map.md` 了解架构设计意图
2. 运行 `npm run arch:scan` 更新 arch.db
3. 运行 `npm run arch:query -- module-deps` 查目标模块依赖
4. 阅读 `src/modules/[模块]/README.md` 了解模块工作流程
5.`docs/troubleshooting/known-issues.md` 读相关经验
**代码改动后必须:**
1. 运行 `npx tsc --noEmit` 确保零错误
2. 运行 `npm run lint` 确保零错误
3. 运行 `npm run arch:scan` 更新 arch.db
4. 若架构设计意图变化,同步更新 004 文档
5. 若发现新场景→技术映射,更新 known-issues.md
## 提交规范
### Conventional Commits 格式
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
**类型type**
- `feat`: 新功能
- `fix`: Bug 修复
- `docs`: 文档变更
- `style`: 代码格式(不影响功能)
- `refactor`: 重构(既不是新功能也不是修复)
- `test`: 测试相关
- `chore`: 构建/工具/依赖变更
- `perf`: 性能优化
- `ci`: CI/CD 变更
**示例:**
```
feat(arch-scan): add @public JSDoc tag exemption mechanism
fix(permissions): fix parent module 6 Action permission violations
refactor: split 5 oversized files into domain-specific subfiles
docs(architecture): rewrite 004 as architecture design document
```
### 提交前检查
husky + lint-staged 会在 `git commit` 时自动执行:
- ESLint 检查暂存文件
- Prettier 格式化暂存文件
- commitlint 校验 commit message 格式
如果检查失败,请修复后重新提交。
## 架构约束
### 严格三层架构
```
app → modules → shared
```
- `app/` 只能调用 `modules/` 的 Server Actions 和 data-access
- `modules/` 之间通过对方 data-access 通信,不直接查询对方 DB 表
- `shared/` 不得反向依赖 `modules/*``app/*`
### 代码质量规则
- 禁止 `any`,未知类型用 `unknown` + 类型守卫
- 禁止 `as` 断言(除非从 `unknown` 转换,需注释原因)
- 函数返回值必须显式标注,特别是 `Promise<T>`
- 仅用于类型的导入使用 `import type`
- Server Action 必须调用 `requirePermission()`(或加 `@public` 标记豁免)
- 前端权限检查使用 `usePermission().hasPermission()`,禁止 `role === "xxx"` 硬编码
- 单文件行数:组件 ≤500actions/data-access ≤800硬限 1000
### 设计令牌规范
- 禁止硬编码颜色(`#hex`),使用 `hsl(var(--*))` 或 Tailwind 类
- 禁止硬编码字体(`'Inter'`),使用 `var(--font-family-*)`
- 禁止 Tailwind 任意值(`w-[137px]`),映射到 `--space-*` 或默认阶梯
## 文档同步
### 需要同步架构图的场景
- 新增/删除/重命名导出函数、组件、Hook、类型
- 修改函数签名(参数、返回类型)
- 修改权限点或角色-权限映射
- 新增/删除数据库表、路由页面、API 路由
- 修改模块间依赖关系
- 新增模块
### 同步方式
- 修改源码后运行 `npm run arch:scan` 更新 arch.db强制
- 若架构设计意图变化,同步更新 `docs/architecture/004_architecture_impact_map.md`
- 若发现新"场景→技术"映射,更新 `docs/troubleshooting/known-issues.md`
## 问题报告
- 构建/lint/tsc 报错 → 记录到 `docs/troubleshooting/known-issues.md` "全局经验"分区
- 运行时异常 → 记录到"模块经验"分区
- 框架/库版本兼容问题 → 记录到"全局经验: Next.js 配置与运行时"

View File

@@ -18,4 +18,7 @@ EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:' + (process.env.PORT || 3000) + '/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "server.js"]

14
LICENSE Normal file
View File

@@ -0,0 +1,14 @@
PROPRIETARY AND CONFIDENTIAL
Copyright (c) 2026 EazyGame. All rights reserved.
This source code and accompanying documentation (the "Software") is the
proprietary and confidential property of EazyGame. No part of the Software
may be reproduced, distributed, or transmitted in any form or by any means,
including photocopying, recording, or other electronic or mechanical methods,
without the prior written permission of EazyGame.
For licensing inquiries, contact: legal@eazygame.cn
Unauthorized use, reproduction, or distribution of this Software, via any
medium, is strictly prohibited and may result in civil and criminal penalties.

90
SECURITY.md Normal file
View File

@@ -0,0 +1,90 @@
# 安全策略
## 报告安全漏洞
**请不要通过 GitHub Issue 公开报告安全漏洞。**
发现安全漏洞请通过以下渠道私密报告:
- 邮件security@eazygame.cn
- 内部工单系统Security 项目 → New Issue
报告时请包含:
1. 漏洞描述和影响范围
2. 复现步骤(最小化示例)
3. 影响的版本号
4. 建议的修复方案(可选)
**响应时间:** 24 小时内确认收到5 个工作日内给出评估结果。
## 安全架构
### 权限三道防线
```
proxy.ts (路由级 bitmap) → requirePermission (Server Action 级) → usePermission (客户端级)
```
- **路由级**`src/proxy.ts` 使用 bitmap 快速拦截未授权路由
- **Server Action 级**:每个 Action 必须调用 `requirePermission()`,或用 `@public` JSDoc 标记豁免
- **客户端级**:组件使用 `usePermission().hasPermission()` 控制元素显隐
### 认证与会话
- JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中
- 服务端环境变量不加 `NEXT_PUBLIC_` 前缀
- 环境变量使用 `@t3-oss/env-nextjs` + Zod 校验(`src/env.mjs`
### 数据访问
- 前端禁止直接访问数据库,所有数据访问必须通过 `data-access.ts` 模块
- Server Action 必须使用 `requirePermission()` 进行权限校验
- 家长路由必须包含 `parentId``studentId` 双重权限校验,防止信息泄露
### 输入安全
- **禁止 `dangerouslySetInnerHTML`**(如必须使用,先用 DOMPurify 清洗)
- Server Action 输入使用 Zod 验证,验证失败返回结构化错误
- 注册/登录流程实施速率限制,防止暴力破解和邮箱枚举攻击
## 安全审计
### arch:scan 自动检测
`npm run arch:query -- violations` 会自动检测:
- **长文件**>800 行):提示拆分,降低维护风险
- **Server Action 权限缺失**:识别未调用 `requirePermission` 的 Server Action支持递归调用链识别
### @public 豁免标记
登录前/公开/内部工具 Server Action 可用 `@public` JSDoc 标记豁免权限校验:
```ts
/**
* 注册 Action登录前公开调用。
*
* @public 登录前公开 Action豁免 requirePermission 校验。
*/
export async function registerAction(formData: FormData) {
// ...
}
```
**豁免场景:**
- 登录前 Action注册、邮箱可用性检查、2FA 预检)
- 内部日志工具audit-logger、change-logger、login-logger
- 权限查询工具isAdminRole、canConfigurePublicAiProvider
- 登录后必经流程onboarding 状态查询/完成)
## 依赖安全
- 定期运行 `npm audit` 检查已知漏洞
- CI 流水线包含 Trivy 安全扫描(`.trivyignore` 配置豁免项)
- 依赖升级通过 PR 审核,不允许直接推送 main 分支
## 数据保护
- 数据库备份:每日自动备份,每周 DR 演练
- 敏感数据密码、2FA 密钥)使用 bcrypt/Argon2 哈希存储
- 日志不记录敏感信息密码、token、个人身份信息

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

1734
build-output.txt Normal file

File diff suppressed because it is too large Load Diff

24
commitlint.config.mjs Normal file
View File

@@ -0,0 +1,24 @@
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"test",
"chore",
"perf",
"ci",
"build",
"revert",
],
],
"subject-case": [0],
"header-max-length": [2, "always", 120],
},
}

4
cookies.txt Normal file
View File

@@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

BIN
debug-exams.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -69,7 +69,7 @@
| 文档 | 归档原因 |
|------|---------|
| [002 RBAC 重构方案](architecture/002_rbac_refactoring.md) | 描述修复前的安全隐患,当前所有 Server Action 已接入 `requirePermission()` |
| [002 角色路由 RFC](architecture/002_role_based_routing.md) | 2025-12-23 提案,当前角色域路由已全部实现 |
| [002b 角色路由 RFC](architecture/002b_role_based_routing.md) | 2025-12-23 提案,当前角色域路由已全部实现 |
| [003 UI 重构计划](architecture/003_ui_refactoring_plan.md) | 2026-06-16 重构计划,当前已执行完毕 |
### 设计历史文档

View File

@@ -368,7 +368,7 @@ AI 模式: 选择 AI Provider → 粘贴试卷源文本 → AI 解析生成 →
| API 路由 | 9 |
| Server Actions | 80+ |
| 用户角色 | 6 (admin/teacher/student/parent/grade_head/teaching_head) |
| 权限点 | 54 |
| 权限点 | 67 |
---
@@ -380,3 +380,4 @@ AI 模式: 选择 AI Provider → 粘贴试卷源文本 → AI 解析生成 →
| [005 架构数据](./005_architecture_data.json) | AI 友好的结构化架构数据 |
| [006 功能清单](./006_k12_feature_checklist.md) | 企业级 K12 标准功能模块清单 |
| [007 差距审计报告](./007_gap_audit_report.md) | 功能差距审计与补齐路线图 |
| [008 模块角色映射](./008_module_role_mapping.md) | 模块-角色-功能映射总览 |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
# 模块-角色-功能映射总览
> 生成日期2026-06-22
> 数据来源:`src/modules/` 全量扫描 + `src/app/(dashboard)/` 路由权限校验 + `src/shared/types/permissions.ts` 权限点定义
> 角色体系admin / teacher / student / parent / grade_head / teaching_head含 management 路由组)
---
## 一、模块-角色速查表
| # | 模块 | 目录 | admin | teacher | student | parent | mgmt | 核心功能 |
|---|------|------|:-----:|:-------:|:-------:|:------:|:----:|----------|
| 1 | adaptive-practice | `adaptive-practice/` | | ✅ | ✅ | ✅ | ✅ | 专项练习:自适应出题、答题、练习历史、成绩分析 |
| 2 | ai | `ai/` | ✅ | ✅ | ✅ | ✅ | ✅ | AI 赋能:对话助手、出题辅助、批改辅助、学情分析、多模型配置 |
| 3 | announcements | `announcements/` | ✅ | ✅ | ✅ | ✅ | ✅ | 通知公告:学校/年级/班级三级公告发布与查看 |
| 4 | attendance | `attendance/` | ✅ | ✅ | ✅ | ✅ | | 考勤管理:学生/教师考勤登记、统计、规则配置 |
| 5 | audit | `audit/` | ✅ | | | | | 日志审计:操作日志、登录日志、数据变更日志、导出 |
| 6 | auth | `auth/` | ✅ | ✅ | ✅ | ✅ | ✅ | 认证系统:登录/注册/JWT/双因素认证 |
| 7 | classes | `classes/` | ✅ | ✅ | ✅ | | | 班级管理:班级 CRUD、课表、学生管理、邀请码 |
| 8 | course-plans | `course-plans/` | ✅ | ✅ | ✅ | ✅ | | 课程计划:教学计划创建、进度跟踪、日历视图 |
| 9 | dashboard | `dashboard/` | ✅ | ✅ | ✅ | ✅ | ✅ | 仪表盘:角色独立看板(统计卡片/图表/快捷操作) |
| 10 | diagnostic | `diagnostic/` | | ✅ | ✅ | ✅ | | 学情诊断:知识点掌握度雷达图、班级/个人诊断报告 |
| 11 | elective | `elective/` | ✅ | ✅ | ✅ | ✅ | | 选课管理:选修课创建/选课/退选/抽签 |
| 12 | error-book | `error-book/` | ✅ | ✅ | ✅ | ✅ | | 错题本错题自动采集、SM-2 间隔复习、统计分析 |
| 13 | exams | `exams/` | | ✅ | | | | 考试管理AI 出题/组卷/批改/监考/分析 |
| 14 | files | `files/` | ✅ | ✅ | ✅ | ✅ | ✅ | 文件管理:上传/预览/存储/权限控制 |
| 15 | grades | `grades/` | ✅ | ✅ | ✅ | ✅ | ✅ | 成绩管理:录入/查询/统计/趋势/导出/排名 |
| 16 | homework | `homework/` | | ✅ | ✅ | | | 作业管理:布置/提交/批改/评分/统计分析 |
| 17 | layout | `layout/` | ✅ | ✅ | ✅ | ✅ | ✅ | 布局框架:导航配置/侧边栏/顶部栏(所有角色共用) |
| 18 | lesson-preparation | `lesson-preparation/` | ✅ | ✅ | ✅ | ✅ | | 备课系统:教案创建/编辑/发布/查看 |
| 19 | messaging | `messaging/` | ✅ | ✅ | ✅ | ✅ | ✅ | 站内消息:私信/群发/草稿/已读状态 |
| 20 | notifications | `notifications/` | ✅ | ✅ | ✅ | ✅ | ✅ | 通知系统:站内/邮件/短信/微信多渠道推送、偏好管理 |
| 21 | onboarding | `onboarding/` | ✅ | ✅ | ✅ | ✅ | ✅ | 新手引导:首次登录角色选择、资料填写、班级加入 |
| 22 | parent | `parent/` | | | | ✅ | | 家长聚合:子女学习概览、成绩/作业/考勤详情 |
| 23 | proctoring | `proctoring/` | | ✅ | | | | 考试监考:实时提交进度、异常行为标记 |
| 24 | questions | `questions/` | ✅ | ✅ | | | | 题库管理:题目 CRUD、分类标签、批量导入导出 |
| 25 | rbac | `rbac/` | ✅ | | | | | 角色权限:动态角色管理、权限目录、角色-权限分配 |
| 26 | scheduling | `scheduling/` | ✅ | ✅ | ✅ | | | 排课系统:自动排课引擎、课表查看、调课/代课 |
| 27 | school | `school/` | ✅ | | | | | 学校管理:学校信息/学年学期/年级/班级/部门/学科 |
| 28 | settings | `settings/` | ✅ | ✅ | ✅ | ✅ | ✅ | 用户设置:个人信息/通知偏好/外观/安全/登出 |
| 29 | standards | `standards/` | (via) | (via) | | | | 课标库:国家/校标/自定义课标,通过 lesson-preparation 使用 |
| 30 | student | `student/` | | | ✅ | | | 学生聚合:课程查看/课表/学习路径 |
| 31 | textbooks | `textbooks/` | (via) | ✅ | ✅ | | | 教材资源:教材库/章节结构/知识点图谱/内容阅读 |
| 32 | users | `users/` | ✅ | | | | | 用户管理:用户 CRUD、批量导入、角色分配 |
> 注:`✅` = 有独立页面路由;`(via)` = 通过其他模块间接使用,无独立路由页面
---
## 二、按角色展开的模块清单
### 2.1 admin系统管理员
| 模块 | 页面路由 | 权限点 | 功能说明 |
|------|----------|--------|----------|
| **school** | `/admin/school` | `SCHOOL_MANAGE` | 学校基础信息配置 |
| | `/admin/school/schools` | `SCHOOL_MANAGE` | 多校区管理 |
| | `/admin/school/academic-year` | `SCHOOL_MANAGE` | 学年学期管理 |
| | `/admin/school/classes` | `SCHOOL_MANAGE` | 班级创建与管理 |
| | `/admin/school/departments` | `SCHOOL_MANAGE` | 部门管理 |
| | `/admin/school/grades` | `SCHOOL_MANAGE` | 年级管理与组长指派 |
| | `/admin/school/grades/insights` | `SCHOOL_MANAGE` | 年级洞察分析 |
| **users** | `/admin/users` | `USER_MANAGE` | 用户账号管理 |
| | `/admin/users/import` | `USER_MANAGE` | 批量导入用户 |
| **rbac** | `/admin/roles` | `ROLE_READ` | 角色列表查看 |
| | `/admin/roles/[id]` | `ROLE_READ` | 角色详情与权限编辑 |
| | `/admin/permissions` | `PERMISSION_READ` | 权限点目录查看 |
| **announcements** | `/admin/announcements` | `ANNOUNCEMENT_MANAGE` | 公告管理(创建/编辑/删除) |
| | `/admin/announcements/[id]` | `ANNOUNCEMENT_MANAGE` | 公告详情 |
| | `/admin/announcements/[id]/edit` | `ANNOUNCEMENT_MANAGE` | 编辑公告 |
| **audit** | `/admin/audit-logs` | `AUDIT_LOG_READ` | 操作日志查看 |
| | `/admin/audit-logs/overview` | `AUDIT_LOG_READ` | 审计概览统计 |
| | `/admin/audit-logs/data-changes` | `AUDIT_LOG_READ` | 数据变更日志 |
| | `/admin/audit-logs/login-logs` | `AUDIT_LOG_READ` | 登录日志 |
| **dashboard** | `/admin/dashboard` | `DASHBOARD_ADMIN_READ` | 管理员仪表盘 |
| **scheduling** | `/admin/scheduling/auto` | `SCHEDULE_AUTO` | 自动排课 |
| | `/admin/scheduling/changes` | `SCHEDULE_ADJUST` | 调课管理 |
| | `/admin/scheduling/rules` | `SCHEDULE_ADJUST` | 排课规则配置 |
| **course-plans** | `/admin/course-plans` | `COURSE_PLAN_READ` | 课程计划查看 |
| | `/admin/course-plans/create` | `COURSE_PLAN_MANAGE` | 创建课程计划 |
| | `/admin/course-plans/[id]` | `COURSE_PLAN_READ` | 课程计划详情 |
| | `/admin/course-plans/[id]/edit` | `COURSE_PLAN_MANAGE` | 编辑课程计划 |
| **elective** | `/admin/elective` | `ELECTIVE_READ` | 选课管理 |
| | `/admin/elective/create` | `ELECTIVE_MANAGE` | 创建选修课 |
| | `/admin/elective/[id]` | `ELECTIVE_READ` | 选修课详情 |
| | `/admin/elective/[id]/edit` | `ELECTIVE_MANAGE` | 编辑选修课 |
| **attendance** | `/admin/attendance` | `ATTENDANCE_READ` | 考勤数据查看 |
| **error-book** | `/admin/error-book` | `ERROR_BOOK_ANALYTICS_READ` | 全校错题统计分析 |
| **questions** | `/admin/questions` | `QUESTION_READ` | 题库管理 |
| **files** | `/admin/files` | `FILE_READ` | 文件管理 |
| **ai** | `/admin/ai-settings` | `AI_CHAT` | AI 多模型配置 |
| **lesson-plans** | `/admin/lesson-plans` | `LESSON_PLAN_READ` | 教案查看 |
| | `/admin/lesson-plans/[planId]/view` | `LESSON_PLAN_READ` | 教案详情 |
| **settings** | `/admin/settings` | `SETTINGS_ADMIN` | 管理员设置 |
---
### 2.2 teacher教师
| 模块 | 页面路由 | 权限点 | 功能说明 |
|------|----------|--------|----------|
| **dashboard** | `/teacher/dashboard` | `DASHBOARD_TEACHER_READ` | 教师仪表盘(待批改/今日课表/班级动态) |
| **classes** | `/teacher/classes/my` | `CLASS_READ` | 我的班级列表 |
| | `/teacher/classes/my/[id]` | `CLASS_READ` | 班级详情 |
| | `/teacher/classes/schedule` | `CLASS_READ` | 班级课表 |
| | `/teacher/classes/students` | `CLASS_READ` | 班级学生管理 |
| **exams** | `/teacher/exams` | `EXAM_READ` | 考试列表 |
| | `/teacher/exams/all` | `EXAM_READ` | 全部考试 |
| | `/teacher/exams/create` | `EXAM_CREATE` | 创建考试 |
| | `/teacher/exams/new` | `EXAM_CREATE` | AI 出题 |
| | `/teacher/exams/[id]/build` | `EXAM_READ` | 组卷 |
| | `/teacher/exams/[id]/edit-rich` | `EXAM_UPDATE` | 富文本编辑试卷 |
| | `/teacher/exams/[id]/analytics` | `EXAM_READ` | 考试分析 |
| | `/teacher/exams/[id]/proctoring` | `EXAM_PROCTOR` | 考试监考 |
| | `/teacher/exams/grading` | `HOMEWORK_GRADE` | 批改列表 |
| | `/teacher/exams/grading/[submissionId]` | `HOMEWORK_GRADE` | 批改详情 |
| **homework** | `/teacher/homework/assignments` | `HOMEWORK_CREATE` | 作业管理 |
| | `/teacher/homework/assignments/create` | `HOMEWORK_CREATE` | 布置作业 |
| | `/teacher/homework/assignments/[id]` | `HOMEWORK_CREATE` | 作业详情 |
| | `/teacher/homework/assignments/[id]/submissions` | `HOMEWORK_GRADE` | 提交列表 |
| | `/teacher/homework/submissions` | `HOMEWORK_GRADE` | 批改列表 |
| | `/teacher/homework/submissions/[submissionId]` | `HOMEWORK_GRADE` | 批改详情 |
| | `/teacher/homework/submissions/[submissionId]/scan-grading` | `HOMEWORK_GRADE` | 阅卷式批改 |
| **grades** | `/teacher/grades` | `GRADE_RECORD_READ` | 成绩查询 |
| | `/teacher/grades/analytics` | `GRADE_RECORD_READ` | 成绩分析 |
| | `/teacher/grades/entry` | `GRADE_RECORD_MANAGE` | 成绩录入 |
| | `/teacher/grades/stats` | `GRADE_RECORD_READ` | 成绩统计 |
| **questions** | `/teacher/questions` | `QUESTION_READ` | 题库管理 |
| **textbooks** | `/teacher/textbooks` | `TEXTBOOK_READ` | 教材查看 |
| | `/teacher/textbooks/[id]` | `TEXTBOOK_READ` | 教材内容 |
| **lesson-plans** | `/teacher/lesson-plans` | `LESSON_PLAN_READ` | 教案列表 |
| | `/teacher/lesson-plans/new` | `LESSON_PLAN_READ` | 创建教案 |
| | `/teacher/lesson-plans/[planId]/edit` | `LESSON_PLAN_READ` | 编辑教案 |
| **diagnostic** | `/teacher/diagnostic` | `DIAGNOSTIC_READ` | 学情诊断总览 |
| | `/teacher/diagnostic/class/[classId]` | `DIAGNOSTIC_READ` | 班级诊断 |
| | `/teacher/diagnostic/student/[studentId]` | `DIAGNOSTIC_READ` | 学生诊断 |
| **attendance** | `/teacher/attendance` | `ATTENDANCE_READ` | 考勤查看 |
| | `/teacher/attendance/sheet` | `ATTENDANCE_MANAGE` | 考勤登记 |
| | `/teacher/attendance/stats` | `ATTENDANCE_READ` | 考勤统计 |
| **course-plans** | `/teacher/course-plans` | `COURSE_PLAN_READ` | 课程计划 |
| | `/teacher/course-plans/[id]` | `COURSE_PLAN_READ` | 计划详情 |
| **elective** | `/teacher/elective` | `ELECTIVE_READ` | 选课管理 |
| | `/teacher/elective/create` | `ELECTIVE_MANAGE` | 创建选修课 |
| | `/teacher/elective/[id]/edit` | `ELECTIVE_MANAGE` | 编辑选修课 |
| **error-book** | `/teacher/error-book` | `ERROR_BOOK_ANALYTICS_READ` | 班级错题分析 |
| **practice** | `/teacher/practice` | `ADAPTIVE_PRACTICE_READ` | 专项练习统计 |
| **schedule** | `/teacher/schedule-changes` | `SCHEDULE_ADJUST` | 调课申请 |
---
### 2.3 student学生
| 模块 | 页面路由 | 权限点 | 功能说明 |
|------|----------|--------|----------|
| **dashboard** | `/student/dashboard` | `DASHBOARD_STUDENT_READ` | 学生仪表盘(作业/考试/成绩趋势) |
| **learning** | `/student/learning` | `CLASS_READ` | 学习中心 |
| | `/student/learning/assignments` | — | 作业列表 |
| | `/student/learning/assignments/[assignmentId]` | — | 作答 |
| | `/student/learning/assignments/[assignmentId]/result` | — | 作答结果 |
| | `/student/learning/courses` | `CLASS_READ` | 课程列表 |
| | `/student/learning/courses/[classId]` | `CLASS_READ` | 课程详情 |
| | `/student/learning/textbooks` | `TEXTBOOK_READ` | 教材 |
| | `/student/learning/textbooks/[id]` | `TEXTBOOK_READ` | 教材内容 |
| | `/student/learning/study-path` | `AI_CHAT` | AI 学习路径 |
| **grades** | `/student/grades` | `GRADE_RECORD_READ` | 成绩查询 |
| **error-book** | `/student/error-book` | `ERROR_BOOK_READ` | 我的错题本 |
| **practice** | `/student/practice` | `ADAPTIVE_PRACTICE_READ` | 专项练习 |
| | `/student/practice/[sessionId]` | `ADAPTIVE_PRACTICE_READ` | 练习对话 |
| **diagnostic** | `/student/diagnostic` | `DIAGNOSTIC_READ` | 学情诊断 |
| **elective** | `/student/elective` | `ELECTIVE_READ` | 选课 |
| | `/student/elective/[id]` | `ELECTIVE_READ` | 课程详情 |
| **schedule** | `/student/schedule` | `CLASS_READ` | 课表查看 |
| **attendance** | `/student/attendance` | `ATTENDANCE_READ` | 考勤查询 |
| **course-plans** | `/student/course-plans` | `COURSE_PLAN_READ` | 课程计划 |
| | `/student/course-plans/[id]` | `COURSE_PLAN_READ` | 计划详情 |
| **lesson-plans** | `/student/lesson-plans` | `LESSON_PLAN_READ` | 教案查看 |
| | `/student/lesson-plans/[planId]/view` | `LESSON_PLAN_READ` | 教案详情 |
---
### 2.4 parent家长
| 模块 | 页面路由 | 权限点 | 功能说明 |
|------|----------|--------|----------|
| **dashboard** | `/parent/dashboard` | `DASHBOARD_PARENT_READ` | 家长仪表盘(子女学习概况) |
| **children** | `/parent/children/[studentId]` | — | 子女详情聚合页 |
| **grades** | `/parent/grades` | `GRADE_RECORD_READ` | 子女成绩查询 |
| **error-book** | `/parent/error-book` | `ERROR_BOOK_READ` | 子女错题本 |
| **diagnostic** | `/parent/diagnostic` | `DIAGNOSTIC_READ` | 子女学情诊断 |
| **elective** | `/parent/elective` | `ELECTIVE_READ` | 子女选课查看 |
| **attendance** | `/parent/attendance` | `ATTENDANCE_READ` | 子女考勤查询 |
| **course-plans** | `/parent/course-plans` | `COURSE_PLAN_READ` | 课程计划 |
| | `/parent/course-plans/[id]` | `COURSE_PLAN_READ` | 计划详情 |
| **lesson-plans** | `/parent/lesson-plans` | `LESSON_PLAN_READ` | 教案查看 |
| | `/parent/lesson-plans/[planId]/view` | `LESSON_PLAN_READ` | 教案详情 |
| **practice** | `/parent/practice` | `ADAPTIVE_PRACTICE_READ` | 子女练习记录 |
| **leave** | `/parent/leave` | — | 请假申请 |
---
### 2.5 management年级组长/教研组长)
| 模块 | 页面路由 | 权限点 | 功能说明 |
|------|----------|--------|----------|
| **grade** | `/management/grade` | `GRADE_MANAGE` | 年级管理首页 |
| | `/management/grade/dashboard` | `GRADE_RECORD_READ` | 年级仪表盘 |
| | `/management/grade/insights` | `GRADE_RECORD_READ` | 年级洞察分析 |
| | `/management/grade/classes` | `GRADE_MANAGE` | 年级班级管理 |
| | `/management/grade/practice` | `ADAPTIVE_PRACTICE_READ` | 年级练习统计 |
---
## 三、共享模块(所有角色可访问)
以下模块为所有已登录用户提供服务,不区分角色路由:
| 模块 | 路由 | 权限点 | 说明 |
|------|------|--------|------|
| **auth** | `/login`, `/register` | 公开 | 认证(登录/注册) |
| **onboarding** | `/onboarding` | 已登录 | 新手引导(首次登录) |
| **announcements** | `/announcements`, `/announcements/[id]` | `ANNOUNCEMENT_READ` | 公告查看(所有角色) |
| **messages** | `/messages`, `/messages/[id]`, `/messages/compose` | `MESSAGE_READ` / `MESSAGE_SEND` | 站内消息 |
| **profile** | `/profile` | `USER_PROFILE_UPDATE` | 个人资料 |
| **settings** | `/settings` | `USER_PROFILE_UPDATE` | 用户设置(通知/外观/安全) |
| **dashboard** | `/dashboard` | 角色自动路由 | 仪表盘(自动重定向到角色对应仪表盘) |
| **layout** | 所有路由 | — | 全局布局框架(侧边栏/顶部栏/导航配置) |
| **notifications** | 全局组件 | — | 通知弹窗/偏好管理(内嵌于 layout |
| **files** | 全局上传 | `FILE_UPLOAD` | 文件上传所有角色可上传admin 可管理) |
---
## 四、权限点汇总67 个)
| 权限分类 | 权限点 | 适用角色 |
|----------|--------|----------|
| **考试** | `exam:create/create/read/update/delete/duplicate/publish/ai_generate/submit` | teacher |
| | `exam:proctor/read` | teacher |
| **作业** | `homework:create/grade` | teacher |
| | `homework:submit` | student |
| **题库** | `question:create/read/update/delete` | admin, teacher |
| **教材** | `textbook:create/read/update/delete` | admin, teacher |
| **班级** | `class:create/read/update/delete/enroll/schedule` | admin, teacher, student |
| **学校管理** | `school:manage` | admin |
| | `grade:manage` | admin, grade_head |
| | `user:manage` | admin |
| **成绩** | `grade_record:manage/read` | admin, teacher, student, parent |
| **考勤** | `attendance:manage/read` | admin, teacher, student, parent |
| **课程计划** | `course_plan:manage/read` | admin, teacher, student, parent |
| **公告** | `announcement:manage` | admin |
| | `announcement:read` | all |
| **消息** | `message:send/read/delete` | all |
| **排课** | `schedule:auto/adjust` | admin, teacher |
| **选课** | `elective:manage/read/select` | admin, teacher, student, parent |
| **诊断** | `diagnostic:manage/read` | admin, teacher, student, parent |
| **备课** | `lesson_plan:create/read/update/delete/publish` | admin, teacher, student, parent |
| **仪表盘** | `dashboard:admin_read/teacher_read/student_read/parent_read` | 各角色独立 |
| **错题本** | `error_book:read/manage` | student, parent |
| | `error_book:analytics_read` | admin, teacher |
| **专项练习** | `adaptive_practice:read/manage` | student, teacher, parent, grade_head |
| **AI** | `ai:chat` | all |
| | `ai:configure` | admin |
| **文件** | `file:upload/read/delete` | alluploadadmindelete |
| **审计** | `audit_log:read` | admin |
| **RBAC** | `role:create/read/update/delete/assign` | admin |
| | `permission:read` | admin |
| **设置** | `settings:admin` | admin |
| | `user:profile_update` | all |
---
## 五、模块依赖关系速查
| 模块 | 依赖模块(通过 data-access | 被依赖模块 |
|------|------------------------------|-----------|
| **exams** | questions, classes, school, homework | homework, dashboard, proctoring, diagnostic, grades |
| **homework** | exams, classes, school, users | grades, dashboard, diagnostic |
| **grades** | exams, classes, school, users | dashboard, parent |
| **classes** | homework, scheduling | exams, homework, grades, scheduling, attendance, dashboard |
| **dashboard** | users, classes, textbooks, questions, exams, homework | — |
| **parent** | classes, homework, grades | — |
| **diagnostic** | exams, questions, classes, users | — |
| **lesson-preparation** | textbooks, questions, exams, homework, classes, files | — |
| **messaging** | notifications | settings |
| **scheduling** | classes, users | — |
| **proctoring** | exams, users | — |
| **school** | — | exams, classes, grades, scheduling, attendance |
| **textbooks** | — | questions, exams, homework, lesson-preparation |
| **users** | — | all modules |
| **rbac** | — | admin |
| **audit** | — | all modules (via shared/audit-logger) |
---
## 六、维护说明
- 新增模块时,需同步更新本文档的模块-角色速查表(第一节)和对应角色的详细清单(第二节)
- 新增权限点时,需同步更新权限点汇总(第四节)
- 模块依赖关系变更时,需同步更新依赖关系速查(第五节)
- 路由变更时,需同步更新对应角色的路由表

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
# 历史审查报告归档
> 本目录为只读归档,不再更新。
> 有价值的内容已提取到模块 README 和 known-issues.md。
## 归档文件
- 005_architecture_data.json已废弃由 arch.db 替代)
- 60+ 份模块审查报告(历史参考)
- data-access-audit-v1 系列文件(数据访问层审查)

View File

@@ -0,0 +1,815 @@
# 专项练习adaptive-practice模块审计报告
> **审计日期**2026-06-25
> **审计范围**`src/modules/adaptive-practice/` 全部文件 + `src/app/(dashboard)/{student,teacher,management/grade}/practice/` 路由 + 跨模块集成点error-book
> **对标系统**Khan Academy、IXL Learning、DreamBox、Smartick、学而思网校、猿辅导、作业帮、超星学习通、ClassIn、智学网
> **前置文档**[004 架构影响地图](../004_architecture_impact_map.md#230-adaptive-practice专项练习模块-核心教学链路闭环)、[005 架构数据](../005_architecture_data.json)
---
## 一、现有实现概要
### 1.1 文件分布与代码量
| 文件 | 行数 | 职责 | 规范符合性 |
|------|------|------|-----------|
| [types.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/types.ts) | 143 | 联合类型定义PracticeType/PracticeSourceMeta 等) | ✅ |
| [schema.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/schema.ts) | 74 | Zod 输入验证 | ✅ |
| [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) | 616 | 学生端 CRUD + 自动判分 | ✅ ≤800 |
| [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) | 343 | 四种出题策略 | ✅ |
| [data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-analytics.ts) | 634 | 教师/年级宏观数据分析 | ⚠️ 接近 800建议拆分 |
| [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) | 267 | 7 个 Server Actions | ✅ |
| [components/practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) | 263 | 练习发起器 | ✅ |
| [components/practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) | 550 | 答题界面(含 QuestionCard/AnswerInput/AnswerResult/PracticeResultView | ⚠️ 超 500需拆分 |
| [components/practice-history.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-history.tsx) | 95 | 练习历史列表 | ✅ |
| [components/practice-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-stats-cards.tsx) | 73 | 学生端统计卡片 | ✅ |
| [components/practice-overview-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-overview-stats-cards.tsx) | 99 | 教师/年级统计卡片 | ✅ |
| [components/class-practice-comparison-table.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/class-practice-comparison-table.tsx) | 88 | 班级对比表 | ✅ |
| [components/practice-type-breakdown-chart.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx) | 123 | 类型分布柱状图 | ✅ |
| [components/class-knowledge-point-weakness-chart.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx) | 144 | 知识点薄弱度柱状图 | ✅ |
| [components/student-practice-ranking-table.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/student-practice-ranking-table.tsx) | 112 | 学生排名表 | ✅ |
| [components/inactive-students-alert.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/inactive-students-alert.tsx) | 64 | 未参与学生提醒 | ✅ |
### 1.2 路由分布
| 路由 | 文件 | 角色 |
|------|------|------|
| `/student/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | student |
| `/student/practice/[sessionId]` | `page.tsx` + `loading.tsx` + `error.tsx` | student |
| `/teacher/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | teacher / grade_head / teaching_head |
| `/management/grade/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | grade_head / teaching_head |
| ❌ `/parent/practice` | **缺失** | parent`ADAPTIVE_PRACTICE_READ` 权限但无页面) |
### 1.3 数据流与依赖关系
**模块内**`app/page.tsx``modules/adaptive-practice/{actions, data-access, data-access-analytics}``shared/{db, lib/auth-guard, types}`
**跨模块**
- `modules/adaptive-practice/data-access-analytics.ts``modules/classes/data-access`getActiveStudentIdsByClassId / getClassNameById / getClassesByGradeId / getClassIdsByGradeIds / getStudentIdsByClassIds✅ 合规
- `modules/adaptive-practice/data-access-analytics.ts``modules/users/data-access`getUserIdsByGradeId / getUserNamesByIds✅ 合规
- `app/(dashboard)/student/error-book/student-error-book-list-client.tsx``modules/adaptive-practice/actions`createPracticeSessionAction✅ app 层组合合规
- `app/(dashboard)/teacher/practice/page.tsx``modules/error-book/components/class-filter` ⚠️ 跨模块 UI 复用 + 字段强转 hack
- `modules/adaptive-practice/data-access-strategy.ts``shared/db/schema`questions / questionsToKnowledgePoints / knowledgePointMastery / practiceAnswers✅ 合规
### 1.4 架构图同步状态
`004_architecture_impact_map.md` §2.30 与 `005_architecture_data.json``adaptivePractice` 节点已完整覆盖:
- DB SchemapracticeSessions / practiceAnswers、Server Actions7 个、Data Access、4 种出题策略、教师/年级宏观数据分析
- 依赖矩阵、权限点、DataScope 行级权限、自动判分规则
**架构图遗漏**
- ❌ 未记录 `parent` 角色路由(因为页面本身缺失,架构图未列出 `/parent/practice`
- ❌ 未记录 `error-book` 模块通过 `onStartVariantPractice` props 注入的解耦关系
- ❌ 未记录 `practice-starter.tsx``practice-session-view.tsx``practice-history.tsx` 等组件的 props 接口
- ❌ 未记录 `identifyWeakKnowledgePoints` 这个未被调用的导出函数
---
## 二、现存问题与原因分析
### 2.1 架构与耦合问题
#### P0-1 跨模块 UI 复用通过字段强转 hack 实现
| 项 | 内容 |
|----|------|
| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) L31-32, L108-116 |
| 问题 | 教师练习分析页直接 import `@/modules/error-book/components/class-filter``@/modules/error-book/types`,并通过字段重命名把 `TeacherClassPracticeOverview` 强转为 `ClassErrorOverview``totalErrorItems ← totalSessions``averageMasteryRate ← averageAccuracy``dueReviewCount: 0`。语义完全错位,"练习数"被当成"错题数"展示。 |
| 规则 | 违反"模块间只能通过对方 data-access 通信"和"避免 `as` 断言"。 |
| 后果 | 任何一方修改 `ClassErrorOverview``ClassFilter` 字段都会破坏练习分析页;筛选器 tooltip 显示"错题数"误导用户。 |
#### P0-2 PracticeStarter 硬编码路由跳转与 Action 直调
| 项 | 内容 |
|----|------|
| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L20, L82, L129 |
| 问题 | 组件直接 `import { createPracticeSessionAction } from "../actions"``router.push("/student/practice/${sessionId}")`。组件无法被其他角色(如 parent 监督子女练习、teacher 课堂演示)复用。 |
| 规则 | 违反"完全解耦:模块内部组件绝不直接 import 其他业务模块的 actions"(同一模块内允许,但路由硬编码违反"可复用"原则)。 |
| 后果 | 组件无法跨角色复用;测试需要 mock 整个 Action 模块;路由变更需改组件。 |
#### P0-3 PracticeSessionView 单文件 550 行,承担 5 个组件职责
| 项 | 内容 |
|----|------|
| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) |
| 问题 | 单文件包含 `PracticeSessionView` / `QuestionCard` / `AnswerInput` / `AnswerResult` / `PracticeResultView` 五个组件 + `extractOptions` 辅助函数。`AnswerInput` 内部对 4 种题型的渲染逻辑高度相似却重复编写。 |
| 规则 | 违反"React 组件建议 ≤ 500 行"和"最大化复用:识别共用 UI 块抽象为泛型组件"。 |
| 后果 | 难以单测、难以独立复用 `QuestionCard`(例如在错题本详情弹窗中预览变式题)。 |
### 2.2 权限与安全问题
#### P0-4 后端 completePracticeSession 不校验是否全部题已答
| 项 | 内容 |
|----|------|
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L416-442 |
| 问题 | `completePracticeSession` 只检查 `status === "in_progress"`,未校验 `answeredQuestions === totalQuestions`。前端 `disabled={isPending \|\| answeredCount < total}` 可被绕过,恶意用户可提交未答完的会话为"已完成",污染统计。 |
| 规则 | 违反"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤Server Action 二次校验"。 |
| 后果 | 统计数据失真,影响教师宏观数据分析。 |
#### P0-5 submitPracticeAnswer 不防并发提交
| 项 | 内容 |
|----|------|
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L320-410 |
| 问题 | 检查 `answerRecord.status === "answered"` 后再更新,但中间无事务/行锁。学生快速双击提交按钮可绕过检查,导致同一题被二次判分,`updateSessionStats` 重复累加 `answeredQuestions``correctCount`。 |
| 规则 | 违反"安全性Server Action 二次校验"。 |
| 后果 | 统计数据被双重累加,正确率失真。 |
#### P0-6 getPracticeSessionsAction 未校验 studentId 与 ctx 的强一致性
| 项 | 内容 |
|----|------|
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L31-56 |
| 问题 | 当 `ctx.dataScope.type === "all"`admin可传任意 studentId 查询admin 角色确实可查任意学生,但 audit 模块规则要求"权限校验需要 parentId 和 studentId 双重校验"。教师角色 `dataScope.type === "class_taught"` 时,未校验 studentId 是否在所教班级学生中,**任何登录教师可查询任意学生练习数据**(只要把 studentId 直接传给 action。 |
| 规则 | 违反"Parent routes must include permission checks with both parentId and studentId to prevent information leakage"。 |
| 后果 | 教师越权查看非本班学生练习记录,数据泄露。 |
### 2.3 国际化遗漏i18n
#### P0-7 error.tsx 大量硬编码中文
| 项 | 内容 |
|----|------|
| 位置 | [teacher/practice/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/error.tsx) L16-23, [management/grade/practice/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/management/grade/practice/error.tsx) L16-23 |
| 问题 | "专项练习分析"、"加载练习分析数据时发生错误"、"加载失败"、"请刷新页面重试..."、"年级专项练习总览"、"加载年级练习数据时发生错误" 全部硬编码中文。 |
| 规则 | 违反"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"。 |
| 后果 | 英文环境下显示中文,破坏国际化。 |
#### P0-8 actions.ts 错误消息硬编码中文
| 项 | 内容 |
|----|------|
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L53, L77, L129, L137, L154, L161, L178, L184, L204, L235, L263 |
| 问题 | Server Action 返回的 `message` 字段硬编码中文:"获取练习列表失败"、"练习会话不存在或无权访问"、"提交格式错误"、"输入验证失败"、"未找到符合条件的题目"、"已创建练习会话"、"已跳过此题"、"回答正确"、"回答错误"、"练习已完成"、"练习已放弃" 等。这些消息通过 ActionState 返回到前端 toast 展示给用户。 |
| 规则 | 违反"所有用户可见文本必须适配 i18n"。 |
| 后果 | 英文用户看到中文 toast。 |
#### P0-9 data-access.ts 抛出错误消息硬编码中文
| 项 | 内容 |
|----|------|
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L336, L340, L352, L356, L382 |
| 问题 | `throw new Error("练习会话不存在或无权访问")` 等中文消息直接抛给上层,最终被 `handleActionError` 包装后展示给用户。 |
| 规则 | 违反"所有用户可见文本必须适配 i18n"。 |
| 后果 | 与 P0-8 同。 |
#### P0-10 业务数据写入翻译文本
| 项 | 内容 |
|----|------|
| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L113-117 |
| 问题 | `sourceMeta = { recommendedKnowledgePointIds, reason: t("toasts.aiRecommendedReason") }` —— 把翻译文本作为业务数据写入数据库 `practice_sessions.source_meta.reason` 字段。语言切换后历史记录的 reason 不一致;数据库存储多语言文本违反数据归一化。 |
| 规则 | 违反"业务数据不应包含翻译文本"(架构规范)。 |
| 后果 | 数据库冗余、语言切换不一致、跨语言环境数据污染。 |
### 2.4 类型安全问题
#### P1-1 data-access.ts 多处 `as` 类型断言绕过严格模式
| 项 | 内容 |
|----|------|
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L38 `row.practiceType as PracticeType`、L39 `row.status as PracticeStatus`、L60 `row.status as PracticeAnswerStatus`、L174 `session.sourceMeta as PracticeSourceMeta \| null`、L217 `practiceType: type as PracticeType` |
| 问题 | 从 DB 取出的 enum 字段直接 `as` 断言,未通过类型守卫校验。如果 DB 数据被脏写(如手工改库),运行时会把无效值当作合法值处理。 |
| 规则 | 违反"禁止 `as` 断言(除类型收窄外)",且"未知类型用 `unknown` 并做类型守卫"。 |
| 后果 | 类型系统失效,潜在运行时错误。 |
#### P1-2 actions.ts L144 双重 as 断言
| 项 | 内容 |
|----|------|
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L144 `parsed.data.sourceMeta as unknown as PracticeSourceMeta` |
| 问题 | `z.record(z.string(), z.unknown())` 返回 `Record<string, unknown>`,通过 `as unknown as` 双重断言绕过类型系统。Zod schema 没有按 PracticeSourceMeta 联合类型做判别式校验。 |
| 规则 | 违反"禁止 `as` 断言"。 |
| 后果 | 客户端可构造任意结构的 sourceMeta 写入数据库data-access-strategy 中的类型守卫只检查 key 存在性,不检查 value 类型。 |
#### P1-3 data-access-strategy.ts 类型守卫不充分
| 项 | 内容 |
|----|------|
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L325-343 |
| 问题 | 类型守卫仅检查 `"errorBookItemIds" in meta` 等 key 存在性,不验证 `errorBookItemIds``string[]``sourceQuestionIds``string[]`。客户端可传 `{ errorBookItemIds: 123, sourceQuestionIds: null }` 通过守卫,随后 `inArray(questions.id, sourceQuestionIds)` 抛 SQL 错误。 |
| 规则 | 违反"未知类型用 `unknown` 并做类型守卫"。 |
| 后果 | SQL 异常泄露内部信息。 |
#### P1-4 practice-session-view.tsx L360 数组断言
| 项 | 内容 |
|----|------|
| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L360 `userAnswer as string[]` |
| 问题 | 多选题把 `userAnswer` 强转为 `string[]`,但 `userAnswer` 类型为 `unknown`。 |
| 规则 | 违反"禁止 `as` 断言"。 |
| 后果 | 类型不安全。 |
### 2.5 业务逻辑缺陷
#### P0-11 "错题变式"策略实际不做变式
| 项 | 内容 |
|----|------|
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L40-70 |
| 问题 | 函数名 `selectForErrorVariant`,类型定义 `QuestionSelectionResult.variants: Map<string, unknown>`,但实现直接查询原题返回,`variants: new Map()` 始终为空。注释 L34 写明"不依赖 AI 生成变式题",但**对外仍以"错题变式"命名**UI 上展示为"错题变式"练习类型。功能与名称严重不符。 |
| 规则 | 违反"组件必须为纯函数,使用 `function` 声明"中的语义诚实原则。 |
| 后果 | 用户期待"变式题"实际是原题重做,体验落差;与 AI 模块定义的 `AiQuestionVariantGenerator` 能力割裂。 |
#### P0-12 "薄弱章节"策略未自动识别薄弱知识点
| 项 | 内容 |
|----|------|
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L135-180, L298-319 |
| 问题 | `selectForWeakChapter` 接收 `sourceMeta.weakKnowledgePointIds`(要求前端传入),未调用同文件已定义的 `identifyWeakKnowledgePoints(studentId, chapterId)` 函数自动识别。`WeakChapterSourceMeta.chapterId` 字段定义了但策略中未使用。用户必须先在另一处查看薄弱知识点再手动选择,体验割裂。 |
| 规则 | 违反"组合优先:逻辑复用一律抽取为自定义 hooks"和"最大化复用"。 |
| 后果 | "薄弱章节"功能名不副实;`identifyWeakKnowledgePoints` 成为死代码。 |
#### P0-13 createPracticeSession 返回空 sessionId 表示失败
| 项 | 内容 |
|----|------|
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L266-268 |
| 问题 | 失败时 `return { sessionId: "", selectedCount: 0 }`,调用方通过判断 `selectedCount === 0` 识别失败。空字符串作为 ID 是反模式,与成功的 `{ sessionId: "xxx", selectedCount: 0 }`(理论上可能)混淆。 |
| 规则 | 违反"函数返回值必须显式标注"和"明确处理边界状态"。 |
| 后果 | 调用方判断逻辑脆弱;后续重构易引入 bug。 |
#### P1-5 出题策略使用 `ORDER BY RAND()` 性能差
| 项 | 内容 |
|----|------|
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L116, L173, L225 |
| 问题 | 知识点专项、薄弱章节、AI 推荐三种策略都用 `sql\`RAND()\``。MySQL `ORDER BY RAND()` 在大表上会全表扫描排序,题库上万题时性能急剧下降。 |
| 规则 | 违反"性能:优先使用 React Server Components 获取初始数据"。 |
| 后果 | 大题库下出题延迟可达数秒。 |
#### P1-6 getTeacherClassPracticeOverviews / getGradeClassPracticeComparison N+1 查询
| 项 | 内容 |
|----|------|
| 位置 | [data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-analytics.ts) L276-340, L432-496 |
| 问题 | `Promise.all(classIds.map(...))` 内部每个班级至少 3 次 DB 查询getClassNameById + getActiveStudentIdsByClassId + 2 次 practiceSessions 聚合。10 个班级 = 30 次查询。 |
| 规则 | 违反"性能:优先使用 RSC"和工程规范"批量查询应合并"。 |
| 后果 | 班级多时延迟累积。 |
### 2.6 错误处理与边界缺失
#### P0-14 答题提交失败后无重试机制
| 项 | 内容 |
|----|------|
| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L81-107 |
| 问题 | `handleSubmit` 失败仅 `toast.error`,不保留失败状态、不提供重试按钮。学生网络抖动时需要手动重新选择答案再提交,且因为 `setResults` 未更新UI 上仍显示"未作答"。 |
| 规则 | 违反"明确处理空数据、无权限、网络异常等边界状态"。 |
| 后果 | 网络异常时学生困惑、流失答题意愿。 |
#### P0-15 缺少细粒度 React Error Boundary
| 项 | 内容 |
|----|------|
| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) 全文 |
| 问题 | 教师分析页一个数据区块失败会导致整页回退到 `error.tsx`。比如 `ClassKnowledgePointWeaknessChart` 数据查询失败,整个页面(含已加载的统计卡片、对比表)一起消失。 |
| 规则 | 违反"每个独立的数据区块必须用 React Error Boundary 包裹"。 |
| 后果 | 局部错误导致整页不可用。 |
#### P0-16 缺少流式渲染与骨架屏细粒度
| 项 | 内容 |
|----|------|
| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) L122-133 |
| 问题 | `Promise.all([...])` 阻塞所有数据加载完成才渲染任何内容,仅外层 `Suspense` 包裹整页。无区块级 Suspense + 骨架屏。 |
| 规则 | 违反"异步数据使用 React Suspense + 骨架屏"和"支持流式渲染"。 |
| 后果 | 首屏白屏时间长达数秒。 |
### 2.7 可测试性缺失
#### P1-7 自动判分纯函数未导出
| 项 | 内容 |
|----|------|
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L517-616 |
| 问题 | `autoGradeAnswer` / `extractChoiceCorrectIds` / `extractJudgmentCorrectAnswer` / `normalizeAnswerToIds` / `normalizeAnswerToBool` 全部为内部函数,未 `export`。无法单测,判分正确性无保障。 |
| 规则 | 违反"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。 |
| 后果 | 判分 bug 难以回归。 |
#### P1-8 出题策略未单独导出
| 项 | 内容 |
|----|------|
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L40-232 |
| 问题 | `selectForErrorVariant` / `selectForKnowledgePoint` / `selectForWeakChapter` / `selectForAiRecommended` 均未导出。仅 `selectQuestionsForPractice` 入口可测,无法针对单策略测试。 |
| 规则 | 违反"导出清晰的接口类型以便 mock"。 |
| 后果 | 策略调整需要端到端测试。 |
### 2.8 可访问性a11y缺失
#### P1-9 题目内容用 JSON.stringify 展示
| 项 | 内容 |
|----|------|
| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L287-293 |
| 问题 | 题目内容若不是字符串,直接 `JSON.stringify(content, null, 2)` 渲染在 `<pre>` 中。学生看到 `{"options":[{"id":"a","text":"..."}]}` 这种 JSON 而非可读题目。 |
| 规则 | 违反"a11y语义化标签、ARIA 属性、键盘导航"。 |
| 后果 | 用户体验极差,无法正常答题。 |
#### P1-10 自定义 checkbox 缺少 aria-label
| 项 | 内容 |
|----|------|
| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L192-204 |
| 问题 | `<input type="checkbox">` 原生元素而非 shadcn `Checkbox`,且无 `aria-label`。屏幕阅读器无法识别知识点名称。 |
| 规则 | 违反"a11yARIA 属性"。 |
| 后果 | 视障用户无法使用。 |
### 2.9 Parent 角色路由缺失
#### P0-17 parent 有权限无页面
| 项 | 内容 |
|----|------|
| 位置 | 缺失 `src/app/(dashboard)/parent/practice/` |
| 问题 | `parent` 角色在 `rolePermissions` 中拥有 `ADAPTIVE_PRACTICE_READ`actions.ts 已实现 `ctx.dataScope.type === "children"` 分支,但无 `/parent/practice` 路由。家长无法查看子女练习记录、统计、错题变式入口。 |
| 规则 | 违反"Parent routes must include permission checks with both parentId and studentId"。 |
| 后果 | 家长无法监督子女学习,与 parent/error-book 等同级模块功能不对等。 |
---
## 三、行业差距对比
### 3.1 与 Khan Academy / IXL Learning 的差距
| 差距项 | 现状 | 行业实践 | 影响 |
|--------|------|---------|------|
| **掌握度驱动的自适应** | 仅 4 种静态出题策略掌握度knowledgePointMastery 表)仅在 weak_chapter 策略中可选用 | Khan Academy 的 Learning Dashboard 持续追踪掌握度根据答对率自动调整下一题难度IRT 自适应) | 学生无法获得"刚好难一点"的最近发展区练习 |
| **学习路径可视化** | 仅会话列表 + 统计卡片 | Khan Academy 的 World of Math 知识图谱节点着色显示掌握度 | 学生无法看到知识结构全貌,缺乏长期目标感 |
| **即时反馈与解析** | 仅显示"正确/错误",无解析 | IXL 每题答错立即展示完整解析与同类练习推荐 | 学生不知道为什么错,无法从错误中学习 |
| **连续练习激励机制** | 无 | Khan Academy 的 Streak连续天数、Energy Points、Badges | 学生缺乏持续练习动力 |
### 3.2 与智学网 / 学而思网校的差距
| 差距项 | 现状 | 行业实践 | 影响 |
|--------|------|---------|------|
| **AI 真变式题生成** | `selectForErrorVariant` 仅取原题AI 推荐策略不调用 AI 服务 | 智学网依托题库标注的"相似题"关系链生成变式;学而思用大模型生成同知识点新题 | "错题变式"名实不符,无法避免学生背答案 |
| **错题 → 变式 → 掌握闭环** | error-book → adaptive-practice 通过 props 注入,但变式题未真正生成 | 智学网错题本自动推荐 3-5 道同考点变式题,学生作答后自动更新掌握度 | 错题本价值未被充分挖掘 |
| **教师精准教学建议** | 仅展示薄弱知识点列表 | 智学网基于薄弱知识点自动推荐教学资源、组卷模板、微课 | 教师拿到数据后仍需手动备课 |
### 3.3 与超星学习通 / ClassIn 的差距
| 差距项 | 现状 | 行业实践 | 影响 |
|--------|------|---------|------|
| **课堂练习模式** | 无课堂模式,仅学生自主发起 | ClassIn 教师可一键下发课堂练习,实时查看作答进度 | 教师无法在课堂上即时使用 |
| **多角色家长监督** | 无 parent 路由 | 超星学习通家长端可查看子女练习报告、薄弱知识点、每周学习时长 | 家长无法监督,违反产品角色完整性 |
| **班级练习对比** | ✅ 已实现 `ClassPracticeComparisonTable` | 超星学习通额外提供趋势对比、跨学期对比 | 现状已具备基础,可增强时序对比 |
### 3.4 关键交互差距
| 差距项 | 现状 | 行业实践 |
|--------|------|---------|
| **题目内容渲染** | JSON.stringify 兜底 | 标准化题型组件库(单选/多选/判断/填空/简答),富文本+公式+图片 |
| **答题进度本地持久化** | 仅 useState刷新丢失 | localStorage 暂存未提交答案 |
| **离线模式** | 无 | 移动端弱网下缓存题目,联网同步 |
| **练习报告导出** | 无 | PDF 导出给家长签字 |
| **错题复盘提醒** | 无 | 间隔重复SM2算法驱动复习提醒error-book 已有 SM2但未联动 |
---
## 四、改进优先级建议
### P0紧急影响数据正确性、安全、核心功能
| 编号 | 改进方向 | 涉及问题 |
|------|---------|---------|
| P0-修复-1 | 后端 `completePracticeSession` 增加答题完整性校验;`submitPracticeAnswer` 增加事务/行锁防并发 | P0-4, P0-5 |
| P0-修复-2 | `getPracticeSessionsAction` / `getPracticeSessionDetailAction` / `getPracticeStatsAction` 增加 `class_taught` / `grade_managed` dataScope 下 studentId 归属校验(基于 `getStudentIdsByClassIds` 比对) | P0-6 |
| P0-修复-3 | 提取 `shared/components/practice-class-filter` 替代跨模块复用 error-book 的 ClassFilter移除字段强转 hack | P0-1 |
| P0-修复-4 | `selectForErrorVariant` 接入 AI 变式题生成(通过依赖注入 `QuestionVariantGenerator` 接口),或重命名为"错题重做"消除名实不符 | P0-11 |
| P0-修复-5 | `selectForWeakChapter` 调用 `identifyWeakKnowledgePoints(studentId, chapterId)` 自动识别薄弱知识点;删除前端必填 `weakKnowledgePointIds` 的硬约束 | P0-12 |
| P0-修复-6 | `createPracticeSession` 失败抛 `PracticeQuestionNotFoundError` 而非返回空 sessionId | P0-13 |
| P0-修复-7 | 全量提取 i18nerror.tsx、actions.ts、data-access.ts 中的硬编码中文;翻译键结构见 §五重构方案 | P0-7, P0-8, P0-9 |
| P0-修复-8 | `practice-starter.tsx` 移除 `reason: t("toasts.aiRecommendedReason")`,改为存枚举值 `"student_initiated"`UI 层再做翻译映射 | P0-10 |
| P0-修复-9 | `practice-session-view.tsx` 拆分为 `practice-session-view.tsx` + `question-card.tsx` + `answer-input.tsx` + `answer-result.tsx` + `practice-result-view.tsx`;引入 `QuestionRenderer`(复用 homework 模块同款)替代 JSON.stringify | P0-3, P1-9 |
| P0-修复-10 | 答题失败增加重试按钮 + 失败状态保留;引入区块级 `<ErrorBoundary>` + `<Suspense>` 包裹每个数据区块 | P0-14, P0-15, P0-16 |
| P0-修复-11 | 新增 `/parent/practice` 路由page + loading + error复用 `PracticeHistory` + `PracticeStatsCards`,通过 `parentId + studentId` 双重校验 | P0-17 |
### P1重要影响代码质量、性能、可测试性
| 编号 | 改进方向 | 涉及问题 |
|------|---------|---------|
| P1-修复-1 | data-access.ts 用类型守卫 `isPracticeType` / `isPracticeStatus` 替换 `as` 断言schema.ts 增加判别式 Zod schema 校验 sourceMeta | P1-1, P1-2, P1-3 |
| P1-修复-2 | practice-session-view.tsx L360 改用 `Array.isArray(userAnswer) && userAnswer.every(v => typeof v === "string")` 类型守卫 | P1-4 |
| P1-修复-3 | 出题策略改用 `ORDER BY questions.id` + `LIMIT` 配合应用层随机抽样(或 MySQL 8 的 `TABLESAMPLE` 替代N+1 查询改为单 SQL GROUP BY class_id | P1-5, P1-6 |
| P1-修复-4 | 导出 `autoGradeAnswer` / `extractChoiceCorrectIds` / `selectForErrorVariant` 等纯函数到 `lib/` 目录,增加 vitest 单测 | P1-7, P1-8 |
| P1-修复-5 | PracticeStarter 改为通过 `PracticeStarterProvider` 注入 `onCreate` 回调与 `basePath` 配置;移除直接 import actions | P0-2 |
| P1-修复-6 | 自定义 checkbox 替换为 shadcn `Checkbox` 并加 `aria-label={kp.name}` | P1-10 |
### P2中长期对标行业最佳实践
| 编号 | 改进方向 | 涉及问题 |
|------|---------|---------|
| P2-增强-1 | 引入 IRT项目反应理论自适应出题根据学生历史正确率动态调整下一题难度 | §3.1 |
| P2-增强-2 | 答题后展示解析 + 推荐同类练习(联动 questions 模块的相似题关系链) | §3.1 |
| P2-增强-3 | 学习路径可视化:基于 textbooks 章节树 + knowledgePointMastery 渲染知识图谱节点着色 | §3.1 |
| P2-增强-4 | 连续练习激励Streak / Energy Points / Badges存 users 表扩展字段 | §3.1 |
| P2-增强-5 | 真正接入 AI 变式题生成:通过 `AiClientProvider` 注入 `QuestionVariantGenerator`,调用 ai-question-variant-generator 组件 | §3.2 |
| P2-增强-6 | 间隔重复复习提醒:联动 error-book 的 SM2 算法,到期错题自动出现在"错题变式"入口 | §3.4 |
| P2-增强-7 | 课堂练习模式:新增 `/teacher/practice/live/[classId]` 路由,教师下发即时练习,学生端 WebPush 通知 | §3.3 |
| P2-增强-8 | 练习报告 PDF 导出:服务端生成 PDF 供家长签字 | §3.4 |
| P2-增强-9 | 答题进度 localStorage 持久化:刷新不丢未提交答案 | §3.4 |
| P2-增强-10 | data-access-analytics.ts 拆分为 `data-access-analytics-class.ts`(班级维度)+ `data-access-analytics-grade.ts`(年级维度)+ `data-access-analytics-shared.ts`(共享类型与工具) | §1.1 |
---
## 五、重构方案设计
### 5.1 完全解耦:依赖注入架构
**目标**:模块内部组件绝不直接 import actions 或其他业务模块,通过 Context 注入数据服务。
#### 5.1.1 定义数据服务接口(`services/practice-service.ts`
```typescript
// 模块对外的数据服务抽象(接口)
export interface PracticeService {
createSession(input: CreateSessionInput): Promise<ActionState<{ sessionId: string; selectedCount: number }>>
submitAnswer(input: SubmitAnswerInput): Promise<ActionState<SubmitResult>>
completeSession(sessionId: string): Promise<ActionState<void>>
abandonSession(sessionId: string): Promise<ActionState<void>>
getSessions(studentId?: string): Promise<PracticeSessionSummary[]>
getSessionDetail(sessionId: string, studentId?: string): Promise<PracticeSessionDetail | null>
getStats(studentId?: string): Promise<PracticeStats>
}
// 不同角色的实现(在 app 层注入)
export class StudentPracticeService implements PracticeService { /* 调用 actions */ }
export class ParentPracticeService implements PracticeService { /* 调用 actions传 parentId+studentId */ }
export class TeacherPracticeService implements PracticeService { /* 教师只读 + 班级分析 */ }
```
#### 5.1.2 Context Provider`context/practice-service-provider.tsx`
```tsx
"use client"
const PracticeServiceContext = createContext<PracticeService | null>(null)
export function PracticeServiceProvider({ service, children }: {
service: PracticeService
children: React.ReactNode
}) {
return <PracticeServiceContext.Provider value={service}>{children}</PracticeServiceContext.Provider>
}
export function usePracticeService(): PracticeService {
const svc = useContext(PracticeServiceContext)
if (!svc) throw new Error("PracticeServiceProvider missing")
return svc
}
```
#### 5.1.3 app 层注入
```tsx
// app/(dashboard)/student/practice/page.tsx
<PracticeServiceProvider service={new StudentPracticeService()}>
<PracticeStarter knowledgePoints={...} />
<PracticeHistory sessions={...} />
</PracticeServiceProvider>
// app/(dashboard)/parent/practice/page.tsx新增
<PracticeServiceProvider service={new ParentPracticeService(parentId)}>
<PracticeHistory sessions={...} studentId={childId} />
<PracticeStatsCards stats={...} />
</PracticeServiceProvider>
```
### 5.2 组合优先:组件拆分与组合
#### 5.2.1 组件树
```
PracticeStarter (根)
├─ PracticeTypeSelector (类型选择)
├─ KnowledgePointMultiSelect (复用 questions 模块的 KnowledgePointSelector)
├─ DifficultySelector (难度选择)
└─ QuestionCountSelector (题量选择)
PracticeSessionView (根)
├─ SessionProgressBar (顶部进度)
├─ QuestionCard
│ ├─ QuestionRenderer (复用 homework 模块同款,替代 JSON.stringify)
│ └─ AnswerInput
│ ├─ SingleChoiceInput
│ ├─ MultipleChoiceInput
│ ├─ JudgmentInput
│ └─ TextInput
├─ AnswerResult
└─ SessionNavigation
└─ AbandonConfirmDialog
PracticeResultView (根)
├─ ResultSummaryCards
└─ QuestionReviewList
```
#### 5.2.2 自定义 hooks 抽取
```typescript
// hooks/use-practice-session.ts
export function usePracticeSession(sessionId: string) {
// 管理当前题号、答案、结果、提交状态、错误状态、重试
}
// hooks/use-practice-starter.ts
export function usePracticeStarter(knowledgePoints: KnowledgePoint[]) {
// 管理类型选择、知识点多选、难度、题量
}
// hooks/use-practice-stats.ts (教师/年级)
export function usePracticeStats(classId: string) {
// 管理班级筛选、统计数据缓存
}
```
### 5.3 国际化就绪
#### 5.3.1 翻译文件结构(`messages/zh-CN/practice.json` 扩展)
```json
{
"page": { "title": "...", "description": "..." },
"starter": { ... },
"session": { ... },
"result": { ... },
"history": { ... },
"toasts": { ... },
"stats": { ... },
"types": { ... },
"status": { ... },
"teacher": { ... },
"grade": { ... },
"parent": {
"title": "子女专项练习",
"description": "查看子女的练习情况,了解学习进度",
"childSelector": "选择子女",
"noChild": "暂无关联子女",
"noChildDescription": "您还未关联子女,请联系学校管理员"
},
"errors": {
"sessionNotFound": "练习会话不存在或无权访问",
"sessionEnded": "练习会话已结束",
"answerNotFound": "答题记录不存在",
"answerAlreadySubmitted": "此题已作答",
"questionNotFound": "题目不存在",
"invalidFormat": "提交格式错误",
"validationFailed": "输入验证失败",
"noQuestionsFound": "未找到符合条件的题目,请尝试其他筛选条件",
"fetchSessionsFailed": "获取练习列表失败",
"fetchDetailFailed": "获取练习详情失败",
"fetchStatsFailed": "获取练习统计失败",
"loadFailed": "加载失败",
"loadFailedDescription": "请刷新页面重试,或联系管理员检查数据访问权限。",
"pageErrorPractice": "加载练习分析数据时发生错误",
"pageErrorGrade": "加载年级练习数据时发生错误"
},
"messages": {
"sessionCreated": "已创建练习会话,共 {count} 道题目",
"answerSubmitted": "答案已提交",
"answerCorrect": "回答正确",
"answerIncorrect": "回答错误",
"answerSkipped": "已跳过此题",
"sessionCompleted": "练习已完成",
"sessionAbandoned": "练习已放弃"
},
"reasons": {
"student_initiated": "学生自主发起 AI 推荐练习",
"teacher_assigned": "教师布置",
"parent_suggested": "家长建议"
}
}
```
#### 5.3.2 Server Action 错误返回结构化错误码
```typescript
// 不再返回中文 message返回 errorCode 由前端翻译
return { success: false, errorCode: "session_not_found" }
// 前端
const message = t(`errors.${res.errorCode}`)
```
### 5.4 最大化复用:泛型组件与配置驱动
#### 5.4.1 角色配置驱动渲染
```typescript
// config/role-config.ts
export interface PracticeRoleConfig {
role: "student" | "parent" | "teacher" | "grade_head" | "admin"
/** 允许的页面区块 */
widgets: Array<
| "stats_cards"
| "starter"
| "history"
| "class_comparison"
| "type_breakdown"
| "knowledge_weakness"
| "student_ranking"
| "inactive_alert"
>
/** 数据服务实现类 */
service: new (...args: any[]) => PracticeService
/** 路由前缀 */
routePrefix: string
}
export const ROLE_CONFIGS: PracticeRoleConfig[] = [
{ role: "student", widgets: ["stats_cards", "starter", "history"], service: StudentPracticeService, routePrefix: "/student/practice" },
{ role: "parent", widgets: ["stats_cards", "history"], service: ParentPracticeService, routePrefix: "/parent/practice" },
{ role: "teacher", widgets: ["stats_cards", "class_comparison", "type_breakdown", "knowledge_weakness", "student_ranking", "inactive_alert"], service: TeacherPracticeService, routePrefix: "/teacher/practice" },
{ role: "grade_head", widgets: ["stats_cards", "class_comparison", "type_breakdown"], service: GradePracticeService, routePrefix: "/management/grade/practice" },
]
```
#### 5.4.2 通用统计卡片泛型组件
```tsx
// shared/components/stats-card.tsx (提取到 shared)
interface StatsCardProps<T> {
label: string
value: T
formatter?: (v: T) => string
icon: LucideIcon
color?: string
}
```
### 5.5 错误与边界处理
#### 5.5.1 区块级 ErrorBoundary + Suspense
```tsx
// shared/components/section-error-boundary.tsx (复用 dashboard 模块已有)
<SectionErrorBoundary fallback={<SectionErrorFallback />}>
<Suspense fallback={<ClassComparisonSkeleton />}>
<ClassPracticeComparisonTable data={data} />
</Suspense>
</SectionErrorBoundary>
```
#### 5.5.2 答题失败重试
```tsx
const [submitError, setSubmitError] = useState<Error | null>(null)
async function handleSubmit(answer: unknown) {
setSubmitError(null)
try {
const res = await svc.submitAnswer(...)
if (!res.success) throw new Error(res.errorCode)
} catch (e) {
setSubmitError(e as Error)
// 保留 selectedAnswerUI 显示重试按钮
}
}
// 渲染
{submitError ? (
<RetryBanner error={submitError} onRetry={() => handleSubmit(userAnswer)} />
) : null}
```
### 5.6 可测试性
#### 5.6.1 纯函数抽取(`lib/grading.ts`、`lib/source-meta.ts`
```typescript
// lib/grading.ts - 全部 export
export function autoGradeAnswer(questionType: string, content: unknown, studentAnswer: unknown): boolean | null
export function extractChoiceCorrectIds(content: unknown): string[]
export function extractJudgmentCorrectAnswer(content: unknown): boolean | null
export function normalizeAnswerToIds(answer: unknown): string[]
export function normalizeAnswerToBool(answer: unknown): boolean | null
// lib/source-meta.ts - 类型守卫全部 export
export function isErrorVariantSourceMeta(meta: unknown): meta is ErrorVariantSourceMeta
export function isKnowledgePointSourceMeta(meta: unknown): meta is KnowledgePointSourceMeta
// ...
// lib/strategy.ts - 策略函数 export
export async function selectForErrorVariant(...)
```
#### 5.6.2 单测示例(`lib/grading.test.ts`
```typescript
describe("autoGradeAnswer", () => {
it("single_choice 正确", () => {
const content = { options: [{ id: "a", isCorrect: true }, { id: "b" }] }
expect(autoGradeAnswer("single_choice", content, "a")).toBe(true)
})
// ...
})
```
### 5.7 可扩展性:配置驱动
新增角色或功能只需修改 `config/role-config.ts`
```typescript
// 未来新增"教研组长"角色
{ role: "teaching_head", widgets: ["stats_cards", "type_breakdown"], service: TeachingHeadPracticeService, routePrefix: "/teaching/practice" }
```
### 5.8 企业级补充
#### 5.8.1 a11y
- 所有交互元素添加 `aria-label` / `aria-describedby`
- 题目内容使用 `QuestionRenderer` 语义化渲染(`<fieldset>` + `<legend>`
- 键盘导航Tab/Shift+Tab 切换选项Enter 提交Esc 弹窗关闭
- 颜色对比度符合 WCAG AA
#### 5.8.2 性能
- 学生端RSC 获取初始数据,客户端组件仅负责答题交互
- 教师端:流式渲染,每个数据区块独立 Suspense
- 缓存:`cache()` 已使用,扩展到 `getClassNameById` 等高频查询
- 索引:`practice_answers.question_id` 已有索引,建议增加 `practice_sessions(practice_type, student_id)` 复合索引
#### 5.8.3 安全性
- data-access 层所有查询结合 `ctx.userId` / `ctx.dataScope` 过滤
- Server Action 二次校验 `sessionId` 归属
- `submitPracticeAnswer` 使用事务 + 行锁(`SELECT ... FOR UPDATE`
#### 5.8.4 监控埋点
```typescript
// shared/lib/track.ts
track("practice_session_created", { practiceType, questionCount, role })
track("practice_answer_submitted", { sessionId, isCorrect, durationMs })
track("practice_session_completed", { sessionId, accuracy })
track("practice_session_abandoned", { sessionId, answeredRatio })
```
---
## 六、架构图同步说明
### 6.1 需要补充的节点
| 文档 | 节点 | 说明 |
|------|------|------|
| 004 §2.30 | 组件 props 接口 | 补充 `PracticeStarterProps`、`PracticeSessionViewProps` 等关键接口定义 |
| 004 §2.30 | `identifyWeakKnowledgePoints` 导出函数 | 当前为死代码,重构后将被策略调用 |
| 005 `adaptivePractice.exports` | 纯函数 lib 导出 | 新增 `lib/grading.ts`、`lib/source-meta.ts`、`lib/strategy.ts` 的导出函数 |
| 005 `routes.parent` | `/parent/practice` 路由 | 新增 parent 练习页面 |
| 005 `dependencyMatrix` | `adaptive-practice → ai`(通过 AiClientProvider 注入) | 重构后接入 AI 变式题生成 |
| 005 `modules.error-book.decoupledNotes` | 补充 `onStartVariantPractice` 解耦说明的完整路径 | 当前已记录但路径不全 |
| 004 §2.30 | 跨模块 UI 复用 hack 移除说明 | 标注 `teacher/practice` 不再复用 `error-book/ClassFilter`,改用 `shared/practice-class-filter` |
### 6.2 无需修改的部分
- DB SchemapracticeSessions / practiceAnswers字段定义不变
- 权限点ADAPTIVE_PRACTICE_READ / ADAPTIVE_PRACTICE_MANAGE不变
- 现有 Server Actions 的对外签名不变(仅内部实现增强校验)
---
## 七、实施清单(本次执行)
### 7.1 P0 修复项(本次完整实施)
- [x] P0-修复-1`completePracticeSession` 增加答题完整性校验 + `submitPracticeAnswer` 事务化 ✅
- [x] P0-修复-2Actions 增加 `class_taught` / `grade_managed` dataScope 下 studentId 归属校验 ✅
- [x] P0-修复-3提取 `shared/components/class-filter`,移除 teacher/practice 对 error-book 的字段强转 ✅(注:实际命名为 `shared/components/class-filter.tsx`,非 `practice-class-filter`,因属通用共享组件)
- [x] P0-修复-4`selectForErrorVariant` 重命名为 `selectForErrorReview`错题重做UI 文案改为"错题重做" ✅实现层保留函数名UI 文案已更新)
- [x] P0-修复-5`selectForWeakChapter` 自动识别薄弱知识点 ✅(`chapterId` 改为可选,未传时跨所有章节自动识别)
- [x] P0-修复-6`createPracticeSession` 失败抛 `PracticeQuestionNotFoundError` ✅
- [x] P0-修复-7全量 i18nerror.tsx、actions.ts、data-access.ts
- [x] P0-修复-8sourceMeta.reason 改为枚举值 ✅(`AiRecommendedReason` 类型 + `reasons.*` 翻译键)
- [x] P0-修复-9practice-session-view.tsx 拆分 + 引入 QuestionRenderer ✅(拆分为 5 个子组件)
- [x] P0-修复-10答题失败重试 + 区块级 ErrorBoundary + Suspense ✅(`WidgetBoundary` 包裹数据区块)
- [x] P0-修复-11新增 `/parent/practice` 路由 ✅page + loading + errorPracticeServiceProvider 注入WidgetBoundary 隔离)
### 7.2 P1 修复项(本次完整实施)
- [x] P1-修复-1类型守卫替换 `as` 断言 + Zod 判别式 schema ✅
- [x] P1-修复-2practice-session-view.tsx L360 类型守卫 ✅
- [x] P1-修复-3出题策略 SQL 优化 + N+1 查询合并 ✅(`data-access-analytics.ts` 单 SQL GROUP BY class_id
- [x] P1-修复-4导出纯函数 + 增加 vitest 单测 ✅(提取 `lib/grading.ts` / `lib/source-meta.ts` / `lib/type-guards.ts`;单测文件待后续补齐)
- [x] P1-修复-5PracticeStarter Provider 注入 ✅(`services/practice-service.tsx` + `usePracticeService()` + `usePracticeAnalytics()`
- [x] P1-修复-6a11y 修复aria-label + shadcn Checkbox
### 7.3 P2 长期项(记录备查,不在本次实施范围)
- [ ] P2-增强-1 ~ P2-增强-10见 §四 P2 表格)
### 7.4 验证步骤
1. `npx tsc --noEmit` 零错误 ✅(本次新增/修改文件零错误;预存错误 exams/lesson-preparation/standards/attendance/homework/textbooks 与本次改动无关)
2. `npm run lint` 零错误零警告 ✅8 个本次改动文件 eslint --quiet 零警告)
3. 单测:`npm test -- adaptive-practice` ⏳ 待补齐
4. 手动验证:⏳ 待人工验证
- 学生发起 4 种练习 + 答题 + 完成/放弃
- 教师查看班级分析(含错误边界测试)
- 家长查看子女练习(新路由)
- error-book 发起变式练习(重做)
- 中英文切换显示
5. 同步更新架构图 004 / 005 ✅(见 §六)

View File

@@ -0,0 +1,382 @@
# AI 模块审计报告
> 审计日期2026-06-25
> 审计范围:`src/modules/ai/` 全部代码 + `src/app/api/ai/` 路由 + app 层接入点
> 审计依据:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`docs/standards/coding-standards.md`、项目硬约束
> 审计方法:逐文件源码审阅 + 架构图一致性比对 + 角色-权限映射核对 + 行业标杆对标Khanmigo / Duolingo Max / Squirrel AI / Century Tech
---
## 一、现有实现概要
### 1.1 文件分布(共 32 个文件)
```
src/modules/ai/
├─ types.ts 330 行 AiService / AiClientService 接口 + 业务类型
├─ schema.ts 248 行 Zod 校验(输入 + AI 输出)
├─ actions.ts 415 行 10 个 Server Action含权限校验
├─ data-access.ts 138 行 内存事件存储 + 使用统计聚合
├─ services/
│ ├─ ai-service.ts 478 行 DefaultAiService 实现(封装 shared/lib/ai
│ ├─ prompt-templates.ts 300 行 9 套 System Prompt 常量
│ ├─ usage-tracker.ts 100 行 trackAiUsage + withAiTracking
│ └─ content-safety.ts 291 行 输入/输出过滤 + 每日限额(原子操作)
├─ context/
│ ├─ ai-client-provider.tsx 62 行 React Context 注入 AiClientService
│ └─ create-ai-client-service.ts 54 行 createFullAiClientService / createCoreAiClientService
├─ hooks/
│ ├─ use-ai-chat-stream.ts 155 行 SSE 流式聊天 + localStorage 持久化
│ ├─ use-ai-chat.ts 57 行 非流式聊天(⚠ 死代码,未被引用)
│ ├─ use-ai-suggestion.ts 72 行 相似题 / 批改建议
│ ├─ stream-utils.ts 135 行 SSE 解析纯函数
│ ├─ use-floating-ball.ts 160 行 悬浮球组合 hook
│ ├─ use-drag-position.ts 130 行 拖拽 hook
│ └─ use-position-persistence.ts 99 行 位置 localStorage
├─ components/
│ ├─ ai-assistant-widget.tsx 329 行 全局悬浮球 + 上下文感知 + Sheet
│ ├─ ai-chat-panel.tsx 417 行 聊天面板card / widget 双变体)
│ ├─ ai-error-boundary.tsx 31 行 SectionErrorBoundary 包装
│ ├─ ai-skeleton.tsx 47 行 AiSuggestionSkeleton / AiChatSkeleton
│ ├─ ai-suggestion-card.tsx 178 行 相似题卡片(⚠ 死代码,未被引用)
│ ├─ ai-provider-selector.tsx 89 行 表单字段react-hook-form
│ ├─ ai-markdown-renderer.tsx 162 行 Markdown + 图表代码块渲染
│ ├─ ai-chart-renderer.tsx 351 行 Recharts 4 图表bar/line/pie/radar
│ ├─ ai-grading-assist.tsx 173 行 教师批改辅助
│ ├─ ai-error-book-analysis.tsx 246 行 学生错题本 AI 分析
│ ├─ ai-lesson-content-generator.tsx 180 行 教师备课内容生成
│ ├─ ai-question-variant-generator.tsx 218 行 题目变体生成
│ ├─ ai-usage-dashboard.tsx 221 行 管理员使用统计
│ ├─ ai-child-summary.tsx 186 行 家长学情摘要(⚠ 未接入页面)
│ └─ ai-study-path.tsx 200 行 学生学习路径(⚠ 未接入页面)
└─ src/app/api/ai/
├─ chat/route.ts 196 行 非流式聊天端点
└─ chat/stream/route.ts 237 行 SSE 流式端点
```
### 1.2 数据流
```
app/(dashboard)/layout.tsx
│ 模块级 const aiClientService = createFullAiClientService()
<AiClientProvider service={aiClientService}> ← React Context
├─ <AiAssistantWidget /> ← 全局悬浮球
│ └─ useAiClientOptional() / useFloatingBall()
│ └─ <AiChatPanel variant="widget">
│ └─ useAiChatStream() → fetch('/api/ai/chat/stream')
│ │
│ ▼
│ route.ts: requirePermission(AI_CHAT)
│ + tryConsumeDailyQuota
│ + filterUserInput / filterAiOutput
│ + createAiChatCompletionStream (shared/lib/ai)
└─ 各业务页面teacher/homework/submissions、student/error-book、teacher/exams/build 等)
└─ <AiClientProvider service={createCoreAiClientService()}>
└─ <AiGradingAssist /> / <AiErrorBookAnalysis /> / ...
└─ useAiClient().suggestGrading(...) → suggestGradingAction
→ requirePermission(AI_CHAT, HOMEWORK_GRADE)
→ createAiService(userId).suggestGrading(input)
→ withAiTracking(...)
→ createAiChatCompletion (shared/lib/ai)
```
### 1.3 架构图记录情况
`005_architecture_data.json``modules.ai` 节点记录了完整的依赖矩阵、exports 清单、集成点、权限点、安全策略、流式特性、i18n 命名空间。
**但比对发现两处与实际实现不一致**(详见 §二 P0-2
- `ai.integrations.parent-dashboard` 声称 `AiChildSummary` 接入 `parent/dashboard` 页面 → 实际未接入
- `ai.integrations.student-learning` 声称 `AiStudyPath` 接入 `student/learning/study-path` 页面 → 实际该路由不存在
---
## 二、现存问题与原因分析
### P0 — 紧急且阻断使用
#### P0-1家长角色完全缺失 `AI_CHAT` 权限
- **位置**[permissions.ts](file:///e:/Desktop/CICD/src/shared/lib/permissions.ts#L161-L176) `ROLE_PERMISSIONS_SEED.parent` 数组
- **问题**parent 角色权限清单中**没有任何** `Permissions.AI_CHAT`,但:
- [actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L267) `generateChildSummaryAction` 第一行调用 `requirePermission(Permissions.AI_CHAT)` → 家长调用必返回 403
- [actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L296) `recommendStudyPathAction` 同上
- `/api/ai/chat/route.ts` L49 同上
- `AiChildSummary` / `AiStudyPath` 组件存在但家长/学生路径下完全无法使用
- **违反规则**
- 项目硬约束「所有 Server Action 必须调用 `requirePermission()` 进行权限校验」—— 校验逻辑本身正确,但权限未授予
- 项目硬约束「家长需要的功能应被授权」K12 系统家长是关键角色)
- **后果**:家长角色付费的 AI 学情摘要功能在生产环境 100% 失败;学生使用学习路径推荐时若依赖家长代调也会失败
#### P0-2架构图虚构集成AiChildSummary / AiStudyPath 完全未接入)
- **位置**
- [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json#L19725-L19744) `ai.integrations.parent-dashboard` / `ai.integrations.student-learning`
- 004 文档同步描述
- **问题**
- 声称 `AiChildSummary` 集成于 `parent/dashboard` —— grep 全仓 `AiChildSummary` 仅在自身文件、actions、context 出现,**app/ 下零引用**
- 声称 `AiStudyPath` 集成于 `student/learning/study-path` —— 该路由**不存在**`app/(dashboard)/student/learning/` 下只有 `textbooks/``assignments/``courses/``page.tsx`
- 声称 `AiUsageDashboard` 集成于 `admin/ai-usage` —— 实际接入在 `admin/ai-settings/page.tsx`(路径不一致)
- **违反规则**
- 项目硬约束「如果发现项目中存在架构图未记录的模块、函数、表、路由等,必须优先完善架构图信息」
- 项目硬约束「改码必同步图」—— 反向也成立:图中记录的集成必须真实存在
- **后果**:依赖架构图做影响分析的开发者会误以为功能已上线,跳过实现;测试用例遗漏;产线功能缺失
#### P0-3`getAiUsageStatsAction` 错误消息 i18n 键错误
- **位置**[actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L381)
- **问题**:管理员查询使用统计失败时返回 `t("error.chatFailed")`"AI 请求失败"/"AI request failed"),与场景不符
- **后果**:管理员看到"AI 请求失败"误以为是 AI 调用失败,实为统计查询失败
### P1 — 高优先级
#### P1-1数据访问层使用内存存储多实例部署不可用
- **位置**
- [data-access.ts](file:///e:/Desktop/CICD/src/modules/ai/data-access.ts#L34) `const eventStore: StoredAiEvent[] = []` 单实例内存
- [content-safety.ts](file:///e:/Desktop/CICD/src/modules/ai/services/content-safety.ts#L137) `const dailyUsageMap = new Map<...>()` 单实例内存
- **问题**:注释自承认"生产环境应替换为 Redis",但当前实现:
- 多实例部署下,`getAiUsageStats` 聚合的统计仅包含当前实例数据
- `tryConsumeDailyQuota` 在多实例下,每个实例独立计数,实际可用次数 = 限额 × 实例数
- 进程重启后所有统计归零
- **违反规则**:项目硬约束「企业级补充」「可扩展性:采用配置驱动设计」
- **后果**K8s 多 Pod 部署后限额失效、统计失真
#### P1-2`AiUsageDashboard` 违反 React Hooks 规范
- **位置**[ai-usage-dashboard.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-usage-dashboard.tsx#L53-L57)
- **问题**
```tsx
useEffect(() => {
void loadStats()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
```
- `loadStats` 依赖 `aiClient` 但被 disable 抑制
- `aiClient` 变化时不会重新加载
- **后果**eslint-disable 掩盖真实 bugaiClient 引用变更时不刷新
#### P1-3`AiClientProvider` 在 layout 模块级创建 service
- **位置**[layout.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/layout.tsx#L10)
- **问题**`const aiClientService = createFullAiClientService()` 在模块加载时执行module scopeservice 对象被所有用户共享
- **当前可工作原因**Server Action 内部 `requirePermission()` 会从 session 动态解析用户
- **风险**:未来若 service 需要请求级状态(如缓存当前用户权限),模块级单例会泄露
- **建议**:移入 Server Component 函数体内创建
#### P1-4`AiAssistantWidget` 内嵌英文 prompt 硬编码
- **位置**[ai-assistant-widget.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L228-L326) `inferContextFromPath`
- **问题**6 个角色的 `systemPrompt` 为英文硬编码字符串,未走 i18n
- **缓解**API 端点会强制覆盖(学生侧 SOCRATIC_TUTOR_SYSTEM_PROMPT客户端 prompt 仅作为上下文提示
- **后果**:维护 prompt 需改代码;多语言场景下非英语用户的提示词不一致
#### P1-5死代码 `useAiChat` Hook
- **位置**[use-ai-chat.ts](file:///e:/Desktop/CICD/src/modules/ai/hooks/use-ai-chat.ts) 57 行
- **问题**grep 全仓 `useAiChat` 仅在自身文件 + 005 架构数据中引用,**实际无任何组件使用**
- **原因**:早期非流式实现被 `useAiChatStream` 取代,但文件未删除
- **后果**:架构图 exports 中仍记录 `useAiChat`,误导调用方
#### P1-6死代码 `AiSuggestionCard` 组件
- **位置**[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx) 178 行
- **问题**grep 全仓 `AiSuggestionCard` 仅在自身文件 + 架构数据中引用,**实际无任何页面使用**
- **原因**`AiErrorBookAnalysis` 已包含相似题功能,`AiSuggestionCard` 是早期独立实现
- **后果**:维护成本;架构图 exports 仍记录该组件
#### P1-7API 路由与非流式路由大量重复代码
- **位置**
- [chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts) 196 行
- [chat/stream/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/stream/route.ts) 237 行
- **问题**:权限校验 / 限流 / Zod 校验 / 配额消费 / 输入过滤 / 系统提示构建 / 配额退款 7 段逻辑几乎逐行复制
- **后果**:修一处漏一处易出 bug测试需双倍
### P2 — 中等优先级
#### P2-1`ai-chart-renderer.tsx` 使用 `as` 断言
- **位置**[ai-chart-renderer.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L122-L132)
- **问题**
```ts
data: obj.data as Array<Record<string, string | number>>, // as 断言
series: obj.series as AiChartSeries[], // as 断言
yDomain: Array.isArray(obj.yDomain) ? obj.yDomain as [number, number] : undefined, // as 断言
```
- **违反规则**:项目硬约束「禁止 `as` 断言(除非从 `unknown` 转换)」—— 严格说此处从 `unknown` 转,但应使用类型守卫或 Zod parse
- **建议**:用 `z.array(AiChartSeriesSchema).parse(obj.series)` 校验
#### P2-2`AiChatPanel` 单文件 417 行接近上限
- **位置**[ai-chat-panel.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chat-panel.tsx) 417 行
- **问题**card / widget 两个变体有 ~60% 重复 JSX消息列表、空状态、输入框、流式指示器各写两遍
- **建议**:抽取 `<ChatMessages>` / `<ChatInput>` / `<ChatEmptyState>` / `<ChatStreamingIndicator>` 子组件
#### P2-3`AiAssistantWidget.inferContextFromPath` 配置硬编码
- **位置**[ai-assistant-widget.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L223-L328)
- **问题**100+ 行 if-else 路由匹配,新增角色/路由需改代码
- **建议**:改为配置驱动
```ts
const CONTEXT_MAP: Array<{ match: RegExp; config: AiContextConfig }> = [...]
```
#### P2-4`content-safety.ts` 关键词仅英文
- **位置**[content-safety.ts](file:///e:/Desktop/CICD/src/modules/ai/services/content-safety.ts#L20-L43)
- **问题**`BLOCKED_INPUT_PATTERNS` / `STUDENT_BLOCKED_PATTERNS` 正则仅匹配英文关键词
- **后果**:中文"自杀/暴力/色情"等不当内容无法识别K12 中国场景下安全防线不足
#### P2-5`AiService.chat` 的 `usage` 字段始终返回 `null`
- **位置**[ai-service.ts](file:///e:/Desktop/CICD/src/modules/ai/services/ai-service.ts#L177)
- **问题**`return { result: { content, usage: null }, tokenUsage }` —— `AiChatResult.usage: unknown` 类型但实际始终 null
- **建议**:将 `tokenUsage` 包入 `usage` 字段,或修改类型为 `usage: null`
#### P2-6架构图遗漏 `/admin/ai-settings` 路由
- **位置**[005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) `routes` 节点
- **问题**:仅在 `ai.integrations.admin-dashboard.page` 字段提及 `admin/ai-usage`,但 `routes` 表中无 `/admin/ai-settings` 条目(实际页面位于 `/admin/ai-settings`
- **后果**:路由审计遗漏
---
## 三、行业差距对比
### 3.1 与 KhanmigoKhan Academy差距
| 维度 | Khanmigo | 我们 | 差距影响 |
|------|----------|------|---------|
| 教师可见学生 AI 对话 | ✓ 教师后台可审阅 | ✗ 对话仅存 localStorage | 教师无法了解学生提问习惯,无法干预 Socratic 失败场景 |
| 多模态输入 | ✓ 支持图片 | ✗ 仅文本 | 数学几何题无法拍照上传 |
| Activity 难度自适应 | ✓ 根据学生水平动态调整 | ✗ 固定 difficulty 参数 | 同一题目对快慢学生无差异 |
| Teacher copilot 模式 | ✓ 教师侧 AI 提示教学策略 | ✗ 仅 widget 通用助手 | 教师备课缺专业引导 |
### 3.2 与 Duolingo Max 差距
| 维维 | Duolingo Max | 我们 | 差距影响 |
|------|--------------|------|---------|
| Explain My Answer | ✓ 错题后一键解释 | ✓ `explainError` 已实现但未接入页面 | 功能闲置 |
| Roleplay | ✓ 情景对话练习 | ✗ 无 | 英语口语训练缺失 |
| "立即练习"按钮 | ✓ 相似题后直接进入练习流 | ✗ 仅"选择" | 学生看到相似题但无法作答,流程断裂 |
### 3.3 与 Squirrel AI松鼠 AI差距
| 维度 | Squirrel AI | 我们 | 差距影响 |
|------|-------------|------|---------|
| 纳米级知识图谱 | ✓ 700+ 知识点拆分 | ⚠ `recommendStudyPath` 已支持 knowledgeGraph 注入,但未接入页面 | 功能已实现但未上线 |
| 自适应路径 | ✓ 实时根据答题调整 | ✗ 一次性生成路径,无反馈循环 | 路径在学习过程中不更新 |
| 学习目标对齐 | ✓ 与课标 / 升学目标对齐 | ✗ studyPathInput.learningGoal 仅文本 | 缺少课标映射 |
### 3.4 与 Century Tech 差距
| 维度 | Century Tech | 我们 | 差距影响 |
|------|--------------|------|---------|
| 全校 AI 成本看板 | ✓ token 消耗 / 预算预警 | ⚠ `AiUsageDashboard` 无 token 字段 | 管理员无法评估成本 |
| 多 Provider 对比 | ✓ A/B 测试 | ✗ 单次调用单 provider | 无法评估哪家性价比高 |
| 课程标准映射 | ✓ AI 推荐与课标对齐 | ✗ 无 | 学习路径与课标脱节 |
### 3.5 K12 通用缺失
- **a11y**`AiAssistantWidget` 悬浮球无键盘焦点;`AiChatPanel` 流式 token 更新未限流(屏幕阅读器频繁打断)
- **空状态**`AiUsageDashboard` 无数据时仅显示文案,无引导管理员"先发起一次 AI 对话"
- **错误恢复**`AiErrorBoundary` 透传 `SectionErrorBoundary`,无 AI 专属重试策略(如降级到非流式)
---
## 四、改进优先级建议
### P0必须立即修复 — 阻断核心功能)
| 编号 | 改进项 | 方向说明 |
|------|--------|---------|
| P0-1 | parent 角色补齐 `AI_CHAT` 权限 | 在 `ROLE_PERMISSIONS_SEED.parent` 数组追加 `Permissions.AI_CHAT` |
| P0-2 | 修复架构图虚构集成 | 二选一:(A) 在 parent/dashboard 接入 `AiChildSummary`,新建 `student/learning/study-path` 路由接入 `AiStudyPath`(B) 从架构图 integrations 中移除两条虚构集成。**本次采用方案 A**:实际接入组件,让功能上线 |
| P0-3 | `getAiUsageStatsAction` 错误 i18n 修复 | 新增 `ai.error.statsFailed` 翻译键,替换 `chatFailed` |
### P1高优先级 — 影响可维护性与正确性)
| 编号 | 改进项 | 方向说明 |
|------|--------|---------|
| P1-1 | 内存存储抽象化 | 提取 `AiUsageStore` 接口,当前内存实现作为 `InMemoryAiUsageStore`,未来可替换 `RedisAiUsageStore`;不阻塞当前发布 |
| P1-2 | `AiUsageDashboard` useEffect 修复 | 抽取 `loadStats` 为 `useCallback`,依赖数组加入 `aiClient` |
| P1-3 | layout service 创建移入 Server Component | 改为 `function DashboardLayout() { const service = createFullAiClientService(); ... }` |
| P1-4 | `inferContextFromPath` 配置化 + i18n 化 | 改为 `CONTEXT_MAP` 数组prompt 走 i18n key |
| P1-5 | 删除 `use-ai-chat.ts` 死代码 | 文件 + 架构图 exports 同步移除 |
| P1-6 | 删除 `ai-suggestion-card.tsx` 死代码 | 同上 |
| P1-7 | API 路由共享逻辑抽取 | 抽取 `prepareAiChatRequest(req)` 返回 `{ body, isStudent, quota, limitResult }` |
### P2中等优先级 — 代码质量与扩展性)
| 编号 | 改进项 | 方向说明 |
|------|--------|---------|
| P2-1 | `ai-chart-renderer` `as` 断言替换为 Zod parse | 用 `AiChartSpecSchema.parse()` 校验 |
| P2-2 | `AiChatPanel` 拆分子组件 | 抽取 `ChatMessages` / `ChatInput` / `ChatEmptyState` |
| P2-3 | `content-safety` 增加中文关键词 | 扩展正则至中文场景 |
| P2-4 | `AiService.chat.usage` 修正 | 返回实际 tokenUsage 或改类型为 `null` |
| P2-5 | 架构图补齐 `/admin/ai-settings` 路由 | 005 routes 节点新增 |
### 中长期方向(不在本次实施范围)
- 接入 Redis 替换内存存储(需运维配合)
- 多模态输入(需 OCR/视觉模型)
- 教师 AI 对话审阅后台(需新增 DB 表)
- 多 Provider A/B 测试(需扩展 Provider 模型)
- 课程标准映射(需课标数据源)
---
## 五、架构图同步说明
本次审计发现架构图需更新如下节点:
### 5.1 `005_architecture_data.json` 同步项
1. **`modules.ai.exports.hooks`** 移除 `useAiChat`(死代码已删除)
2. **`modules.ai.exports.components`** 移除 `AiSuggestionCard`(死代码已删除)
3. **`modules.ai.integrations.parent-dashboard`** 更新 `page` 字段为真实接入路径
4. **`modules.ai.integrations.student-learning`** 更新 `page` 字段为真实接入路径 `student/learning/study-path`
5. **`modules.ai.integrations.admin-dashboard`** 更正 `page` 为 `admin/ai-settings`(原误记为 `admin/ai-usage`
6. **`routes./admin/ai-settings`** 新增节点(若 routes 表中确实缺失)
7. **`rolePermissionsSeed.parent`** 追加 `ai:chat` 权限
8. **`modules.ai.i18n.v2Keys`** 追加 `error.statsFailed` 翻译键
### 5.2 `004_architecture_impact_map.md` 同步项
1. AI 模块章节的「集成点」表格更新实际接入路径
2. 移除已删除组件的引用
---
## 六、本次实施清单
### 已实施(本次审计直接修复)
| 编号 | 类型 | 改动 |
|------|------|------|
| P0-1 | 代码 | `permissions.ts` parent 角色追加 `AI_CHAT` |
| P0-2 | 代码 + 架构图 | 接入 `AiChildSummary` 到 parent/dashboard`ParentDashboard` 新增 `aiSummarySlot`page.tsx 为每个子女渲染 `AiChildSummary`);新建 `student/learning/study-path` 路由page.tsx + loading.tsx + error.tsx接入 `AiStudyPath`;同步 004/005 文档集成点与路由表 |
| P0-3 | 代码 + i18n | `actions.ts` 修复错误键;`ai.json` (zh/en) 新增 `error.statsFailed` |
| P1-2 | 代码 | `ai-usage-dashboard.tsx` 修复 useEffect |
| P1-3 | 代码 | `layout.tsx` service 移入函数体 |
| P1-5 | 代码 | 删除 `use-ai-chat.ts`;架构图 exports.hooks 移除 `useAiChat` |
| P1-6 | 代码 | 删除 `ai-suggestion-card.tsx`;架构图 exports.components 移除 `AiSuggestionCard` |
| P2-1 | 代码 | `ai-chart-renderer.tsx` 替换 `as` 为 Zod parse新增 `AiChartSpecSchema` |
| P2-4 | 代码 | `ai-service.ts` 修正 `usage` 字段返回实际 tokenUsage |
| 架构图 | 文档 | 004/005 同步上述改动;新增 `student.studyPath` i18n 块zh/en |
| 权限 | 文档 | `005` 的 `rolePermissionsSeed.parent` 追加 `AI_CHAT` |
| i18n | 文档 | `005` 的 `modules.ai.i18n.v2Keys` 追加 `error.statsFailed` |
| 路由 | 文档 | `005` 的 `routes` 表新增 `/student/learning/study-path`;更新 `/parent/dashboard` 描述含 AI 摘要集成;`/admin/ai-settings` 校正为 admin 集成路径(原误记为 `admin/ai-usage` |
### 未实施(中长期,需独立任务)
| 编号 | 原因 |
|------|------|
| P1-1 Redis 替换内存 | 需运维提供 Redis 实例 |
| P1-4 prompt 完全 i18n 化 | 客户端 prompt 已被服务端覆盖,影响小 |
| P1-7 API 路由共享逻辑抽取 | 涉及测试回归,单独 PR |
| P2-2 AiChatPanel 拆分 | 涉及大量回归,单独 PR |
| P2-3 中文关键词扩展 | 需安全策略评审 |

View File

@@ -0,0 +1,390 @@
# AI 模块审计报告 V3 — 架构解耦与全角色对标
> 审计范围:基于 V1`ai-module-audit-report.md`)与 V2`ai-module-audit-report-v2.md`)已完成实现,进行第三轮深度架构审计。
> 审计日期2026-06-24
> 审计方法:全文件逐行扫描 + 三层架构合规性矩阵 + 跨模块依赖图 + K12 行业标杆对标Khanmigo / Duolingo Max / Squirrel AI / Century Tech / MagicSchool AI
> 审计依据:`docs/standards/coding-standards.md`、`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、项目规则
---
## 一、现有实现概要
### 1.1 文件分布30 个文件)
| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| **types** | `modules/ai/types.ts` | 295 | AiService/AiClientService 接口 + 8 个业务场景类型 |
| **schema** | `modules/ai/schema.ts` | 227 | Zod 验证8 输入 + 8 输出) |
| **actions** | `modules/ai/actions.ts` | 381 | 9 个 Server Actions含权限校验 |
| **data-access** | `modules/ai/data-access.ts` | 138 | AI 事件内存存储 + 统计聚合 |
| **services** | `modules/ai/services/ai-service.ts` | 439 | DefaultAiService 实现8 方法) |
| **services** | `modules/ai/services/prompt-templates.ts` | 277 | 10 个系统提示词模板 |
| **services** | `modules/ai/services/usage-tracker.ts` | 99 | AI 使用量埋点 |
| **services** | `modules/ai/services/content-safety.ts` | 291 | 内容安全过滤(输入/输出/配额/Socratic |
| **context** | `modules/ai/context/ai-client-provider.tsx` | 62 | React Context Provider + Hooks |
| **hooks** | `modules/ai/hooks/use-ai-chat-stream.ts` | 155 | 流式 AI 对话 Hook |
| **hooks** | `modules/ai/hooks/use-ai-chat.ts` | 57 | 非流式 AI 对话 Hook |
| **hooks** | `modules/ai/hooks/use-ai-suggestion.ts` | 72 | AI 建议 Hook |
| **hooks** | `modules/ai/hooks/use-floating-ball.ts` | 243 | 悬浮球拖拽 Hook |
| **hooks** | `modules/ai/hooks/stream-utils.ts` | 135 | SSE 流解析工具 |
| **components** | `modules/ai/components/ai-chat-panel.tsx` | 417 | AI 对话面板 |
| **components** | `modules/ai/components/ai-assistant-widget.tsx` | 329 | 全局 AI 助手悬浮球 |
| **components** | `modules/ai/components/ai-markdown-renderer.tsx` | 143 | Markdown 渲染器 |
| **components** | `modules/ai/components/ai-chart-renderer.tsx` | 329 | 图表渲染器 |
| **components** | `modules/ai/components/ai-error-boundary.tsx` | 88 | AI 错误边界 |
| **components** | `modules/ai/components/ai-skeleton.tsx` | 47 | 骨架屏 |
| **components** | `modules/ai/components/ai-provider-selector.tsx` | 89 | 服务商选择器 |
| **components** | `modules/ai/components/ai-grading-assist.tsx` | 173 | AI 批改辅助 |
| **components** | `modules/ai/components/ai-error-book-analysis.tsx` | 246 | 错题本 AI 分析 |
| **components** | `modules/ai/components/ai-lesson-content-generator.tsx` | 180 | 备课内容生成器 |
| **components** | `modules/ai/components/ai-question-variant-generator.tsx` | 218 | 题目变体生成器 |
| **components** | `modules/ai/components/ai-child-summary.tsx` | 186 | 家长学情摘要 |
| **components** | `modules/ai/components/ai-usage-dashboard.tsx` | 221 | 管理员使用统计 |
| **components** | `modules/ai/components/ai-study-path.tsx` | 200 | 学生学习路径 |
| **components** | `modules/ai/components/ai-suggestion-card.tsx` | 164 | 相似题建议卡片 |
| **api** | `app/api/ai/chat/route.ts` | 48 | 非流式聊天端点 |
| **api** | `app/api/ai/chat/stream/route.ts` | 237 | SSE 流式端点 |
### 1.2 数据流
```
客户端组件
└─▶ useAiClient() / useAiClientOptional()
└─▶ AiClientProvider (app/layout.tsx 或子页面注入)
└─▶ Server Actions (modules/ai/actions.ts)
└─▶ createAiService(userId) → DefaultAiService
└─▶ createAiChatCompletion (shared/lib/ai)
└─▶ OpenAI SDK + ai_providers 表
SSE 流式端点(独立路径):
app/api/ai/chat/stream/route.ts
└─▶ requirePermission(AI_CHAT)
└─▶ content-safety (filterUserInput / tryConsumeDailyQuota)
└─▶ createAiChatCompletionStream (shared/lib/ai)
└─▶ content-safety (filterAiOutput / validateSocraticOutput)
```
### 1.3 架构图记录情况
- `004_architecture_impact_map.md` 第 2.29 节完整记录了 AI 模块V2/V3/V4 变更)
- `005_architecture_data.json` 包含 `modules.ai` 节点exports/dependencies/integrations/safety/streaming/i18n
- **结论:架构图对 AI 模块的记录基本完整**,但未记录跨模块违规依赖(见 2.1.1)。
---
## 二、现存问题与原因分析
### 2.1 架构分层问题
#### 问题 2.1.15 处跨模块直接依赖 `shared/lib/ai`(绕过 modules/ai
- **位置**
- [ai-suggest.ts:5](file:///e:/Desktop/CICD/src/modules/lesson-preparation/ai-suggest.ts#L5) — `import { createAiChatCompletion } from "@/shared/lib/ai"`
- [settings/actions.ts:14](file:///e:/Desktop/CICD/src/modules/settings/actions.ts#L14) — `import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderConfig } from "@/shared/lib/ai"`
- [exams/actions.ts:987](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L987) — 动态 `import("@/shared/lib/ai")`
- [exams/ai-pipeline/request.ts:11](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/request.ts#L11) — `import { createAiChatCompletion, getAiErrorMessage } from "@/shared/lib/ai"`
- [exams/ai-pipeline/parse.ts:11](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/parse.ts#L11) — `import { createAiChatCompletion } from "@/shared/lib/ai"`
- **原因**AI 模块在 V2 重构后才形成独立模块,但这些历史调用点未同步迁移。
- **后果**:绕过 `modules/ai` 的内容安全过滤、每日配额、Socratic 模式、使用量埋点等保护机制AI 调用无法统一治理。
- **违反规则**`项目规则 → 架构分层规则 → 模块间只能通过对方 data-access 通信``项目规则 → Server Action 规范 → 每个 Action 必须调用 requirePermission()`
#### 问题 2.1.2:非流式聊天端点绕过 modules/ai
- **位置**[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts)
- **现状**:直接调用 `shared/lib/ai``createAiChatCompletion`,未走 `aiChatAction`缺失内容安全过滤、每日配额、Socratic 模式等保护。
- **后果**:与非流式端点(`/api/ai/chat/stream`)形成安全策略不一致;学生可通过非流式端点绕过 Socratic 模式获取直接答案。
- **违反规则**`项目规则 → 安全规范``项目规则 → Server Action 规范`
#### 问题 2.1.34 个子页面重复创建 AiClientService
- **位置**
- [teacher/lesson-plans/[planId]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/page.tsx)
- [teacher/homework/submissions/[submissionId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/homework/submissions/[submissionId]/page.tsx)
- [student/error-book/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/error-book/page.tsx)
- [teacher/exams/[id]/build/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
- **现状**:每个页面各自创建只含 6 个 Action 的 `AiClientService`,覆盖 layout.tsx 的全局 Provider含 9 个 Action
- **后果**:代码重复;`generateChildSummary``recommendStudyPath``getAiUsageStats` 在这些页面内为 `undefined`,若未来组件调用将运行时错误。
- **违反规则**`项目规则 → 工程约定 → 最大化复用`
### 2.2 权限问题
#### 问题 2.2.1:非流式聊天端点未走 requirePermission 体系
- **位置**[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts)
- **现状**:虽然调用了 `requirePermission(Permissions.AI_CHAT)`,但绕过了 `modules/ai/actions.ts``aiChatAction`导致内容安全过滤、每日配额、Socratic 模式等保护机制缺失。
- **后果**:权限校验通过但安全策略不一致。
- **违反规则**`项目规则 → Server Action 规范 → 每个 Action 必须调用 requirePermission()`(虽调用但绕过 Action 编排层)。
### 2.3 国际化问题
#### 问题 2.3.1ai-chart-renderer.tsx 硬编码中文
- **位置**[ai-chart-renderer.tsx:147](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L147)
- **代码**`图表数据格式错误,无法渲染`
- **后果**:无法切换语言。
- **违反规则**`项目规则 → 所有用户可见文本必须适配 i18n`
#### 问题 2.3.2ai-assistant-widget.tsx systemPrompt 硬编码英文
- **位置**[ai-assistant-widget.tsx:230-326](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L230-L326)
- **现状**`inferContextFromPath` 函数中所有 `systemPrompt``contextMessage` 为硬编码英文。
- **判定**systemPrompt 是发送给 AI 的指令(非用户可见文本),可保留英文(模型兼容性最佳);但 `contextMessage` 显示在 UI 中,应 i18n 化。
- **后果**contextMessage 无法国际化。
- **违反规则**`项目规则 → 所有用户可见文本必须适配 i18n`
### 2.4 类型安全问题
#### 问题 2.4.1ai-markdown-renderer.tsx 使用 as 断言
- **位置**[ai-markdown-renderer.tsx:92](file:///e:/Desktop/CICD/src/modules/ai/components/ai-markdown-renderer.tsx#L92)
- **代码**`lang.slice(CHART_LANG_PREFIX.length) as AiChartType`
- **后果**:若 lang 不在 AiChartType 枚举内,类型不安全。
- **违反规则**`项目规则 → TypeScript 规则 → 禁止 as 断言`
#### 问题 2.4.2ai-provider-selector.tsx 使用 as 断言
- **位置**[ai-provider-selector.tsx:66](file:///e:/Desktop/CICD/src/modules/ai/components/ai-provider-selector.tsx#L66)
- **代码**`field.value as string`
- **后果**react-hook-form 的 field.value 类型应为泛型,此处强转。
- **违反规则**`项目规则 → TypeScript 规则 → 禁止 as 断言`
#### 问题 2.4.3ai-chart-renderer.tsx 使用 as 断言
- **位置**[ai-chart-renderer.tsx:117](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L117)
- **代码**`JSON.parse(data) as AiChartSpec`
- **后果**JSON 结构不可信,直接断言可能运行时错误。
- **违反规则**`项目规则 → TypeScript 规则 → 禁止 as 断言`
### 2.5 错误处理问题
#### 问题 2.5.1ai-suggestion-card.tsx 未包裹 Error Boundary
- **位置**[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx)
- **现状**:组件内部有 try/catch 处理异步错误,但未用 `AiErrorBoundary` 包裹渲染期错误。
- **后果**:若 `result.data` 结构异常或 `questions.map` 渲染时抛错,整页崩溃。
- **违反规则**`审计要求 → 每个独立数据区块必须用 React Error Boundary 包裹`
#### 问题 2.5.2:非流式聊天端点错误处理不完整
- **位置**[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts)
- **现状**`getStatusFromError` 基于错误消息字符串匹配状态码,脆弱且不可靠。
- **后果**:错误分类不准确。
- **违反规则**`项目规则 → 错误处理`
### 2.6 文件大小问题
#### 问题 2.6.1use-floating-ball.ts 超出 Hook 行数限制
- **位置**[use-floating-ball.ts](file:///e:/Desktop/CICD/src/modules/ai/hooks/use-floating-ball.ts)
- **现状**243 行,超出 Hook 文件 80 行建议上限 163 行。
- **后果**:职责过多(位置加载/保存、拖拽事件、边缘吸附、半隐藏),难以维护和测试。
- **违反规则**`项目规则 → 单文件行数 → 自定义 Hook建议 ≤ 80 行`
### 2.7 可复用性问题
#### 问题 2.7.14 个子页面重复创建 AiClientService
- 见问题 2.1.3。
#### 问题 2.7.2ai-suggestion-card.tsx 未被使用
- **位置**[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx)
- **现状**:组件已实现但无任何引用。
- **后果**:死代码,维护负担。
- **违反规则**`审计要求 → 最大化复用`(应集成到错题本等页面)。
### 2.8 监控问题
#### 问题 2.8.1:非流式聊天端点无使用量埋点
- **位置**[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts)
- **现状**:未调用 `trackAiUsage`,调用数据不入统计。
- **后果**:管理员仪表盘数据不完整。
- **违反规则**`审计要求 → 监控`
---
## 三、行业差距对比
### 3.1 与 KhanmigoKhan Academy的差距
| 功能 | Khanmigo | 本项目 | 差距 |
|------|----------|--------|------|
| 学生 Socratic 模式 | ✅ 强制引导式 | ✅ 已实现 | 无 |
| 教师备课助手 | ✅ 课程计划生成 | ✅ 已实现 | 无 |
| 多语言支持 | ✅ 20+ 语言 | ⚠️ 仅中英文 | 缺少多语言 Prompt |
| 学生情绪识别 | ✅ 检测挫败感 | ❌ 未实现 | 缺少情绪分析 |
| 家长沟通建议 | ✅ 家庭教育指导 | ✅ 已实现 | 无 |
### 3.2 与 Duolingo Max 的差距
| 功能 | Duolingo Max | 本项目 | 差距 |
|------|-------------|--------|------|
| 解释我的答案 | ✅ AI 解释错误原因 | ❌ 未实现 | 缺少错题 AI 解释 |
| 角色扮演练习 | ✅ 情景对话练习 | ❌ 未实现 | 缺少口语/情景练习 |
| 个性化复习 | ✅ 基于遗忘曲线 | ⚠️ 仅 SM2 算法 | AI 未参与复习规划 |
### 3.3 与 Squirrel AI 的差距
| 功能 | Squirrel AI | 本项目 | 差距 |
|------|-------------|--------|------|
| 纳米级知识图谱 | ✅ 10000+ 知识点 | ⚠️ V3 已集成 | 知识图谱粒度较粗 |
| 自适应学习路径 | ✅ 实时调整 | ✅ V3 已实现 | 无 |
| 多模态学习 | ✅ 视频+图文+音频 | ❌ 仅文本 | 缺少多模态 |
| 学习风格识别 | ✅ VARK 模型 | ❌ 未实现 | 缺少学习风格分析 |
### 3.4 与 MagicSchool AI 的差距
| 功能 | MagicSchool AI | 本项目 | 差距 |
|------|----------------|--------|------|
| 50+ AI 工具 | ✅ 丰富工具集 | ⚠️ 8 个能力 | 工具数量不足 |
| IEP 生成 | ✅ 特殊教育计划 | ❌ 未实现 | 缺少 IEP |
| 家校沟通模板 | ✅ 邮件/通知模板 | ❌ 未实现 | 缺少沟通模板 |
| 跨学科项目设计 | ✅ PBL 项目设计 | ❌ 未实现 | 缺少 PBL |
### 3.5 关键差距总结
1. **AI 能力数量不足**:仅 8 个能力,行业平均 15-20 个
2. **多模态缺失**:仅支持文本,缺少图像/语音/视频输入
3. **学习风格识别缺失**:未识别学生 VARK 学习风格
4. **情绪识别缺失**:未检测学生挫败感/兴奋度
5. **IEP/PBL 缺失**:未支持特殊教育和项目式学习
6. **跨模块数据联动不足**AI 未与考勤、行为、心理等数据联动分析
---
## 四、改进优先级建议
### P0紧急 — 安全/架构合规)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | 5 处跨模块直接依赖 shared/lib/ai | 迁移至通过 modules/ai 的 data-access 或 actions 调用 |
| P0-2 | 非流式聊天端点绕过 modules/ai | 重构为调用 aiChatAction 或迁移安全策略 |
| P0-3 | ai-chart-renderer.tsx 硬编码中文 | 迁移至 i18n |
| P0-4 | 3 处 as 断言 | 改用类型守卫函数 |
### P1重要 — 规范/复用)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | 4 个子页面重复创建 AiClientService | 提取 createAiClientService 工厂函数 |
| P1-2 | use-floating-ball.ts 超出行数限制 | 拆分为 use-drag-position + use-edge-snap |
| P1-3 | ai-suggestion-card.tsx 未包裹 Error Boundary | 集成时用 AiErrorBoundary 包裹 |
| P1-4 | ai-assistant-widget.tsx contextMessage 硬编码 | 迁移至 i18n |
| P1-5 | 非流式端点无使用量埋点 | 集成 trackAiUsage |
### P2增强 — 行业对标)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | 缺少错题 AI 解释 | 新增 explainErrorAction |
| P2-2 | 缺少学习风格识别 | 新增 VARK 评估 |
| P2-3 | 缺少 IEP 生成 | 新增特殊教育模块 |
| P2-4 | 缺少家校沟通模板 | 新增沟通模板生成 |
| P2-5 | 缺少多模态输入 | 支持图像/语音输入 |
| P2-6 | 缺少情绪识别 | 集成情绪分析 API |
---
## 五、架构图同步说明
### 5.1 需补充的节点
1. **跨模块违规依赖**:在 `005_architecture_data.json``dependencyMatrix` 中标注 `lesson-preparation``settings``exams``shared/lib/ai` 的违规依赖(应改为通过 `modules/ai`)。
2. **非流式端点安全策略缺失**:在 `004_architecture_impact_map.md` 的 AI 模块安全机制章节补充非流式端点的安全策略差距。
3. **ai-suggestion-card.tsx 未使用**:在文件清单中标注该组件为"已实现未集成"。
### 5.2 需修改的节点
1. **V4 超时优化**:已在 V4 中记录(`shared/lib/ai/client.ts` 按场景分离超时)。
2. **V4 规范修复**:已在 V4 中记录ai-service.ts 消除 as 断言、actions.ts 消除非空断言、ai-assistant-widget.tsx i18n 修复)。
---
## 六、实施记录
### 6.1 P0 改进实施
#### P0-1迁移跨模块依赖部分
**已实施**
- 修复 `ai-chart-renderer.tsx` 硬编码中文P0-3
- 修复 `ai-markdown-renderer.tsx` as 断言P0-4 部分)
- 修复 `ai-provider-selector.tsx` as 断言P0-4 部分)
- 修复 `ai-chart-renderer.tsx` as 断言P0-4 部分)
**未实施(需中长期计划)**
- 5 处跨模块直接依赖 shared/lib/ai 的迁移(涉及 exams/lesson-preparation/settings 三个模块的重构,影响范围大,需单独排期)
#### P0-2非流式聊天端点安全策略重构已实施
**实施**:重构 `app/api/ai/chat/route.ts`,与流式端点 `/api/ai/chat/stream` 安全策略完全对齐:
- Zod 校验输入(`AiChatInputSchema`,限制消息数 50/长度 8000
- `tryConsumeDailyQuota` 原子化每日限额(防 TOCTOU 竞态)
- `filterUserInput` 输入安全过滤
- `filterAiOutput` 输出安全过滤
- 学生侧 Socratic 模式(服务端强制 `SOCRATIC_TUTOR_SYSTEM_PROMPT`,忽略客户端 systemPrompt
- `validateSocraticOutput` 苏格拉底式输出校验
- 过滤/失败时 `refundDailyQuota`(不惩罚用户)
- `trackEvent` 使用量埋点(成功/失败均记录)
#### P0-3ai-chart-renderer.tsx 硬编码中文(已实施)
**实施**:添加 i18n 键 `ai.chart.parseError`,替换硬编码中文。
#### P0-4as 断言修复(已实施)
**实施**
- `ai-markdown-renderer.tsx`:改用类型守卫函数 `isAiChartType`
- `ai-provider-selector.tsx`:改用 `String(field.value ?? "")`
- `ai-chart-renderer.tsx`:改用 Zod schema 校验
### 6.2 P1 改进实施
#### P1-1提取 createAiClientService 工厂(已实施)
**实施**:新建 `modules/ai/context/create-ai-client-service.ts`,导出 `createFullAiClientService`(含全部 9 个 Action`createCoreAiClientService`(仅 6 个常用 Action两个工厂函数。`app/(dashboard)/layout.tsx` 使用前者4 个子页面error-book、lesson-plans、homework、exams使用后者消除重复代码。
#### P1-2拆分 use-floating-ball.ts已实施
**实施**:将 243 行的 `use-floating-ball.ts` 拆分为 3 个文件:
- `use-position-persistence.ts`Position 类型、常量、clamp/load/save 纯函数、位置状态 HooklocalStorage + resize 校正)
- `use-drag-position.ts`:拖拽状态 + pointer 事件处理 Hook通过回调委托业务逻辑
- `use-floating-ball.ts`:主组合 Hook边缘吸附 + 半隐藏 + hovered 状态 + show/resetPosition
#### P1-3ai-suggestion-card.tsx Error Boundary已实施
**实施**:将原组件重命名为 `AiSuggestionCardInner`,新建 `AiSuggestionCard` 包装器用 `AiErrorBoundary` 包裹内部组件,保持公开 API 不变。
#### P1-4ai-assistant-widget.tsx contextMessage i18n已实施
**实施**:在 `en/ai.json``zh-CN/ai.json` 中添加 `chat.contextMessage.*` 翻译键7 个场景teacherGrading/teacherLesson/teacherExam/studentErrorBook/studentHomework/parent/admin替换 `inferContextFromPath` 中 7 处硬编码英文。
#### P1-5非流式端点使用量埋点已实施
**实施**:在 `app/api/ai/chat/route.ts` 中集成 `trackEvent`(事件名 `ai.chat`),成功时记录 durationMs/tokenCount失败时记录 errorMessage/durationMs。与流式端点`ai.chat_stream`)对齐。
### 6.3 P2 改进(中长期计划)
#### P2-1错题 AI 解释(已实施)
**实施**
- 新增 `ExplainErrorInput`/`ExplainErrorResult` 类型(`modules/ai/types.ts`
- 新增 `ExplainErrorInputSchema`/`ExplainErrorResultSchema` Zod 校验(`modules/ai/schema.ts`
- 新增 `EXPLAIN_ERROR_SYSTEM_PROMPT` 提示词(`modules/ai/services/prompt-templates.ts`
- 新增 `explainError` 服务方法(`modules/ai/services/ai-service.ts`
- 新增 `explainErrorAction` Server Action`modules/ai/actions.ts`权限AI_CHAT + ERROR_BOOK_READ
- 更新 `AiService`/`AiClientService` 接口,添加 `explainError` 方法
- 更新 `createFullAiClientService` 工厂函数包含 `explainError`
- 更新 `AiCapability` 类型添加 `"explain-error"`
- 更新 `AiUsageEvent`/`AI_EVENT_MAP` 添加 `explain_error` 埋点
- 更新 i18n 翻译键 `capability.explainError`en/zh-CN
- 更新架构文档 004/005
**未实施(需后续排期)**
- P2-2VARK 学习风格评估(需新增评估模块 + DB 表)
- P2-3IEP 特殊教育计划生成(需新增特殊教育模块)
- P2-4家校沟通模板生成需新增模板管理模块
- P2-5多模态输入支持需接入图像/语音 API
- P2-6情绪识别需接入情绪分析 API

View File

@@ -0,0 +1,392 @@
# 公告announcements模块审计报告
> 审查日期2026-06-25
> 审查范围:`src/modules/announcements/**`、`src/app/(dashboard)/announcements/**`、`src/app/(dashboard)/admin/announcements/**`、`src/shared/i18n/messages/{zh-CN,en}/announcements.json`
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.16、`docs/architecture/005_architecture_data.json#announcements`
> 关联报告:`announcements-messages-audit-report.md`合并版2026-06-22已不再维护本文为公告模块的独立深度审计
---
## 一、现有实现概要
### 1.1 文件分布
| 层 | 路径 | 文件 | 行数 | 说明 |
|----|------|------|------|------|
| 路由 · 用户端 | `src/app/(dashboard)/announcements/page.tsx` | 1 | 36 | 列表页(所有非管理角色共用) |
| 路由 · 用户端 | `src/app/(dashboard)/announcements/[id]/page.tsx` | 1 | 41 | 详情页(只读) |
| 路由 · 用户端 | `src/app/(dashboard)/announcements/{loading,error,[id]/error}.tsx` | 3 | 60 | 骨架屏 + 错误边界 |
| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/page.tsx` | 1 | 45 | 管理列表页 |
| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/[id]/page.tsx` | 1 | 47 | 编辑页(直接渲染表单,无详情视图) |
| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/{loading,error,[id]/error}.tsx` | 3 | 64 | 骨架屏 + 错误边界 |
| 模块 | `src/modules/announcements/actions.ts` | 1 | 403 | 9 个 Server Action + 通知编排 + 埋点 |
| 模块 | `src/modules/announcements/data-access.ts` | 1 | 413 | CRUD + 发布/归档 + 置顶/已读 + 3 个页面编排函数 |
| 模块 | `src/modules/announcements/types.ts` | 1 | 75 | 类型定义 |
| 模块 | `src/modules/announcements/schema.ts` | 1 | 95 | Zod 校验 + `refineAudience` 条件校验 |
| 模块 · 组件 | `src/modules/announcements/components/announcement-list.tsx` | 1 | 126 | 列表(纯服务端过滤) |
| 模块 · 组件 | `src/modules/announcements/components/announcement-card.tsx` | 1 | 125 | 卡片 + 置顶切换 |
| 模块 · 组件 | `src/modules/announcements/components/announcement-detail.tsx` | 1 | 267 | 详情 + 管理操作 + 自动已读 |
| 模块 · 组件 | `src/modules/announcements/components/announcement-form.tsx` | 1 | 230 | 创建/编辑表单 |
| 模块 · 组件 | `src/modules/announcements/components/admin-announcements-view.tsx` | 1 | 67 | 管理端视图(列表 + 创建 Dialog |
| i18n | `src/shared/i18n/messages/{zh-CN,en}/announcements.json` | 2 | 103/103 | 11 命名空间翻译字典 |
| 测试 | — | 0 | 0 | **零测试文件** |
文件大小均在规范内(组件 ≤500 行actions/data-access ≤800 行)。
### 1.2 数据流
```
[Route] /announcements/page.tsx
└─▶ announcements/data-access.getUserAnnouncementsPageData(userId, dataScope)
├─▶ resolveAudience(userId, dataScope) // 内部函数
│ └─▶ classes/data-access.{getClassGradeId | getStudentActiveClassId | getStudentActiveGradeId}
└─▶ getAnnouncements({ status: "published", audience })
[Route] /announcements/[id]/page.tsx ⚠️ 未做受众/状态过滤
├─▶ announcements/data-access.getAnnouncementById(id)
└─▶ announcements/data-access.isAnnouncementReadByUser(id, userId)
[Route] /admin/announcements/page.tsx
└─▶ announcements/data-access.getAdminAnnouncementsPageData(status)
├─▶ getAnnouncements({ status })
├─▶ school/data-access.getGrades()
└─▶ classes/data-access.getAdminClasses()
[Route] /admin/announcements/[id]/page.tsx
└─▶ announcements/data-access.getEditAnnouncementPageData(id)
├─▶ getAnnouncementById(id)
└─▶ school/data-access.getGrades()
[Action] createAnnouncementAction / updateAnnouncementAction / publishAnnouncementAction
└─▶ notifyAnnouncementPublished(announcement)
├─▶ resolveTargetUserIds(announcement) // ⚠️ 纯业务逻辑在 actions.ts
│ ├─▶ users/data-access.{getAllUserIds | getUserIdsByGradeId}
│ └─▶ classes/data-access.{getStudentIdsByClassId | getTeacherIdsByClassIds}
└─▶ notifications.sendBatchNotifications(payloads)
```
### 1.3 架构图记录情况
`004_architecture_impact_map.md` §2.16 对 announcements 模块的记录较为完整:
- ✅ 导出函数9 个 Action + 14 个 data-access 函数)记录准确
- ✅ 依赖关系(`shared/*``@/auth``school``classes``users``notifications`)记录准确
- ✅ 已修复问题清单P1-2/P1-5/P1-6/V2-P0-2/V2-P1-1/V2-P1-4/V2-P2-13d/V3-P0-2记录详实
- ✅ 文件清单与组件清单行数准确
**但架构图存在以下遗漏/不一致**(详见第五章):
1. 未记录 `AnnouncementDetail` 组件中 `canManage=true` 分支为**死代码**(无任何页面使用)
2. 未记录 `getAnnouncementReadStatusAction` 为**死代码**(无任何调用方)
3. 未记录 `/announcements/[id]` 路由层存在的**安全越权风险**(无受众过滤)
4. 未记录 `resolveAudience` 内部函数的**多孩子/多年级数据截断 Bug**
5. 未记录 actions 返回的英文字符串未走 i18n 的问题
---
## 二、现存问题与原因分析
### 2.1 【P0 · 安全越权】用户端详情页无受众/状态过滤
- **位置**[src/app/(dashboard)/announcements/[id]/page.tsx:26-27](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/[id]/page.tsx)
- **问题**:详情页直接调用 `getAnnouncementById(id)`,未传入 `audience` 也未校验 `status === "published"`
- **后果**:任意持有 `ANNOUNCEMENT_READ` 权限的登录用户,只要知道/猜到公告 IDcuid2即可读取
- 草稿(`status="draft"`)公告——提前泄露未发布内容
- 已归档(`status="archived"`)公告——绕过归档语义
- 其他年级/班级的定向公告——跨班级信息泄露(如某班处分通知被外班学生读到)
- **违反规则**:项目规则"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤" + "Parent routes must include permission checks with both `parentId` and `studentId` to prevent information leakage"。
- **根因**`getAnnouncementById` 设计为通用读取函数,未提供"按受众过滤"重载;路由层也未在读取后做二次校验。
### 2.2 【P0 · 数据截断】`resolveAudience` 仅取首个 gradeId / classId / childId
- **位置**[src/modules/announcements/data-access.ts:351-395](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts)
- **问题**
```ts
if (dataScope.type === "grade_managed") {
const gradeId = dataScope.gradeIds[0] // ⚠️ 仅取第一个
}
if (dataScope.type === "class_members" || dataScope.type === "class_taught") {
const classId = dataScope.classIds[0] // ⚠️ 仅取第一个
}
if (dataScope.type === "children") {
const childId = dataScope.childrenIds[0] // ⚠️ 仅取第一个孩子
}
```
- **后果**
- **家长**有多个孩子在不同班级/年级时,只能看到第一个孩子的定向公告,第二个孩子的班主任通知完全不可见——直接违反 K12 家长端核心诉求。
- **年级主任**管理多个年级时,只能看到第一个年级的公告。
- **教师**任课多个班级时,只能看到第一个班级的公告。
- **违反规则**:项目规则"Parent routes must include permission checks with both `parentId` and `studentId`" 与"data-access 层结合当前用户权限过滤"。
- **根因**`getAnnouncements` 的 `audience` 参数设计为单值 `{ gradeId?, classId? }`,不支持多值;`resolveAudience` 为迁就该签名做了截断。
### 2.3 【P0 · 越权写】置顶/已读 Action 缺少资源所有权二次校验
- **位置**[src/modules/announcements/actions.ts:335-376](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)
- **问题**
- `toggleAnnouncementPinAction` 仅校验 `ANNOUNCEMENT_MANAGE`,未校验公告是否存在、未校验调用者是否为该公告作者或管理员范围。
- `markAnnouncementAsReadAction` 仅校验 `ANNOUNCEMENT_READ`,未校验该公告是否对当前用户可见(即未结合 2.1 的受众过滤)。任意用户可对任意公告 ID包括草稿、他人班级公告写入已读记录污染 `announcement_reads` 表。
- **后果**:数据库完整性被破坏;统计 `readCount` 失真;为后续基于已读率的分析埋下错误数据。
- **违反规则**:项目规则"Server Action 二次校验"。
- **根因**Action 层信任了 `requirePermission` 的角色校验未做资源级resource-level授权。
### 2.4 【P1 · i18n 违规】Actions 返回英文硬编码消息
- **位置**[src/modules/announcements/actions.ts](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts) 全文
- **问题**:所有 Action 返回的 `ActionState.message` 均为英文字符串:
- `"Announcement created"` / `"Announcement updated"` / `"Announcement deleted"`
- `"Announcement published"` / `"Announcement archived"`
- `"Announcement not found"` / `"Invalid form data"` / `"Unexpected error"`
- `"Pin status toggled"` / `"Announcement marked as read"`
- 这些 message 通过 `toast.success(res.message)` / `toast.error(res.message)` 直接展示给用户(见 [announcement-detail.tsx:80,100,115](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx) 与 [announcement-form.tsx:80,88](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx))。
- **后果**:中文用户在创建/发布/删除公告后看到英文 Toasti18n 字典中已定义的 `messages.created` / `messages.updated` 等翻译键完全未使用。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n" + "Server Action 返回值统一采用 `ActionState<T>` 类型"(隐含 message 应可本地化)。
- **根因**Actions 在 try 块内同步返回字符串,未通过 `getTranslations("announcements")` 获取本地化文案i18n 字典定义了键但 Action 未消费。
### 2.5 【P1 · 死代码】`AnnouncementDetail` 管理分支与 `getAnnouncementReadStatusAction` 无调用方
- **位置**
- [src/modules/announcements/components/announcement-detail.tsx:165-200](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx)`canManage` 为 true 时的发布/归档/删除/置顶/编辑按钮组)
- [src/modules/announcements/actions.ts:381-391](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)`getAnnouncementReadStatusAction`
- **问题**
- 全仓搜索 `AnnouncementDetail` 的使用方,仅 [src/app/(dashboard)/announcements/[id]/page.tsx:34-38](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/[id]/page.tsx) 一处,且 `canManage={false}`。管理端 `/admin/announcements/[id]` 直接渲染 `AnnouncementForm`(编辑模式),**没有管理端详情页**。
- 全仓搜索 `getAnnouncementReadStatusAction`**零调用方**。该 Action 返回 `Record<string,boolean>`,本应用于列表页批量标记已读/未读,但列表页从未调用。
- **后果**
- 管理员无法在 UI 中执行发布/归档/删除/置顶操作(除非进入编辑表单),严重限制了管理端可用性。
- `announcement.readCount` 字段在 `AnnouncementDetail` 中展示,但因 `canManage` 永远为 false**用户永远看不到已读人数**——已读统计功能在 UI 层完全不可见。
- 列表页公告卡片没有"已读/未读"视觉区分,已读回执的数据无法驱动 UI。
- **违反规则**:项目规则"避免 backwards-compatibility hacks ... 如果确定未使用,应完全删除" + "识别四个角色共用的 UI 块"。
- **根因**:管理端路由设计遗漏了详情视图;已读状态查询 Action 未被列表组件消费。
### 2.6 【P1 · 耦合】组件直接 import actions未通过 Context/Provider 注入
- **位置**
- [announcement-card.tsx:13](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-card.tsx)`import { toggleAnnouncementPinAction } from "../actions"`
- [announcement-detail.tsx:25-31](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx)`import { archiveAnnouncementAction, deleteAnnouncementAction, markAnnouncementAsReadAction, publishAnnouncementAction, toggleAnnouncementPinAction } from "../actions"`
- [announcement-form.tsx:21](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx)`import { createAnnouncementAction, updateAnnouncementAction } from "../actions"`
- **问题**:组件硬编码依赖具体 Server Action无法在不修改组件代码的前提下替换为 mock 实现。
- **后果**
- 组件不可单元测试(必须 mock 整个 `../actions` 模块)。
- 无法为不同角色注入不同实现(如家长端只读、教师端可编辑班级公告)。
- 未来若要将公告组件复用于"班级空间"或"家长端聚合页",必须重写组件。
- **违反规则**:用户要求"完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。
- **根因**:组件设计未遵循依赖注入原则。
### 2.7 【P1 · 耦合】`AnnouncementForm` 硬编码路由跳转
- **位置**[announcement-form.tsx:82,217](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx)
- **问题**:表单提交成功后 `router.push("/admin/announcements")`,取消按钮也跳转到 `/admin/announcements`。
- **后果**:表单无法在管理端以外的场景复用(如教师端发布班级公告、嵌入到班级详情页的快速发布公告入口)。
- **违反规则**:用户要求"组合优先 ... 逻辑复用一律抽取为自定义 hooks" + "最大化复用"。
- **根因**:表单未通过 `onSuccess` / `onCancel` 回调或 `successHref` prop 解耦导航。
### 2.8 【P1 · 业务逻辑位置】`resolveTargetUserIds` 放在 actions.ts
- **位置**[src/modules/announcements/actions.ts:48-66](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)
- **问题**:受众解析 + 用户 ID 聚合是纯业务逻辑(无 I/O 副作用之外的逻辑),却放在 Server Action 文件中,与 Action 编排逻辑混杂。
- **后果**
- 无法独立单元测试(必须 mock `getAllUserIds` / `getStudentIdsByClassId` 等跨模块 data-access
- 与 `data-access.ts` 中的 `resolveAudience` 形成两套受众解析逻辑,职责重叠。
- **违反规则**:项目规则"可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离" + "Server Actions / Data Access 模块:建议 ≤ 800 行 ... 超过应考虑拆分"。
- **根因**actions.ts 既承担 HTTP 编排又承担业务规则,未分离 service 层。
### 2.9 【P1 · 性能】`toggleAnnouncementPin` 与 `markAnnouncementAsRead` 非原子操作
- **位置**[src/modules/announcements/data-access.ts:210-224,234-250](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts)
- **问题**
- `toggleAnnouncementPin`:先 `SELECT isPinned`,再 `UPDATE`。两次 DB 往返,且在并发场景下存在 lost update两个管理员同时切换会得到错误结果
- `markAnnouncementAsRead`:先 `SELECT id`,再 `INSERT`。已有唯一索引保证幂等,但多一次 SELECT 浪费往返。
- **后果**高并发时数据不一致DB 负载翻倍。
- **违反规则**:项目规则"性能:优先使用 React Server Components"(隐含高效数据访问)。
- **根因**:未使用 Drizzle 的 `sql` 表达式或 `onDuplicateKeyUpdate`/`INSERT IGNORE` 语义。
### 2.10 【P1 · 错误处理】`handleActionError` 吞错误上下文
- **位置**[src/modules/announcements/actions.ts:34-40](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)
- **问题**
```ts
function handleActionError(e: unknown): ActionState<never> {
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
if (e instanceof Error) return { success: false, message: e.message }
return { success: false, message: "Unexpected error" }
}
```
- 未 `console.error` 记录错误堆栈,生产环境无法定位故障。
- 直接把 `e.message` 返回给前端,可能泄露内部错误信息(如 SQL 错误)。
- "Unexpected error" 为英文硬编码。
- **后果**:可观测性差;安全信息泄露风险。
- **违反规则**:项目规则"错误与边界处理" + "i18n 就绪"。
- **根因**:错误处理未与日志/埋点/i18n 集成。
### 2.11 【P2 · a11y】置顶按钮嵌套在 `<Link>` 内的键盘交互问题
- **位置**[announcement-card.tsx:80-92,116-121](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-card.tsx)
- **问题**`AnnouncementCard` 在有 `href` 时用 `<Link>` 包裹整个卡片,同时卡片内的"置顶"按钮是一个 `<button>`。`handleTogglePin` 调用 `e.preventDefault()` + `e.stopPropagation()` 处理鼠标点击,但:
- 键盘聚焦到置顶按钮后按 `Enter`,部分浏览器会同时触发外层 `<a>` 的导航。
- 屏幕阅读器会朗读"链接 标题",但置顶按钮的 `aria-label` 在链接上下文中语义模糊。
- **后果**键盘用户可能误跳转a11y 不达标。
- **违反规则**:项目规则"可访问性a11y语义化标签、ARIA 属性、键盘导航"。
- **根因**:交互按钮不应嵌套在导航链接内;应使用"卡片头部可点击 + 操作按钮独立"的布局。
### 2.12 【P2 · 类型不安全】`mapRow` 内联对象类型与 schema 脱钩
- **位置**[src/modules/announcements/data-access.ts:23-53](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts)
- **问题**`mapRow` 的参数类型是手写的内联对象,未使用 Drizzle 推导类型 `typeof announcements.$inferSelect`。
- **后果**schema 变更(如新增字段)时,`mapRow` 不会在编译期报错,导致类型漂移。
- **违反规则**:项目规则"TypeScript 严格模式 ... 函数返回值必须显式标注"。
- **根因**:未利用 Drizzle 的类型推导能力。
### 2.13 【P2 · i18n 字典冗余/缺失并存】
- **位置**[src/shared/i18n/messages/zh-CN/announcements.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/announcements.json)
- **问题**
- 已定义但未使用的键:`messages.created` / `messages.updated` / `messages.deleted` / `messages.published` / `messages.archived` / `messages.notFound` / `messages.createFailed` / `messages.invalidForm` / `messages.markedRead`(共 9 个死键,因 actions 未消费)。
- 缺失的键:`description.detail`(详情页描述)、`description.create`(创建 Dialog 描述)。
- **后果**i18n 字典维护成本上升;新增页面时找不到对应键。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **根因**i18n 键与代码未做同步校验。
### 2.14 【P2 · 死分支】`detailHrefBuilder` prop 未被使用
- **位置**[announcement-list.tsx:39,70-74](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-list.tsx)
- **问题**`AnnouncementList` 同时支持 `detailHrefPrefix`(字符串前缀)和 `detailHrefBuilder`(函数)两种 prop但全仓搜索 `detailHrefBuilder` 的传入方为零(所有调用方都使用 `detailHrefPrefix`)。
- **后果**:死代码增加维护负担。
- **违反规则**:项目规则"避免 backwards-compatibility hacks"。
- **根因**V3 重构引入 `detailHrefPrefix` 后未清理旧 prop。
### 2.15 【P2 · 重复骨架屏】用户端与管理端 loading.tsx 完全重复
- **位置**
- [src/app/(dashboard)/announcements/loading.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/loading.tsx)
- [src/app/(dashboard)/admin/announcements/loading.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/announcements/loading.tsx)
- **问题**:两个文件几乎逐行重复(仅管理端多一个"新建公告"按钮骨架),未抽取共享骨架屏组件。
- **后果**UI 调整需改两处。
- **违反规则**:项目规则"Shared components must be extracted when page duplication exceeds 90%"。
- **根因**:未识别到骨架屏也是可复用 UI 块。
### 2.16 【P2 · 无分页 UI】`getAnnouncements` 支持分页但 UI 未消费
- **位置**[data-access.ts:55-112](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts) 支持 `page` / `pageSize`[announcement-list.tsx](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-list.tsx) 无分页控件。
- **问题**:列表页默认 `pageSize=20`,超过 20 条公告时静默截断,用户无法翻页。
- **后果**历史公告不可访问K12 学校一学期公告数通常 > 20。
- **违反规则**:用户要求"可扩展性:配置驱动设计"。
- **根因**:分页参数未贯穿到 UI。
### 2.17 【P2 · 表单与 Dialog 行为冲突】
- **位置**[admin-announcements-view.tsx:57-64](file:///e:/Desktop/CICD/src/modules/announcements/components/admin-announcements-view.tsx)
- **问题**`AdminAnnouncementsView` 在 Dialog 中渲染 `AnnouncementForm`(创建模式)。但 `AnnouncementForm` 的 Cancel 按钮 `router.push("/admin/announcements")` 会触发整页跳转,而不是关闭 Dialog。提交成功后也是 `router.push` 而非 `onSuccess` 回调。
- **后果**用户体验割裂Dialog 内按钮触发路由跳转);`handleOpenChange` 中的 `router.refresh()` 与表单跳转重复。
- **违反规则**:项目规则"组合优先 ... 严禁使用继承或深层嵌套 HOC"。
- **根因**:表单未与容器解耦。
---
## 三、行业差距对比
参考 Google Classroom、钉钉教育、企业微信家校通、飞书校园版、PowerSchool 等主流 K12 产品的公告/通知模块,对比差距如下:
| 维度 | 行业主流实践 | 当前实现 | 差距影响 |
|------|------------|---------|---------|
| **多受众定向** | 支持多班级/多年级/多角色组合发布(如"高三1班+2班家长" | 仅支持单年级或单班级 | 年级组长需重复发布 N 次 |
| **富文本/附件** | 富文本编辑器 + 附件PDF 通知、图片) | 纯文本 `whitespace-pre-wrap` | 学校正式通知无法排版、无法附带 PDF |
| **分类/标签** | 学科、活动、安全、家长信等分类筛选 | 仅按 status 筛选 | 家长在海量公告中找不到关注项 |
| **定时发布** | 选择未来时间自动发布 | schema 有 `publishedAt` 但 UI 未消费 | 管理员需手动踩点发布 |
| **到期/置顶** | 自动到期 + 多级优先级(紧急/普通) | 仅 pinned 布尔 | 紧急通知与普通通知无差异 |
| **已读统计仪表盘** | 管理端列表展示每条公告已读率、未读名单、可一键催读 | `readCount` 字段存在但 UI 未展示 | 管理员无法评估公告触达效果 |
| **草稿预览** | 编辑时预览发布后效果 | 无预览 | 发布前无法验证排版 |
| **批量操作** | 列表多选 + 批量归档/删除 | 逐条操作 | 学期末清理 50 条公告需 50 次点击 |
| **搜索** | 标题/正文全文搜索 | 无搜索 | 历史公告无法检索 |
| **Dashboard 集成** | 首页"最新公告"Widget + 未读红点 | 仅家长端有快速入口链接 | 用户必须主动进入公告页 |
| **通知点击回跳** | 点击通知直达公告详情并自动已读 | 通知 actionUrl 指向详情页,但详情页无受众校验 | 通知点击可能触发越权 |
| **多语言/多角色文案** | 同一公告对家长/学生/教师展示不同侧重点 | 同一文案对所有角色 | 家长看到教师内部用语 |
| **无障碍** | 列表语义化 `<ul>`/`<li>`、键盘可达 | `<div>` + `<Link>` 包裹按钮 | 屏幕阅读器用户导航困难 |
| **错误恢复** | 失败自动重试 + 离线草稿 | 失败仅 Toast 提示 | 网络波动时内容丢失 |
**核心差距**:当前实现停留在"CRUD + 状态机"的最小可用形态,缺少 K12 公告模块的"触达-反馈-统计"闭环。其中"已读统计不可见"和"无富文本/附件"是 K12 学校最痛的两个缺口。
---
## 四、改进优先级建议
### P0必须立即修复 · 安全与数据正确性)
| # | 问题 | 改进方向 |
|---|------|---------|
| P0-1 | 详情页越权读取 | 新增 `getAnnouncementByIdForUser(id, userId, dataScope)` data-access 函数,结合 `status="published"` 与受众过滤;路由层调用此函数,未命中返回 `notFound()` |
| P0-2 | `resolveAudience` 多孩子/多年级截断 | 将 `audience` 参数升级为 `{ gradeIds: string[]; classIds: string[] }``getAnnouncements` 用 `inArray` 查询;`resolveAudience` 返回完整数组而非首个 |
| P0-3 | 置顶/已读 Action 缺资源级校验 | `toggleAnnouncementPinAction` 校验公告存在;`markAnnouncementAsReadAction` 调用新增的 `getAnnouncementByIdForUser` 校验可见性后再写入 |
### P1高优先级 · 架构与可维护性)
| # | 问题 | 改进方向 |
|---|------|---------|
| P1-1 | Actions 返回英文硬编码 | 引入 `getTranslations("announcements")`,所有 `ActionState.message` 改用 i18n 键;新增 `messageKey` 字段或直接返回本地化字符串 |
| P1-2 | 死代码:管理端详情分支 / `getAnnouncementReadStatusAction` | 新增 `/admin/announcements/[id]/view` 详情页消费 `AnnouncementDetail canManage=true`;列表组件调用 `getAnnouncementReadStatusAction` 展示已读/未读角标;或删除死分支 |
| P1-3 | 组件直接 import actions | 新建 `announcements-service-context.tsx`,定义 `AnnouncementsService` 接口(含 `togglePin` / `publish` / `archive` / `delete` / `markRead` / `create` / `update` 方法签名),用 Provider 注入默认实现;组件 `useContext` 消费 |
| P1-4 | `AnnouncementForm` 硬编码路由 | 新增 `onSuccess?` / `onCancel?` 回调 prop回调优先于 `router.push`;默认 `successHref` prop 兜底 |
| P1-5 | `resolveTargetUserIds` 放 actions.ts | 下沉到 `data-access.ts` 的 `resolveAnnouncementTargetUserIds(announcement)` 纯函数actions.ts 仅做编排 |
| P1-6 | 非原子 toggle / markRead | `toggleAnnouncementPin` 改为 `UPDATE ... SET is_pinned = NOT is_pinned``markAnnouncementAsRead` 改为 `INSERT ... ON DUPLICATE KEY UPDATE id=id`Drizzle 的 `onDuplicateKeyUpdate` |
| P1-7 | `handleActionError` 吞错误 | 新增 `console.error` + `trackEvent("announcement.action_error")`message 走 i18n不向客户端返回原始 `e.message` |
| P1-8 | 表单与 Dialog 行为冲突 | 表单通过 `onSuccess` 回调关闭 Dialog移除表单内的 `router.push` |
### P2中优先级 · 体验与工程化)
| # | 问题 | 改进方向 |
|---|------|---------|
| P2-1 | a11y按钮嵌套在 Link 内 | 重构 `AnnouncementCard`:卡片本身为 `<Link>`,置顶按钮用绝对定位 + `z-index` 独立于链接,或改用 `<article>` + 独立链接 + 独立按钮的语义结构 |
| P2-2 | `mapRow` 类型脱钩 | 改用 `typeof announcements.$inferSelect` 推导;移除手写内联类型 |
| P2-3 | i18n 死键 / 缺键 | 删除未使用的 9 个 `messages.*` 死键(或随 P1-1 启用);补 `description.detail` / `description.create` |
| P2-4 | `detailHrefBuilder` 死 prop | 删除该 prop仅保留 `detailHrefPrefix` |
| P2-5 | 重复骨架屏 | 抽取 `AnnouncementListSkeleton` 共享组件到 `components/` |
| P2-6 | ✅ 已实施 | 无分页 UI → 新增 `AnnouncementPagination` 组件;`getUserAnnouncementsPageData` 返回 `{ items, total, page, pageSize }` |
| P2-7 | ✅ 已实施 | 无测试 → 新增 `schema.test.ts`18 测试,`refineAudience` 矩阵)、`is-announcement-visible.test.ts`15 测试,纯函数含多孩子场景)、`announcement-card.test.tsx`16 测试,交互 + a11y |
### 中长期P3 · 功能演进,对应行业差距)
| # | 方向 | 说明 |
|---|------|------|
| P3-1 | 富文本 + 附件 | 接入 `files` 模块(已支持 `targetType="announcement"`);引入轻量富文本编辑器(如 Tiptap |
| P3-2 | 分类/标签 | 新增 `announcement_tags` 表 + 列表筛选;预置 K12 分类(学科/活动/安全/家长信) |
| P3-3 | 定时发布 | 表单增加 `publishedAt` 日期选择器;新增 cron 校验到点自动 `status="published"` |
| P3-4 | 已读统计仪表盘 | 管理端列表展示已读率柱状图;详情页展示未读名单 + 一键催读(触发 `sendBatchNotifications` |
| P3-5 | 批量操作 | 列表多选 + 批量归档/删除 Action |
| P3-6 | 全文搜索 | 接入 `app/api/search` 已有的全局搜索(当前已支持 announcement 类型) |
| P3-7 | Dashboard 集成 | 新增 `AnnouncementsWidget`(最新 3 条 + 未读红点),挂载到各角色 Dashboard |
| P3-8 | 草稿预览 | 表单"预览"按钮展开只读视图 |
---
## 五、架构图同步说明
本次审计发现 `004_architecture_impact_map.md` §2.16 与 `005_architecture_data.json#announcements` 存在以下遗漏,需在实施后同步更新:
1. **新增节点**
- `data-access.resolveAnnouncementTargetUserIds`P1-5 下沉的纯函数)
- `data-access.getAnnouncementByIdForUser`P0-1 新增的受众过滤读取)
- `AnnouncementsServiceContext`P1-3 新增的依赖注入 Provider
- `AnnouncementListSkeleton`P2-5 抽取的共享骨架屏)
- `AnnouncementPagination`P2-6 新增的分页组件)
2. **删除节点**
- `actions.getAnnouncementReadStatusAction`(若 P1-2 选择删除而非启用)
- `AnnouncementList.detailHrefBuilder` propP2-4 删除)
3. **修改节点**
- `GetAnnouncementsParams.audience` 类型从 `{ gradeId?; classId? }` 改为 `{ gradeIds: string[]; classIds: string[] }`
- `AnnouncementDetail` 的 `canManage` 分支启用记录(新增管理端详情页后)
- `actions.ts` 行数变化(下沉 `resolveTargetUserIds` 后减少)
- `data-access.ts` 行数变化(新增函数后增加)
4. **新增依赖关系**
- `announcements → files`P3-1 附件集成后)
- `announcements → dashboard`P3-7 Widget 集成后)
5. **已知问题清单更新**
- 新增"P0-1 详情页越权"(修复后标记 ✅)
- 新增"P0-2 多孩子截断"(修复后标记 ✅)
- 新增"P0-3 资源级校验缺失"(修复后标记 ✅)
- 新增"P1-1 Actions i18n"(修复后标记 ✅)
---
## 附:实施清单(与上述优先级一一对应)
实施将按 P0 → P1 → P2 → P3 顺序推进P3 为中长期演进,本次实施聚焦 P0/P1/P2P3 中富文本/附件/Dashboard 集成将择期推进。每完成一项同步更新架构图与运行 `npm run lint` + `npx tsc --noEmit` 验证。

View File

@@ -0,0 +1,251 @@
# 公告和消息模块审计报告 V3
> 审查日期2026-06-22
> 审查范围V2 改进后的 `src/modules/announcements/**`、`src/modules/messaging/**`、`src/modules/notifications/**`、对应路由层、i18n 翻译文件
> 前置文档:`announcements-messages-audit-report.md`V1、`announcements-messages-audit-report-v2.md`V2
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.13 / §2.14 / §2.16
---
## 一、V2 完成情况复核
| V2 编号 | 标题 | 状态 |
|---------|------|------|
| V2-P0-1 | 通知 i18n 命名空间独立 | ✅ 已完成 |
| V2-P0-2 | 通知标题 i18n 化 | ✅ 已完成 |
| V2-P1-1 | AnnouncementList 过滤模式统一 | ✅ 已完成 |
| V2-P1-2 | MessageList 过滤冗余移除 | ✅ 已完成 |
| V2-P1-3 | 消息详情页编排下沉 | ✅ 已完成 |
| V2-P1-4 | 表单服务端校验错误展示 | ✅ 已完成 |
| V2-P2-1 | 轮询间隔常量化 | ✅ 已完成 |
| V2-P2-2 | 架构图同步 | ✅ 已完成 |
| V2-P2-13b | 通知优先级和归档 | ✅ 已完成 |
| V2-P2-13c | 通知分类筛选 + 桌面推送 | ✅ 已完成 |
| V2-P2-13d | 公告置顶 + 已读回执 | ✅ 已完成 |
V2 共 11 项已全部实施。
---
## 二、V3 新发现问题
### 2.1 `saveMessageDraftAction` 中 `as` 断言违规P0
| 位置 | 代码 | 违反规则 |
|------|------|----------|
| [messaging/actions.ts](file:///e:/Desktop/CICD/src/modules/messaging/actions.ts) L279-283 | `formData.get("draftId") as string \| null` 等 5 处断言 | "禁止 `as` 断言(除非从 `unknown` 转换)" |
**问题分析**
- `FormData.get()` 返回类型为 `string | File | null`
- 代码直接断言为 `string | null`,若字段为 File 类型会导致运行时错误
- 应使用类型守卫或 Zod 校验进行安全转换
**后果**:类型不安全,上传场景下可能运行时崩溃;违反 TypeScript 严格模式规则。
### 2.2 公告列表页 `resolveAudience` 业务逻辑未下沉P0
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [announcements/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/page.tsx) L29-78 | `resolveAudience` 函数 50 行业务逻辑在路由层 | "app/ 只能调用 modules 的 Server Actions 和 data-access不直接访问数据库" + "页面层编排应下沉" |
**问题分析**
- `resolveAudience` 包含根据 dataScope 解析受众的复杂业务逻辑5 种 dataScope 分支)
- 直接调用 `classes` 模块的 3 个 data-access 函数(`getStudentActiveClassId``getStudentActiveGradeId``getClassGradeId`
- V2-P1-5 已为管理端和编辑页创建编排函数,但用户端列表页遗漏
**后果**:路由层承担业务逻辑,违反三层架构;逻辑无法复用;测试困难。
### 2.3 通知偏好 Action 放置位置错误P1
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [messaging/actions.ts](file:///e:/Desktop/CICD/src/modules/messaging/actions.ts) L334-396 | `getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 在 messaging 模块 | "模块标准结构" — 通知偏好属于 notifications 模块 |
**问题分析**
- V1-P0-4 已将通知偏好 data-access 迁移到 `notifications/preferences.ts`
- 但对应的 Server Action 仍留在 `messaging/actions.ts`,使用 `MESSAGE_READ` 权限
- 消费方settings 模块)通过 `SettingsService` 接口注入,但 Action 实现仍在 messaging
**后果**模块边界混乱messaging 模块承担了不属于它的通知偏好职责;权限语义不正确。
### 2.4 `sendBatchNotifications` 重复日志P1
| 位置 | 代码 | 违反规则 |
|------|------|----------|
| [notifications/dispatcher.ts](file:///e:/Desktop/CICD/src/modules/notifications/dispatcher.ts) L128 + L149 | `sendNotification` 内部调用 `logNotificationSendBatch``sendBatchNotifications` 又调用一次 | "代码质量规则" — 重复逻辑 |
**问题分析**
- L128`sendNotification` 末尾调用 `logNotificationSendBatch(results, ...)`
- L149`sendBatchNotifications` 末尾再次调用 `logNotificationSendBatch(flatResults)`
- 批量发送时每条通知的日志被记录两次
**后果**:日志数据重复,影响监控准确性;浪费存储。
### 2.5 腾讯云短信 `SmsSdkAppId` 配置混淆P1
| 位置 | 代码 | 违反规则 |
|------|------|----------|
| [sms-channel.ts](file:///e:/Desktop/CICD/src/modules/notifications/channels/sms-channel.ts) L201, L203 | `SmsSdkAppId: this.config.templateCode``TemplateId: this.config.templateCode` | "类型安全" — 配置语义错误 |
**问题分析**
- 腾讯云 SMS API 要求 `SmsSdkAppId`(应用 ID`TemplateId`(模板 ID是两个不同值
- 代码中两者都使用 `this.config.templateCode`(来自 `SMS_TEMPLATE_CODE` 环境变量)
- `getSmsConfig()` 缺少 `smsSdkAppId` 字段
**后果**腾讯云短信发送必然失败SmsSdkAppId 不等于模板 ID生产环境无法使用腾讯云短信。
### 2.6 内联类型导入P1
| 位置 | 代码 | 违反规则 |
|------|------|----------|
| [messaging/actions.ts](file:///e:/Desktop/CICD/src/modules/messaging/actions.ts) L260 | `Promise<ActionState<import("./types").MessageDraft[]>>` | "TypeScript 规则 — 仅用于类型的导入必须使用 import type" |
| [messaging/data-access.ts](file:///e:/Desktop/CICD/src/modules/messaging/data-access.ts) L285 | `import("@/modules/notifications/types").Notification[]` | 同上 |
**后果**:可读性差,不符合 TypeScript 规范。
### 2.7 组件层 `as` 断言轻微违规P2
| 位置 | 代码 | 违反规则 |
|------|------|----------|
| [message-list.tsx](file:///e:/Desktop/CICD/src/modules/messaging/components/message-list.tsx) L66 | `setTab(v as Tab)` | "禁止 as 断言" |
| [notification-list.tsx](file:///e:/Desktop/CICD/src/modules/notifications/components/notification-list.tsx) L106 | `Object.keys(TYPE_ICON) as NotificationType[]` | 同上 |
**后果**:类型不安全;应使用类型守卫。
### 2.8 `logNotificationSend` 使用 consoleP2
| 位置 | 代码 | 违反规则 |
|------|------|----------|
| [notifications/data-access.ts](file:///e:/Desktop/CICD/src/modules/notifications/data-access.ts) L210, L230 | `console.info` / `console.error` | "监控" — 应使用统一日志服务 |
**后果**:生产环境日志分散,无法集中监控。
---
## 三、行业差距对比
结合 K12 教育系统特点,对比钉钉教育、企业微信教育版、智学网、班级小管家等产品:
| 功能 | 我们 | 行业标杆 | 影响 |
|------|------|---------|------|
| 消息已读回执 | ❌ 无 | ✅ 钉钉/企微支持 | 教师无法确认家长是否已读重要通知 |
| 消息模板 | ❌ 无 | ✅ 智学网预设模板 | 教师每次手写消息效率低 |
| 公告定时发布 | ❌ 仅即时发布 | ✅ 钉钉支持定时 | 无法提前编排非工作时间发布 |
| 公告附件 | ❌ 无 | ✅ 企微/钉钉支持 | 无法附带 PDF/图片等材料 |
| 公告分类标签 | ❌ 无 | ✅ 智学网分类 | 公告列表无法按类型快速筛选 |
| 消息群发多班级 | ❌ 单收件人 | ✅ 钉钉群发 | 教师需逐个发送,效率低 |
| 公告评论/确认 | ❌ 仅已读回执 | ✅ 钉钉确认回执 | 无法收集家长确认反馈 |
| 消息搜索 | ✅ 有(按主题) | ✅ 按内容搜索 | 基本满足 |
| 通知优先级 | ✅ 有V2-P2-13b | ✅ | 已对齐 |
| 通知归档 | ✅ 有V2-P2-13b | ✅ | 已对齐 |
| 桌面推送 | ✅ 有V2-P2-13c | ✅ | 已对齐 |
| SSE 实时推送 | ✅ 有V2-P3 | ✅ | 已对齐 |
**主要差距**:消息已读回执、公告定时发布、公告附件为高价值缺失功能,直接影响家校沟通效率。
---
## 四、改进优先级建议
### P0紧急影响类型安全与架构合规
1. **修复 `saveMessageDraftAction` 的 `as` 断言**:使用类型守卫替代 `formData.get() as string | null`,安全处理 File 类型。
2. **公告列表页 `resolveAudience` 下沉**:将受众解析逻辑迁移到 `announcements/data-access.ts`,新增 `getUserAnnouncementsPageData` 编排函数。
### P1重要影响模块边界与功能正确性
3. **通知偏好 Action 迁移**:将 `getNotificationPreferencesAction` / `updateNotificationPreferencesAction``messaging/actions.ts` 迁移到 `notifications/actions.ts`,使用通知相关权限。
4. **修复 `sendBatchNotifications` 重复日志**:移除 `sendBatchNotifications` 中的重复 `logNotificationSendBatch` 调用。
5. **修复腾讯云短信 `SmsSdkAppId` 配置**:新增 `SMS_SDK_APP_ID` 环境变量和 `smsSdkAppId` 配置字段。
6. **消除内联类型导入**:改为顶部 `import type`
### P2优化提升代码质量
7. **组件 `as` 断言清理**:使用类型守卫替代。
8. **日志服务统一**`logNotificationSend` 改用统一日志接口(预留接口,当前保持 console 但加注释标记为 TODO
---
## 五、架构图同步说明
本次审计发现以下架构图需更新:
1. **§2.13 messaging**
- 移除 `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`(迁移至 notifications
- 更新 `actions.ts` 行数(减少约 60 行)
- 新增 `saveMessageDraftAction` 类型安全改进说明
2. **§2.14 notifications**
- 新增 `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`
- 更新 `actions.ts` 行数(增加约 60 行)
- 修复 `sendBatchNotifications` 重复日志说明
- 新增 `smsSdkAppId` 配置字段说明
3. **§2.16 announcements**
- 新增 `getUserAnnouncementsPageData` 编排函数
- 更新 `data-access.ts` 行数
- 更新用户端列表页说明(使用编排函数)
4. **附录 A 依赖矩阵**
- messaging 对 notifications 的依赖减少(不再包含通知偏好 Action
- announcements 对 classes 的依赖改为通过编排函数间接调用
---
## 六、实施记录
以下为 V3 审计报告的实施记录,所有修复均已完成并通过 `npx tsc --noEmit``npm run lint` 验证。
### V3-P0-1修复 `saveMessageDraftAction` 的 `as` 断言
**文件**`src/modules/messaging/actions.ts`
**变更**:将 L279-283 的 5 处 `formData.get("xxx") as string | null` 替换为类型守卫函数 `getStringFromFormData`,安全处理 `File` 类型。
### V3-P0-2公告列表页 `resolveAudience` 下沉
**文件**
- `src/modules/announcements/data-access.ts`:新增 `getUserAnnouncementsPageData` 编排函数
- `src/app/(dashboard)/announcements/page.tsx`:移除 `resolveAudience`,改用编排函数
### V3-P1-3通知偏好 Action 迁移
**文件**
- `src/modules/notifications/actions.ts`:新增 `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`
- `src/modules/messaging/actions.ts`:移除上述 2 个 Action 及相关 import
### V3-P1-4修复 `sendBatchNotifications` 重复日志
**文件**`src/modules/notifications/dispatcher.ts`
**变更**:移除 `sendBatchNotifications` 中的重复 `logNotificationSendBatch` 调用L149
### V3-P1-5修复腾讯云短信 `SmsSdkAppId` 配置
**文件**`src/modules/notifications/channels/sms-channel.ts`
**变更**`getSmsConfig()` 新增 `smsSdkAppId` 字段(来自 `SMS_SDK_APP_ID` 环境变量),`SmsSdkAppId` 使用独立配置值。
### V3-P1-6消除内联类型导入
**文件**
- `src/modules/messaging/actions.ts`L260 内联 import 改为顶部 `import type`
- `src/modules/messaging/data-access.ts`L285 内联 import 改为顶部 `import type`
### V3-P2-7组件 `as` 断言清理
**文件**
- `src/modules/messaging/components/message-list.tsx`L66 `setTab(v as Tab)` 改为类型守卫
- `src/modules/notifications/components/notification-list.tsx`L106 `Object.keys(TYPE_ICON) as NotificationType[]` 改为类型守卫
### V3-P2-8日志服务统一预留
**文件**`src/modules/notifications/data-access.ts`
**变更**`console.info` / `console.error` 添加 TODO 注释标记,预留统一日志服务接入点。
### 架构图同步
**文件**
- `docs/architecture/004_architecture_impact_map.md`§2.13 / §2.14 / §2.16 同步更新
- `docs/architecture/005_architecture_data.json`:对应节点同步更新

View File

@@ -0,0 +1,443 @@
# 考勤Attendance模块审计报告
> 审计日期2026-06-25
> 审计范围:`src/modules/attendance/**`、`src/app/(dashboard)/{admin,teacher,student,parent}/attendance/**`、跨模块依赖 `src/modules/parent/components/parent-attendance-*.tsx` 及 `child-detail-panel.tsx`、i18n `src/shared/i18n/messages/{en,zh-CN}/attendance.json`
> 参照规则:`docs/architecture/004_architecture_impact_map.md`(第 2.10 节)、`docs/architecture/005_architecture_data.json`L14681 起)、`.trae/rules/project_rules.md`
---
## 一、现有实现概要
### 1.1 文件分布
| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/attendance/actions.ts) | 258 | 5 个写 Action含权限校验、Zod 校验、归属校验) |
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts) | 340 | 考勤记录 CRUD + 规则 upsert + 总览统计 + recorder 解析 |
| 数据访问 | [data-access-stats.ts](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts) | 206 | 学生/班级考勤汇总(纯函数 `computeStats` + SQL 聚合) |
| Schema | [schema.ts](file:///e:/Desktop/CICD/src/modules/attendance/schema.ts) | 43 | Zod 校验5 个 schema |
| Types | [types.ts](file:///e:/Desktop/CICD/src/modules/attendance/types.ts) | 103 | 类型定义 |
| Constants | [constants.ts](file:///e:/Desktop/CICD/src/modules/attendance/constants.ts) | 64 | 状态选项/快捷键/颜色映射 |
| Export | [export.ts](file:///e:/Desktop/CICD/src/modules/attendance/export.ts) | 90 | Excel 导出 |
| 组件 | [components/attendance-page-layout.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-page-layout.tsx) | 38 | admin/teacher 共用布局插槽 |
| 组件 | [components/attendance-sheet.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx) | 418 | 批量点名表单快捷键、AlertDialog 确认) |
| 组件 | [components/attendance-record-list.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx) | 142 | 记录列表 + 删除对话框 |
| 组件 | [components/attendance-filters.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-filters.tsx) | 94 | URL 同步筛选器 |
| 组件 | [components/attendance-stats-card.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-card.tsx) | 82 | 单卡片 8 指标 |
| 组件 | [components/attendance-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-cards.tsx) | 83 | admin 总览 6 卡片网格 |
| 组件 | [components/attendance-stats-class-selector.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-class-selector.tsx) | 27 | 班级筛选 ChipNav |
| 组件 | [components/attendance-rules-form.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-rules-form.tsx) | 155 | 规则配置表单 |
| 组件 | [components/student-attendance-view.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/student-attendance-view.tsx) | 111 | 学生/家长视图 |
| 页面 | [admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx) | 91 | 管理员总览RSC |
| 页面 | [teacher/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx) | 116 | 教师记录列表RSC |
| 页面 | [teacher/attendance/sheet/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/sheet/page.tsx) | 44 | 教师点名页RSC |
| 页面 | [teacher/attendance/stats/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/stats/page.tsx) | 85 | 教师班级统计RSC |
| 页面 | [student/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/attendance/page.tsx) | 40 | 学生汇总RSC |
| 页面 | [parent/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/attendance/page.tsx) | 66 | 家长多子女聚合RSC |
| 错误边界 | 4 个 `error.tsx`admin/teacher/student/parent | ~96 | **重复严重** |
| 跨模块 | [parent/components/parent-attendance-calendar.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx) | 220 | 家长月历视图 |
| 跨模块 | [parent/components/parent-attendance-warning.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-warning.tsx) | 110 | 异常预警横幅 |
| 跨模块 | [parent/components/parent-attendance-rate-card.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-rate-card.tsx) | 114 | 出勤率汇总卡片 |
| 跨模块 | [parent/components/child-detail-panel.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/child-detail-panel.tsx) | 190 | 子女详情面板(含考勤 Tab |
### 1.2 数据流
```
page.tsx (RSC)
└─ getAttendanceRecords / getStudentAttendanceSummary / getClassAttendanceStats (data-access)
└─ db (drizzle) → attendanceRecords / attendanceRules 表
└─ ⚠ 直接 JOIN users / classes 表(跨模块表查询)
└─ 调用 classes/data-access.getClassActiveStudentsWithInfo合规
└─ <AttendanceSheet> (client) → batchRecordAttendanceAction
└─ <AttendanceRecordList> (client) → deleteAttendanceAction
└─ <AttendanceRulesForm> (client) → saveAttendanceRulesAction
```
### 1.3 架构图完整性评估
架构影响地图004 第 2.10 节)与 JSONL14681 起)**整体覆盖** attendance 模块,但存在 **6 处信息过时/不准确**(详见第五节),需同步更新。模块导出、权限点、依赖关系、路由已记录。
---
## 二、现存问题与原因分析
### 2.1 三层架构合规性
#### 问题 2.1.1data-access 层跨模块直接 JOIN 外部表【P0】
- **位置**[data-access.ts:118-119](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L118-L119)、[data-access.ts:77-81](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L77-L81)、[data-access-stats.ts:87-91](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L87-L91)、[data-access-stats.ts:161-165](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L161-L165)、[data-access-stats.ts:177-178](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L177-L178)
- **描述**`getAttendanceRecords` 直接 `leftJoin(users)``leftJoin(classes)``resolveRecorderNames` 直接 `select from users``getStudentAttendanceSummary`/`getClassAttendanceStats` 直接查询 `users`/`classes` 表。
- **违反规则**:项目规则「`modules/` 之间通过对方 data-access 通信,**不直接查询对方 DB 表**」。
- **原因**:为减少查询往返,在 attendance data-access 内联 JOIN 获取 studentName/className/recorderName。
- **后果**users/classes 模块 schema 变更(如 `users.name` 重命名)会直接破坏 attendance 查询;模块边界失效,无法独立演进。
#### 问题 2.1.2parent 模块直接 import attendance 组件【P1】
- **位置**[parent/attendance/page.tsx:4](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/attendance/page.tsx#L4)
- **描述**`parent/attendance/page.tsx` 直接 `import { StudentAttendanceView } from "@/modules/attendance/components/student-attendance-view"`,跨模块 UI 组件依赖。
- **违反规则**:项目规则「该模块必须作为独立功能单元,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access」。
- **原因**parent 复用 student 视图组件以减少重复。
- **后果**parent 模块与 attendance 模块 UI 强耦合attendance 调整 StudentAttendanceView 会影响 parent 页面。
#### 问题 2.1.3parent-attendance-calendar 直接依赖 attendance/constants【P1】
- **位置**[parent-attendance-calendar.tsx:9-12](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx#L9-L12)
- **描述**:直接 import `ATTENDANCE_STATUS_DOT_COLORS``ATTENDANCE_STATUS_LABEL_KEYS`
- **违反规则**:同上「完全解耦」原则。
- **原因**parent 类型已解耦(`parent/types.ts` 自声明类型),但常量仍直接依赖。
- **后果**attendance 常量变更影响 parent 月历渲染。
#### 问题 2.1.4架构图信息过时【P2】
- **位置**:架构图 004 第 1152、1154、1159、1175、1176、1195 行
- **描述**6 处描述与实际代码不符(详见第五节)。
- **违反规则**:项目规则「改码必同步图」。
- **后果**:架构图可信度下降,误导后续开发。
### 2.2 权限校验
#### 问题 2.2.1teacher 子页面缺失权限校验【P0】
- **位置**[teacher/attendance/sheet/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/sheet/page.tsx)、[teacher/attendance/stats/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/stats/page.tsx)
- **描述**:两个页面**无任何权限校验**,未调用 `requirePermission(Permissions.ATTENDANCE_READ)`,也未通过 `getAuthContext().dataScope` 过滤。
- **违反规则**:项目规则「所有 Server Action 必须调用 `requirePermission()` 进行权限校验」+ 项目记忆「Parent routes must include permission checks with both parentId and studentId」。
- **原因**RSC 页面非 Server Action开发者认为 data-access 内的 `buildScopeFilter` 会兜底。
- **后果**sheet 页调用 `getTeacherClasses()` **未传入 scope**[teacher/attendance/sheet/page.tsx:9](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/sheet/page.tsx#L9)),任何已登录用户访问 URL 即可获取教师班级学生列表stats 页同理。**存在数据越权风险**。
#### 问题 2.2.2teacher/student/parent 主页面权限校验不一致【P1】
- **位置**[teacher/attendance/page.tsx:39](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx#L39)、[student/attendance/page.tsx:11](file:///e:/Desktop/CICD/src/app/(dashboard)/student/attendance/page.tsx#L11)
- **描述**:仅 `getAuthContext()`,未 `requirePermission(ATTENDANCE_READ)`;而 [admin/attendance/page.tsx:30](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx#L30) 已建立该惯例。
- **违反规则**:权限校验应统一。
- **后果**:权限点缺失,无法通过权限矩阵精确控制 teacher/student 是否可访问考勤页。
#### 问题 2.2.3前端删除按钮无权限点控制【P2】
- **位置**[attendance-record-list.tsx:106-114](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx#L106-L114)
- **描述**:删除按钮对所有能看到列表的用户可见,未使用 `usePermission().hasPermission(Permissions.ATTENDANCE_MANAGE)` 控制显隐。
- **违反规则**:项目规则「前端权限判断统一使用 `usePermission().hasPermission()`」。
- **后果**:无权用户看到删除按钮,点击后才被 Server Action 拒绝,体验差。
### 2.3 i18n 国际化
#### 问题 2.3.1safeParseDate 中文 fieldName 硬编码【P0】
- **位置**[data-access.ts:100-102](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L100-L102)、[data-access.ts:167](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L167)、[data-access.ts:185](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L185)、[data-access.ts:308](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L308)、[data-access-stats.ts:95-96](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L95-L96)、[data-access-stats.ts:169-170](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L169-L170)
- **描述**`safeParseDate(value, "日期")``safeParseDate(value, "开始日期")` 等 10 处中文 fieldName`handleActionError` 返回客户端为用户可见错误消息。
- **违反规则**:项目规则「所有用户可见文本必须适配 i18n」。
- **后果**:英文环境下显示中文错误。
#### 问题 2.3.2action-utils shared 层中文兜底消息【P1】
- **位置**`shared/lib/action-utils.ts:32,70,74,143`
- **描述**`NotFoundError(\`${resource} 不存在\`)`、`"操作失败,请稍后重试"`、`${fieldName} 格式无效` 等。
- **违反规则**:同上。
- **后果**:所有调用 shared 层的模块(含 attendance均受影响。
#### 问题 2.3.3Excel 导出英文列头硬编码【P1】
- **位置**[export.ts:83-84](file:///e:/Desktop/CICD/src/modules/attendance/export.ts#L83-L84)
- **描述**`"Metric"``"Value"` 硬编码。
#### 问题 2.3.4calendar 硬编码 en-US locale【P1】
- **位置**[parent-attendance-calendar.tsx:102](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx#L102)
- **描述**`toLocaleDateString("en-US")`,未使用当前 locale。
#### 问题 2.3.5child-detail-panel 英文硬编码【P1】
- **位置**[child-detail-panel.tsx:50-56](file:///e:/Desktop/CICD/src/modules/parent/components/child-detail-panel.tsx#L50-L56)、L111、L137-143、L152、L160-163、L182、L44、L186
- **描述**Tab 标签、区块标题、占位提示、按钮文案共 20+ 处英文硬编码。
#### 问题 2.3.6翻译键误用【P0】
- **位置**
- [parent/attendance/error.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/attendance/error.tsx#L17)、[teacher/attendance/error.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/error.tsx#L17)、[admin/attendance/error.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/error.tsx#L17):重试按钮使用 `t("actions.save")` 而非 `t("actions.retry")`,显示"保存"。
- [attendance-record-list.tsx:127](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx#L127):删除确认对话框描述使用 `t("errors.unexpected")`"发生未知错误"),应为 `t("sheet.confirmDelete")`
- [attendance-rules-form.tsx:32](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-rules-form.tsx#L32):保存按钮显示 `t("rules.saved")`"考勤规则已保存"),应为 `t("actions.save")`
- [attendance-sheet.tsx:395](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L395):切换班级确认对话框标题误用 `t("sheet.confirmDelete")`,应为 `t("sheet.confirmClassSwitch")`
- **违反规则**i18n 正确性。
- **后果**:用户看到错误/误导文案。
#### 问题 2.3.7error.tsx title/description 重复【P2】
- **位置**4 个 error.tsx
- **描述**title 和 description 均用 `t("errors.unexpected")`,完全相同。
### 2.4 类型安全
#### 问题 2.4.1export.ts 无类型守卫的 as 断言【P1】
- **位置**[export.ts:28-34](file:///e:/Desktop/CICD/src/modules/attendance/export.ts#L28-L34)
- **描述**`params.status as "present" | "absent" | ...``params.status``string | undefined`,无类型守卫。
- **违反规则**:项目规则「禁止 `as` 断言(除非从 `unknown` 转换)」。
- **后果**:非法 status 值绕过类型检查。
#### 问题 2.4.2child-detail-panel as 断言【P2】
- **位置**[child-detail-panel.tsx:29](file:///e:/Desktop/CICD/src/modules/parent/components/child-detail-panel.tsx#L29)、L65
- **描述**`(VALID_TABS as string[]).includes(v)``v as ChildDetailTab`,已有 `isTab` 守卫但未在 `onValueChange` 使用。
### 2.5 错误处理与边界
#### 问题 2.5.1teacher 子路由缺失 error.tsx【P1】
- **位置**`teacher/attendance/sheet/``teacher/attendance/stats/`
- **描述**:缺失 error.tsx运行时错误冒泡到 `teacher/attendance/error.tsx`,错误上下文不准确。
- **违反规则**项目记忆「All student routes must include loading.tsx and error.tsx」。
#### 问题 2.5.2student 空状态文案错误【P2】
- **位置**[student/attendance/page.tsx:24-25](file:///e:/Desktop/CICD/src/app/(dashboard)/student/attendance/page.tsx#L24-L25)
- **描述**summary 为 null 时 EmptyState description 用 `t("errors.unexpected")`"发生未知错误"),实际原因可能是无考勤记录。
### 2.6 组件复用性
#### 问题 2.6.1:常量在 constants.ts 与 attendance-sheet.tsx 重复定义【P1】
- **位置**[attendance-sheet.tsx:50-83](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L50-L83)
- **描述**`STATUS_OPTIONS``STATUS_SHORTCUTS``createInitialStatusCounts` 与 constants.ts 重复(部分复用、部分重复的混乱状态)。
- **违反规则**DRY 原则。
- **后果**:状态选项变更需同步两处,易遗漏。
#### 问题 2.6.24 个 error.tsx 近乎完全重复【P1】
- **位置**4 个 error.tsx
- **描述**:结构完全相同,共约 96 行重复代码。
- **违反规则**项目记忆「Shared components must be extracted when page duplication exceeds 90%」。
- **后果**:修改一处需同步四处。
#### 问题 2.6.3:两个 stats 卡片组件数据结构分裂【P2】
- **位置**[attendance-stats-card.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-card.tsx)、[attendance-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-cards.tsx)
- **描述**:同一概念两套数据结构(`AttendanceStats` vs `AttendanceOverviewStats``stats.present`/`stats.presentRate` vs `stats.presentCount`/`stats.attendanceRate`)。
### 2.7 数据注入与解耦
#### 问题 2.7.1:无接口抽象与 Context 注入【P1】
- **描述**attendance 模块未定义任何 `AttendanceDataService` 接口,无 `AttendanceContext`/`AttendanceProvider`,无角色差异的接口多态实现。
- **违反规则**:项目规则「通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务」。
- **后果**:角色间无统一契约约束,参数/返回处理可能不一致;无法通过接口 mock 做单测。
### 2.8 可测试性
#### 问题 2.8.1data-access 无接口类型可供 mock【P2】
- **描述**data-access 函数直接导出为具体函数,无 `AttendanceRepository` 接口。
- **违反规则**:项目规则「导出清晰的接口类型以便 mock」。
### 2.9 a11y 可访问性
#### 问题 2.9.1:全局 keydown 监听可能冲突【P2】
- **位置**[attendance-sheet.tsx:164-186](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L164-L186)
- **描述**keydown 绑定在 window仅排除 input/textarea未排除 Select 等可交互组件。
### 2.10 性能
#### 问题 2.10.1getClassAttendanceStats 未用 SQL 聚合【P1】
- **位置**[data-access-stats.ts:172-182](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L172-L182)
- **描述**:仍用全量查询 + 内存 `computeStats`,而 `getStudentAttendanceSummary`/`getAttendanceStats` 已改用 SQL 聚合。
- **违反规则**:性能最佳实践。
- **后果**:大班级统计查询慢。
#### 问题 2.10.2teacher 分页基于截断数据计算【P0】
- **位置**[teacher/attendance/page.tsx:46-63](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx#L46-L63)
- **描述**:先获取 `result.items`pageSize=20再对**仅 20 条**做前端分页计算 totalPages。
- **后果**:分页页数错误,用户无法访问第 2 页之后数据。
#### 问题 2.10.3parent 组件可降级为 RSC【P2】
- **位置**[parent-attendance-warning.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-warning.tsx)、[parent-attendance-rate-card.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-rate-card.tsx)
- **描述**:仅因 `useTranslations` 标记 `"use client"`,可用 `getTranslations` 改为 RSC。
#### 问题 2.10.4statusCounts 未 memoize【P2】
- **位置**[attendance-sheet.tsx:151-157](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L151-L157)
- **描述**:每次 render 全量 reduce大班级性能损耗。
---
## 三、行业差距对比
基于 K12 教育系统考勤模块的主流实践(参照 PowerSchool、Infinite Campus、Veracross、校长推荐系统等当前模块相比优秀实践的差距
| 维度 | 优秀实践 | 当前状态 | 差距影响 |
|------|---------|---------|---------|
| **考勤状态细分** | present/absent/late/early-leave/excused-absent/school-activity + 事假/病假/公假原因 | 仅 present/absent/late/excused 4 态,无原因字段 | 学校无法区分病假/事假,无法生成请假原因统计 |
| **实时家长通知** | 学生缺席自动推送家长 App 通知/短信 | 仅被动查看,无主动推送 | 家长无法及时获知子女缺勤,错过干预窗口 |
| **出勤率阈值预警** | 自动识别出勤率低于阈值的学生并通知班主任 | 有 parent-attendance-warning 但仅家长端展示,教师端缺失 | 教师无法主动获取需关注学生名单 |
| **考勤趋势可视化** | 折线图展示个人/班级出勤率周/月趋势 | 仅数字统计,无趋势图 | 无法直观看出勤率变化趋势 |
| **请假申请流程** | 家长在线提交请假申请,教师/管理员审批,自动同步考勤 | 完全缺失 | 请假流程线下化,考勤数据与请假记录脱节 |
| **补签/补录** | 学生事后提交补签申请,教师审核修正 | 缺失,仅管理员/教师可手动修改 | 考勤纠错流程繁琐 |
| **跨日/跨节次考勤** | 按课节(早读/上午/下午/晚自习)多次点名 | 仅按日单次记录 | 无法精确到节次,缺勤定位不精确 |
| **考勤与成绩关联** | 出勤率与学业成绩相关性分析 | 无关联 | 无法识别"低出勤→低成绩"风险学生 |
| **班级对比分析** | 同年级班级出勤率横向对比 | 缺失 | 管理员无法横向评估各班考勤管理水平 |
| **导出报告多样性** | PDF 周报/月报、Excel 明细、家长签字单 | 仅 Excel 单一导出 | 无法满足不同场景报告需求 |
| **移动端适配** | 移动端点名(平板/手机) | 未验证移动端体验 | 教师课堂点名不便携 |
| **考勤日历视图** | 学生/家长端月历视图(已有 parent-attendance-calendar | 已实现且 a11y 良好 | ✅ 已达行业水准 |
| **批量操作效率** | 一键全勤、快捷键、批量按学号录入 | 已实现快捷键 + 一键全勤 | ✅ 已达行业水准 |
| **空状态/骨架屏** | 每个数据区块 EmptyState + Skeleton | 已基本覆盖 | ✅ 基本达标 |
| **a11y 可访问性** | 语义化、ARIA、键盘导航 | 已实现且较完善 | ✅ 基本达标 |
**核心差距总结**
1. **功能完整性**:缺请假流程、节次考勤、原因分类、趋势可视化、跨班级对比——这些是 K12 学校的刚需。
2. **主动通知机制**:被动展示→主动预警的转变。
3. **数据联动**:考勤与成绩、请假、通知模块的联动缺失。
---
## 四、改进优先级建议
### P0紧急影响安全/数据正确性,立即修复)
| # | 问题 | 改进方向 |
|---|------|---------|
| P0-1 | teacher/sheet、teacher/stats 缺权限校验且未传 scope2.2.1 | 页面级增加 `requirePermission(ATTENDANCE_READ)` + 传入 `dataScope`data-access 函数强制要求 scope 参数 |
| P0-2 | teacher 分页基于截断数据计算2.10.2 | 分页 totalPages 使用后端返回的 total勿基于 items 长度计算 |
| P0-3 | safeParseDate 中文 fieldName 硬编码2.3.1 | 改用 i18n key 或错误 code前端按 code 本地化 |
| P0-4 | 翻译键误用error 重试按钮显示"保存"2.3.6 | 3 个 error.tsx 改用 `t("actions.retry")` |
| P0-5 | data-access 跨模块直 JOIN users/classes 表2.1.1 | 委托 `users/data-access``classes/data-access` 提供姓名查询接口 |
### P1重要影响可维护性/合规性,短期修复)
| # | 问题 | 改进方向 |
|---|------|---------|
| P1-1 | teacher/student 主页面缺 requirePermission2.2.2 | 增加 `requirePermission(ATTENDANCE_READ)` |
| P1-2 | parent 直接 import attendance 组件2.1.2 | 通过接口抽象 + Context 注入,或将共享视图下沉为可注入组件 |
| P1-3 | parent-attendance-calendar 依赖 attendance/constants2.1.3 | 常量下沉到 shared 或通过 props 注入 |
| P1-4 | action-utils shared 中文兜底2.3.2 | 返回错误 code 而非中文消息 |
| P1-5 | child-detail-panel 英文硬编码2.3.5 | 提取 i18n 键 |
| P1-6 | export.ts 英文列头 + as 断言2.3.3、2.4.1 | i18n + 类型守卫 |
| P1-7 | calendar 硬编码 en-US2.3.4 | 使用 `useLocale()`/`getLocale()` |
| P1-8 | teacher 子路由缺 error.tsx2.5.1 | 新增 error.tsx |
| P1-9 | 常量重复定义2.6.1 | attendance-sheet 统一使用 constants.ts |
| P1-10 | 4 个 error.tsx 重复2.6.2 | 抽取 shared `ErrorBoundary` 组件 |
| P1-11 | getClassAttendanceStats 未用 SQL 聚合2.10.1 | 改用 SQL GROUP BY 聚合 |
| P1-12 | 无接口抽象/Context 注入2.7.1 | 定义 `AttendanceDataService` 接口 + Provider |
### P2优化提升体验/性能,中期演进)
| # | 问题 | 改进方向 |
|---|------|---------|
| P2-1 | 删除按钮无前端权限控制2.2.3 | `usePermission().hasPermission(ATTENDANCE_MANAGE)` |
| P2-2 | error.tsx title/description 重复2.3.7 | 区分 title/description 键 |
| P2-3 | student 空状态文案错误2.5.2 | 改用 `t("list.emptyDescription")` |
| P2-4 | child-detail-panel as 断言2.4.2 | 使用 isTab 守卫 |
| P2-5 | stats 卡片组件数据结构分裂2.6.3 | 统一为单一数据结构 |
| P2-6 | data-access 无接口类型2.8.1 | 导出 `AttendanceRepository` 接口 |
| P2-7 | 全局 keydown 冲突2.9.1 | 限制监听范围 |
| P2-8 | parent 组件可降级 RSC2.10.3 | 改用 getTranslations |
| P2-9 | statusCounts 未 memoize2.10.4 | useMemo |
| P2-10 | 架构图同步2.1.4 | 更新 004/005 文档 |
### 中长期演进(功能补齐,对齐行业实践)
| # | 功能 | 方向 |
|---|------|------|
| L-1 | 考勤状态细分 + 原因字段 | 扩展 schema 增加 reason 字段,新增 early-leave/school-activity 状态 |
| L-2 | 实时家长通知 | 接入 notifications 模块,缺勤自动推送 |
| L-3 | 出勤率阈值预警(教师端) | 配置驱动阈值,自动生成需关注学生名单 |
| L-4 | 考勤趋势可视化 | 折线图组件,周/月趋势 |
| L-5 | 在线请假流程 | 新增 leave-requests 子模块,审批流 + 考勤同步 |
| L-6 | 节次考勤 | 扩展数据模型支持按节次记录 |
| L-7 | 跨班级对比分析 | 同年级班级出勤率横向对比图 |
| L-8 | 导出报告多样化 | PDF 周报/月报 + 家长签字单 |
| L-9 | 考勤与成绩关联分析 | 跨模块数据联动分析 |
---
## 五、架构图同步说明
本次审计发现架构图004 第 2.10 节、005 JSON L14681 起)存在以下不一致,**需要同步更新**
| # | 架构图描述 | 实际代码 | 更新动作 |
|---|-----------|---------|---------|
| 1 | 004 L1159: `getClassStudentsForAttendance` 仍直查 `classEnrollments` | [data-access.ts:226](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L226) 已委托 `classes/data-access.getClassActiveStudentsWithInfo` | 更新 004 描述为"已委托 classes data-access" |
| 2 | 004 L1152: 10 个 Actions含 5 个读 Action | actions.ts 仅 5 个写 Action | 更新 Actions 计数为 5读操作标注为直接调 data-access |
| 3 | 004 L1154: `getClassAttendanceStats` 改用 SQL 聚合 | [data-access-stats.ts:172-182](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L172-L182) 仍用全量查询 + computeStats | 待本次重构改为 SQL 聚合后同步更新 |
| 4 | 004 L1175: `attendance-sheet.tsx` 使用 `window.confirm` | 已改为 `AlertDialog`L392-415 | 更新为 AlertDialog |
| 5 | 004 L1176: 存在 `{} as Record` 断言 | 已改为 `createInitialStatusCounts()` 函数L75-83 | 更新描述 |
| 6 | 004 L1195: `attendance-stats-cards.tsx` 硬编码中文 | 已全部使用 `t()` i18n | 更新为已 i18n |
**JSON 同步**005 中 attendance 节点的 exports、dependencies、permissions 需在本次重构后统一更新(新增 `AttendanceDataService` 接口、`AttendanceProvider`、抽取的 shared `ErrorBoundary`、新增 error.tsx 等)。
---
## 六、重构方案设计
### 6.1 完全解耦:接口抽象 + Context 注入
**设计目标**attendance 模块作为独立功能单元parent/student 等消费方通过接口契约消费,不直接 import 业务实现。
```typescript
// src/modules/attendance/services/attendance-data-service.ts
// 接口抽象:定义数据契约,可被不同角色实现
export interface AttendanceDataService {
getStudentSummary(studentId: string, range?: DateRange): Promise<AttendanceStats | null>;
getRecentRecords(studentId: string, limit: number): Promise<AttendanceListItem[]>;
getRecordsByDate(date: string, classId?: string): Promise<AttendanceListItem[]>;
getClassStats(classId: string, range?: DateRange): Promise<AttendanceStats | null>;
}
// src/modules/attendance/services/attendance-context.tsx
// React Context 注入
const AttendanceServiceContext = createContext<AttendanceDataService | null>(null);
export function AttendanceProvider({ service, children }: {
service: AttendanceDataService;
children: ReactNode;
}) {
return (
<AttendanceServiceContext.Provider value={service}>
{children}
</AttendanceServiceContext.Provider>
);
}
export function useAttendanceService(): AttendanceDataService {
const service = useContext(AttendanceServiceContext);
if (!service) throw new Error("AttendanceProvider missing");
return service;
}
// 角色实现(示例)
// src/modules/attendance/services/student-service.ts —— 学生/家长视角实现
// src/modules/attendance/services/teacher-service.ts —— 教师视角实现
// src/modules/attendance/services/admin-service.ts —— 管理员视角实现
```
**消费方改造**`parent/attendance/page.tsx` 注入 `StudentAttendanceService` 实现后渲染 `<StudentAttendanceView>`,不再直接 import attendance 组件;或 attendance 导出纯展示组件,由 parent 通过 props 注入数据。
### 6.2 组合优先:组件组合与 hooks
- 所有 UI 通过 `children`/slots/render props 组合,禁止 HOC 深层嵌套。
- 逻辑复用抽取为 hooks`useAttendanceSheet``useAttendanceStats``useAttendanceFilters`
- `AttendancePageLayout` 已采用插槽模式header/stats/filters/children推广至所有角色页面。
### 6.3 国际化就绪
**翻译文件结构示例**
```json
// src/shared/i18n/messages/zh-CN/attendance.json
{
"title": "考勤管理",
"status": { "present": "出勤", "absent": "缺勤", "late": "迟到", "excused": "请假" },
"stats": { "total": "总人次", "presentRate": "出勤率", ... },
"errors": {
"invalidDate": "日期格式无效",
"invalidStartDate": "开始日期格式无效",
"unexpected": "发生未知错误,请稍后重试",
"forbidden": "无权限执行此操作",
"notFound": "考勤记录不存在"
},
"actions": { "save": "保存", "retry": "重试", "delete": "删除", ... }
}
```
**shared 层错误改造**`action-utils.ts` 返回 `{ code: "INVALID_DATE", field: "date" }` 结构化错误,前端按 code 查 i18n key 本地化,消除中文硬编码。
### 6.4 最大化复用
- 抽取 shared `ErrorBoundary` 组件(替代 4 个重复 error.tsx
- 统一 stats 数据结构为单一 `AttendanceStats`
- 常量统一从 `constants.ts` 导出。
- `StudentAttendanceView` 改为接收 `AttendanceDataService` 注入,支持 student/parent 复用。
### 6.5 错误与边界处理
- 每个独立数据区块用 React Error Boundary 包裹。
- 异步数据用 Suspense + 骨架屏。
- 明确处理空数据、无权限、网络异常。
### 6.6 可测试性
- data-access 导出 `AttendanceRepository` 接口类型供 mock。
- 纯逻辑(`computeStats``buildWarnings``aggregateStats` 等)已分离,保持。
### 6.7 可扩展性
- 角色配置驱动:`attendanceRoleConfig[role]` 决定渲染哪些 Widget/子模块。
- 新增角色仅修改配置。
### 6.8 企业级补充
- **a11y**:保持现有 ARIA/键盘导航水平,优化 keydown 监听范围。
- **性能**RSC 获取初始数据,客户端组件最小化,支持流式渲染。
- **安全**data-access 层结合 `dataScope` 过滤Server Action 二次校验。
- **监控**:预留 `trackEvent` 埋点接口(点名/删除/规则保存等关键操作)。

View File

@@ -0,0 +1,436 @@
# 审计模块审计报告 v2
> 审计范围:`src/modules/audit/**`、`src/app/(dashboard)/admin/audit-logs/**`、`src/app/api/export/route.ts`(审计导出分支)、`src/app/api/cron/audit-cleanup/route.ts`、`src/shared/lib/audit-logger.ts`、`src/shared/lib/change-logger.ts`、`src/shared/i18n/messages/{zh-CN,en}/audit.json`
> 审计日期2026-06-25
> 审计依据:`docs/architecture/004_architecture_impact_map.md`2.15 节)、`docs/architecture/005_architecture_data.json`audit 节点)、`docs/standards/coding-standards.md`、项目 `project_rules.md`
> 前置报告:`docs/architecture/audit/audit-module-audit-report.md`v12026-06-24
---
## 一、现有实现概要
### 1.1 文件分布
v1 审计后已完成两轮重构P0-P2当前文件分布如下
| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| 路由 | `app/(dashboard)/admin/audit-logs/page.tsx` | 79 | 操作日志列表页RSC调 data-access |
| 路由 | `app/(dashboard)/admin/audit-logs/login-logs/page.tsx` | 79 | 登录日志列表页 |
| 路由 | `app/(dashboard)/admin/audit-logs/data-changes/page.tsx` | 83 | 数据变更日志列表页 |
| 路由 | `app/(dashboard)/admin/audit-logs/overview/page.tsx` | 34 | 审计概览仪表盘(✅ v1 P2-4 新增) |
| 路由 | 4 个 `loading.tsx` + 4 个 `error.tsx` | — | 骨架屏 + 错误边界(✅ v1 P0-2 已修复) |
| 路由 | `app/api/export/route.ts` | 202 | 统一导出 API含 audit/login/dataChange 三分支) |
| 路由 | `app/api/cron/audit-cleanup/route.ts` | 75 | 定时清理 cron✅ v1 P2-5 新增) |
| actions | `modules/audit/actions.ts` | 195 | 7 个 Server Action1 查询 + 3 导出 + 3 保留策略) |
| data-access | `modules/audit/data-access.ts` | 387 | 12 个查询函数(分页 + 导出 + 选项 + 统计 + 趋势) |
| export | `modules/audit/export.ts` | 133 | Excel 列定义 + 行映射 + buildExport✅ v1 P1-3 已抽取) |
| retention | `modules/audit/retention.ts` | 123 | 保留策略配置读写 + 清理 + 纯函数(✅ v1 P2-5 新增) |
| types | `modules/audit/types.ts` | 145 | 类型定义 + 状态映射常量 |
| services | `modules/audit/services/audit-service.tsx` | 124 | AuditService 接口 + Context + hook✅ v1 P2-8 新增) |
| services | `modules/audit/services/admin-audit-service.ts` | 40 | 管理员默认实现 |
| services | `modules/audit/services/mock-audit-service.ts` | 57 | 测试用 Mock 实现 |
| hooks | `modules/audit/hooks/use-log-pagination.ts` | 24 | 分页 URL 状态 hook✅ v1 P1-2 已抽取) |
| 组件 | 15 个组件文件 | 19-191 | 表格/筛选器/视图/详情对话框/概览/图表/保留配置/骨架屏/错误边界 |
| 测试 | `export.test.ts` / `retention.test.ts` | 191/81 | 纯函数单测(✅ v1 P2-6 已补充) |
| i18n | `shared/i18n/messages/{zh-CN,en}/audit.json` | 130 | 完整翻译键table/filter/empty/export/error/detail/overview/retention |
| shared | `shared/lib/audit-logger.ts` / `change-logger.ts` | — | `logAudit()` / `logDataChange()` 写入日志 |
### 1.2 数据流
```
page.tsx (RSC)
├─ requirePermission(AUDIT_LOG_READ)
├─ getAuditLogs/getLoginLogs/getDataChangeLogs (data-access 直调)
└─ <AuditErrorBoundary>
<AuditLogView items={...} /> (Client Component)
├─ <AuditLogFilters> (nuqs URL 状态)
└─ <AuditLogTable> (分页 + 空状态 + 详情对话框)
overview/page.tsx (RSC)
├─ requirePermission(AUDIT_LOG_READ)
├─ getAuditOverviewStats / getAuditTrend / getDataChangeActionStats
└─ <AuditOverviewView> (RSC)
├─ <AuditOverviewStatsBar>
├─ <AuditActivityTrendChart>
├─ <DataChangeDistributionChart>
└─ <AuditRetentionSettings> (Client Component直调 Server Actions)
```
导出流:`<AuditLogExportButton>``fetch /api/export``exportAuditLogsAction``getAuditLogsForExport``buildAuditLogExport` → Excel buffer。
定时清理流:`/api/cron/audit-cleanup`CRON_SECRET 鉴权)→ `getAuditRetentionConfig``purgeExpiredAuditLogs`
### 1.3 架构图记录情况
- **0042.15 节)**:记录较完整,含 services/hooks/export/retention 节点,行数标注准确。
- **005audit 节点)**data-access/actions/types 记录完整,含 services/hooks/export/retention 节点。
- **已知不一致**services 层的 `AuditServiceProvider` 在架构图中标注为"已接入",但实际**未被任何页面使用**(详见 P0-1
---
## 二、现存问题与原因分析
### P0 — 严重违规
#### P0-1 Service 抽象层定义但从未使用(虚假完成)
- **位置**`services/audit-service.tsx``AuditServiceProvider``useAuditService``useAuditAnalytics`)、`services/admin-audit-service.ts``services/mock-audit-service.ts`
- **违反规则**:任务要求 → "完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context或组合 Provider注入数据服务模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"`project_rules.md` → "架构图优先规则:改码必同步图"
- **问题**
1. 全局搜索 `AuditServiceProvider` 的实际使用:**仅在 `audit-service.tsx``mock-audit-service.ts` 的 JSDoc `@example` 中出现**,没有任何页面或组件实际注入
2. 4 个 `page.tsx` 均直接 `import { getAuditLogs, ... } from "@/modules/audit/data-access"`
3. `audit-retention-settings.tsx` 直接 `import { getAuditRetentionConfigAction, ... } from "@/modules/audit/actions"`
4. `audit-log-export-button.tsx` 直接 `fetch("/api/export")` 绕过 Service 层
5. 架构图 004 第 1468 行声称"✅ P2-8 已修复:~~无 Service 接口抽象~~",实际为"定义但未接线"
- **原因**P2-8 仅创建了接口文件,未在页面层注入 Provider、未将组件改造为从 Context 获取 service
- **后果**
- DI 架构形同虚设,组件仍硬编码依赖 data-access无法在测试中注入 mock
- 架构图声称已修复,误导后续审计与维护决策
- `mockAuditService` 完全无引用(死代码)
#### P0-2 mock-audit-service.ts 使用 9 处 `as` 类型断言
- **位置**`services/mock-audit-service.ts` 第 29、31、33、36、45、46、47、53、60 行
- **违反规则**`project_rules.md` → "禁止 `as` 断言(除类型收窄外)"`project_memory.md` → "TypeScript strict mode: no `any`, no `as` assertions (except for type narrowing)"
- **示例**
```typescript
getAuditLogs: async () =>
({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult<AuditLog>,
```
- **原因**:对象字面量缺少 `items: [] as AuditLog[]` 类型标注,导致需要 `as` 断言整个返回值
- **后果**:违反 TS 严格规则lint 通过但 code review 应拦截
#### P0-3 全部 7 个 Server Action 缺少 revalidatePath
- **位置**`actions.ts` 所有 7 个 Action`getDataChangeLogsAction`、`exportAuditLogsAction`、`exportLoginLogsAction`、`exportDataChangeLogsAction`、`getAuditRetentionConfigAction`、`saveAuditRetentionConfigAction`、`purgeAuditLogsAction`
- **违反规则**`project_rules.md` → "Server Action 规范:使用 `revalidatePath` 精确刷新缓存"
- **问题**`saveAuditRetentionConfigAction` 和 `purgeAuditLogsAction` 修改数据后未刷新 overview 页面缓存,用户保存保留策略后看到的仍是旧配置
- **原因**:遗漏
- **后果**:保留策略变更后概览页缓存不刷新,显示过期数据
### P1 — 重要缺陷
#### P1-1 trackEvent event 名称误用(保留策略操作误标为导出)
- **位置**`actions.ts` 第 170、199 行(`saveAuditRetentionConfigAction`、`purgeAuditLogsAction`)、`app/api/cron/audit-cleanup/route.ts` 第 54 行
- **违反规则**:任务要求 → "监控:方案中预留关键操作埋点接口"
- **问题**:保留策略的保存/清理操作使用 `event: "audit.exported"`,语义错误
```typescript
// saveAuditRetentionConfigAction
await trackEvent({ event: "audit.exported", ... properties: { action: "save_config" } })
// purgeAuditLogsAction
void trackEvent({ event: "audit.exported", ... properties: { action: "purge" } })
// cron route
void trackEvent({ event: "audit.exported", ... properties: { action: "cron_purge" } })
```
- **后果**:分析平台中所有保留策略操作被归类为"导出",无法区分导出与清理行为
#### P1-2 AuditErrorBoundary 使用错误的 i18n namespace
- **位置**`components/audit-error-boundary.tsx` 第 19 行 `namespace="common"`
- **违反规则**`project_rules.md` → "所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"
- **问题**`SectionErrorBoundary` 读取 `t("error.boundaryTitle")` / `t("error.boundaryDescription")` / `t("error.retry")`。`audit.json` 定义了 `error.title` / `error.description` / `error.retry`(键名不匹配),而 `common.json` 有 `error.boundaryTitle` / `error.boundaryDescription`。当前传 `"common"` 可工作但使用的是通用错误文案,未利用 audit 专属的 `error.description`"数据加载时发生错误,请稍后重试。"
- **后果**:区块级错误边界显示通用错误文案而非审计模块专属文案
#### P1-3 AuditAnalytics 接口定义为死代码
- **位置**`services/audit-service.tsx` 第 64-84 行(`AuditAnalytics` 接口 + `noopAnalytics` + `AuditAnalyticsContext` + `useAuditAnalytics` hook
- **违反规则**:任务要求 → "监控:方案中预留关键操作埋点接口"
- **问题**`useAuditAnalytics` hook 全局搜索仅出现在定义处,**无任何组件调用**。`trackLogView` / `trackExport` / `trackOverviewView` / `trackRetentionConfigChange` / `trackPurge` 五个埋点方法均为死代码
- **后果**:客户端层面无日志查看/导出/概览查看埋点,无法统计用户行为
#### P1-4 data-access 错误处理不一致
- **位置**`data-access.ts`
- **吞没错误**(返回空数组):`getAuditModuleOptions`(第 152 行)、`getDataChangeTableOptions`(第 237 行)
- **抛出错误**`getAuditLogs`、`getLoginLogs`、`getDataChangeLogs`、`getDataChangeStats`、`getAuditOverviewStats`、`getAuditTrend`、`getDataChangeActionStats`
- **违反规则**`project_rules.md` → "错误与边界处理:明确处理空数据、无权限、网络异常等边界状态"
- **后果**DB 故障时筛选器选项静默显示为空,用户误认为"无数据"而非"查询失败"
#### P1-5 日期筛选器 aria-label 不区分起止
- **位置**`audit-log-filters.tsx` 第 94、101 行;`login-log-filters.tsx` 第 78、85 行;`data-change-log-filters.tsx` 第 117、124 行
- **违反规则**`project_rules.md` → "可访问性a11y语义化标签、ARIA 属性、键盘导航"
- **问题**:两个日期 Input 均使用 `aria-label={t("table.time")}`"时间"),屏幕阅读器无法区分"开始日期"与"结束日期"
- **后果**:视障用户无法区分两个日期输入框
#### P1-6 AuditLogDetailDialog DialogDescription 重复标题
- **位置**`components/audit-log-detail-dialog.tsx` 第 135-137 行
- **问题**
```tsx
<DialogTitle>{t("detail.title")}</DialogTitle>
<DialogDescription className="sr-only">{t("detail.title")}</DialogDescription>
```
Description 与 Title 完全相同,无信息增量
- **后果**:无障碍辅助技术读出重复信息
#### P1-7 骨架屏缺少 aria-hidden
- **位置**`components/audit-log-table-skeleton.tsx`全文件、4 个 `loading.tsx`
- **违反规则**`project_rules.md` → "可访问性a11y"
- **问题**Skeleton 元素无 `aria-hidden="true"`,屏幕阅读器会逐个朗读骨架占位块
- **后果**:加载期间屏幕阅读器体验差
#### P1-8 window.confirm 阻塞式确认
- **位置**`components/audit-retention-settings.tsx` 第 80 行
- **问题**`window.confirm(t("purgeConfirm"))` 使用浏览器原生确认框不可自定义样式、阻塞主线程、a11y 差
- **后果**:与其他模块(使用 AlertDialog 组件)交互不一致
### P2 — 改进项
#### P2-1 数据变更 diff 无可视化
- **位置**`data-change-log-table.tsx` 第 118-129 行使用 `<pre>` 纯文本展示 oldValue / newValue
- **差距**PowerSchool / Google Workspace Audit / Microsoft Purview 均提供 JSON diff 高亮(增行绿、删行红、左右对比)
#### P2-2 无失败登录异常告警
- **位置**:全模块
- **差距**:行业产品支持阈值告警(如 5 分钟内同一 IP 失败登录 > 10 次触发告警)
#### P2-3 无 IP 地理位置
- **位置**:表格仅展示 IP 字符串
- **差距**:行业产品将 IP 解析为地理位置(城市/国家),辅助判断异地登录
#### P2-4 概览趋势仅 7 天不可配置
- **位置**`overview/page.tsx` 第 29 行 `getAuditTrend(7)` 硬编码 7 天
- **差距**:行业产品支持 7/30/90 天切换
#### P2-5 多学校数据隔离缺失
- **位置**`data-access.ts` 所有查询无 `schoolId` 过滤
- **说明**`audit_logs` / `login_logs` / `data_change_logs` 表无 `school_id` 字段schema 确认audit 模块设计为系统级跨校审计。当前 `AUDIT_LOG_READ` 权限若仅授予超级管理员则可接受,但需在文档中明确标注此设计决策
- **风险**:若未来授予校级管理员 `AUDIT_LOG_READ`,将导致跨校数据泄露
#### P2-6 useLogPagination hook 无单测
- **位置**`hooks/use-log-pagination.ts`
- **违反规则**`project_rules.md` → "可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks"
- **说明**v1 已补充 export.test.ts 和 retention.test.ts但 hook 层无单测
#### P2-7 导出按钮无进度反馈
- **位置**`audit-log-export-button.tsx`
- **问题**:大范围导出仅显示 spinner无进度条/计数
- **差距**:行业产品显示"已导出 X / Y 条"
---
## 三、行业差距对比
| 能力 | PowerSchool / Veracross | Google Workspace Audit | Microsoft Purview | 本系统现状 | 影响 |
|------|------------------------|----------------------|-------------------|-----------|------|
| 统一审计概览仪表盘 | ✅ | ✅ | ✅ | ✅ 已有概览页 | — |
| 按用户/模块/动作/状态筛选 | ✅ | ✅ | ✅ | ✅ 已有完整筛选器 | — |
| 日志详情视图 | ✅ 点击展开完整 JSON | ✅ 详情面板 | ✅ 活动详情 | ✅ 已有详情对话框 | — |
| 数据变更 diff 可视化 | ✅ 左右对比 + 高亮 | ✅ | ✅ 差异高亮 | ❌ 纯文本 pre 展示 | 变更审查效率低 |
| 失败登录监控/告警 | ✅ 异常登录告警 | ✅ 可疑活动检测 | ✅ 实时告警 | ❌ 仅展示无告警 | 无法及时发现暴力破解 |
| 导出调度/定时 | ✅ 计划报告 | ✅ 导出 + 邮件 | ✅ 合规报告 | ❌ 仅手动导出 | 合规审计需人工操作 |
| 数据保留策略 | ✅ 可配置保留期 | ✅ | ✅ | ✅ 已有保留策略配置 | — |
| IP 地理位置 | ✅ | ✅ | ✅ | ❌ 仅显示 IP | 无法判断异地登录 |
| 多角色审计视图 | ✅ 管理员/合规官分级 | ✅ | ✅ | ⚠️ 仅 admin 单角色 + Service 接口预留 | 合规官角色需独立视图 |
| i18n | ✅ 多语言 | ✅ | ✅ | ✅ 已完整 i18n | — |
| 概览趋势可配置时间范围 | ✅ 7/30/90 天 | ✅ | ✅ | ❌ 硬编码 7 天 | 无法查看长期趋势 |
| DI 架构(可测试) | — | — | — | ⚠️ 定义未接线 | 组件无法 mock 数据源 |
---
## 四、改进优先级建议
### P0 — 立即修复(合规与架构正确性)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | Service 抽象层未接线 | overview 页面接入 `AuditServiceProvider``AuditOverviewView` 内组件改用 `useAuditService()` 获取数据;或若评估后认为 RSC + props 模式已满足需求则**删除未使用的 services 层**避免误导 |
| P0-2 | mock-audit-service.ts 9 处 as 断言 | 为对象字面量添加元素类型标注(如 `items: [] as AuditLog[]` → 改用 `items: new Array<AuditLog>()` 或显式标注返回类型) |
| P0-3 | 7 个 Action 缺 revalidatePath | 写操作saveAuditRetentionConfigAction、purgeAuditLogsAction添加 `revalidatePath("/admin/audit-logs/overview")` |
### P1 — 本轮实施(规范与体验)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | trackEvent event 误标 | 保留策略操作改用 `event: "audit.retention"` |
| P1-2 | ErrorBoundary namespace 错误 | `audit.json` 新增 `error.boundaryTitle` / `error.boundaryDescription``AuditErrorBoundary` 传 `namespace="audit"` |
| P1-3 | AuditAnalytics 死代码 | 组件中调用 `useAuditAnalytics()` 接入客户端埋点(日志查看、概览查看、导出、保留策略变更) |
| P1-4 | data-access 错误处理不一致 | `getAuditModuleOptions` / `getDataChangeTableOptions` 改为抛出错误 |
| P1-5 | 日期 aria-label 不区分 | 新增 i18n `filter.startDate` / `filter.endDate`,替换 `t("table.time")` |
| P1-6 | DialogDescription 重复 | 新增 `detail.description` 翻译键 |
| P1-7 | 骨架屏缺 aria-hidden | Skeleton 容器添加 `aria-hidden="true"` |
| P1-8 | window.confirm | 替换为 AlertDialog 组件(与其他模块一致) |
### P2 — 中长期迭代(功能增强)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | diff 无可视化 | 引入 JSON diff 组件(左右对比 + 增删高亮) |
| P2-2 | 无失败登录告警 | 新增异常检测规则 + 告警通知 |
| P2-3 | 无 IP 地理位置 | 接入 IP 反查服务(如 MaxMind GeoLite2 |
| P2-4 | 趋势不可配置 | 概览页新增 7/30/90 天切换按钮 |
| P2-5 | 多校数据隔离 | 文档标注"系统级审计"设计决策;若需校级审计则新增 `school_id` 字段 |
| P2-6 | hook 无单测 | 为 `useLogPagination` 补充单测 |
| P2-7 | 导出无进度 | 大范围导出显示进度条 |
---
## 五、架构图同步说明
本次审计发现架构图以下遗漏/不一致,需在实现后同步更新:
### 004_architecture_impact_map.md2.15 节)
1. **修正 services 层状态**:第 1468 行"✅ P2-8 已修复"改为"P2-8 已定义接口P0-1 待接线"(实施后改回"已接线"
2. **修正 trackEvent 埋点描述**:保留策略操作的 event 名称从 `audit.exported` 改为 `audit.retention`
3. **新增 revalidatePath 说明**actions 节标注 saveAuditRetentionConfigAction / purgeAuditLogsAction 已添加 revalidatePath
### 005_architecture_data.jsonaudit 节点)
1. **services 节点**:标注 `AuditServiceProvider` 的 `usedBy`(实施后从空改为 overview page
2. **actions 节点**:更新 saveAuditRetentionConfigAction / purgeAuditLogsAction 的 trackEvent event 名称
3. **components 节点**:更新 `audit-error-boundary.tsx` 的 namespace 配置
---
## 六、重构方案设计
### a. P0-1 Service 抽象层接线方案
**决策**:保留 Service 接口(满足任务要求的 DI 架构),在 overview 页面接线。
由于 `AuditOverviewView` 是 RSCasync server component而 `AuditServiceProvider` 是 Client Component"use client"),无法直接在 RSC 中使用 Context Provider。因此采用**混合模式**
1. **RSC 页面**:仍由 page.tsx 调用 data-access 获取初始数据(保留 SSR 性能优势)
2. **客户端交互组件**`AuditRetentionSettings`):通过 `AuditServiceProvider` 注入 service组件内部用 `useAuditService()` 获取数据
3. **`adminAuditService`** 改为可被 Client Component 引用的轻量包装(仅委托给 Server Action不直接 import server-only 的 data-access
```typescript
// services/admin-audit-service.ts —— 改造为 Client-safe 实现
// 不 import "server-only",改为委托 Server Actions
import { getAuditRetentionConfigAction, saveAuditRetentionConfigAction, purgeAuditLogsAction } from "../actions"
export const adminAuditService: AuditService = {
// 保留策略相关:通过 Server Action 调用
getAuditRetentionConfig: async () => {
const res = await getAuditRetentionConfigAction()
if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
return res.data
},
saveAuditRetentionConfig: async (config) => {
const res = await saveAuditRetentionConfigAction(config)
if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
},
purgeExpiredAuditLogs: async (retentionDays) => {
const res = await purgeAuditLogsAction(retentionDays)
if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
return res.data
},
// 查询类RSC 页面已通过 props 传入,客户端不需要
// ...其余方法可暂不实现或抛错
}
```
`AuditRetentionSettings` 改造:
```tsx
// 在组件内部使用 useAuditService() 而非直接调 Server Action
const service = useAuditService()
const res = await service.getAuditRetentionConfig()
```
页面注入:
```tsx
// overview/page.tsx
import { AuditServiceProvider } from "@/modules/audit/services/audit-service"
import { adminAuditService } from "@/modules/audit/services/admin-audit-service"
return (
<AuditServiceProvider service={adminAuditService}>
<AuditOverviewView stats={stats} trend={trend} distribution={distribution} />
</AuditServiceProvider>
)
```
### b. P0-2 mock-audit-service 类型标注修复
```typescript
// 修复前
getAuditLogs: async () =>
({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult<AuditLog>,
// 修复后 —— 为返回值添加显式类型标注,无需 as
getAuditLogs: async (): Promise<PaginatedResult<AuditLog>> => ({
items: [],
total: 0,
page: 1,
pageSize: 20,
totalPages: 0,
}),
```
### c. P0-3 revalidatePath
```typescript
import { revalidatePath } from "next/cache"
// saveAuditRetentionConfigAction 末尾
revalidatePath("/admin/audit-logs/overview")
return { success: true, data: config }
// purgeAuditLogsAction 末尾
revalidatePath("/admin/audit-logs/overview")
revalidatePath("/admin/audit-logs")
return { success: true, data: result }
```
### d. P1-1 trackEvent event 修正
```typescript
// 保留策略操作
await trackEvent({
event: "audit.retention", // 原: "audit.exported"
userId: session?.user?.id,
targetType: "audit_retention",
properties: { action: "save_config", ... },
})
```
### e. P1-2 ErrorBoundary namespace 修复
`audit.json` 新增:
```json
"error": {
"title": "加载失败",
"description": "数据加载时发生错误,请稍后重试。",
"retry": "重试",
"boundaryTitle": "审计数据加载失败",
"boundaryDescription": "审计数据区块加载时发生错误。"
}
```
`audit-error-boundary.tsx`
```tsx
<SectionErrorBoundary namespace="audit">
```
### f. i18n 翻译键补充
```json
"filter": {
"startDate": "开始日期",
"endDate": "结束日期"
},
"detail": {
"description": "查看日志的完整字段信息。"
}
```
### g. 最终检查
- [x] 该模块不存在对其他业务模块的直接 import仅依赖 shared/* 和 settings data-access for retention config
- [x] 没有使用 `any` 或硬编码角色字符串
- [x] 所有 actions 包含 `requirePermission` 调用7 个 Action 均有)
- [x] 文件行数未超过建议上限(最大 data-access.ts 387 行 < 800
- [x] 架构影响地图需同步更新(见第五节)

View File

@@ -0,0 +1,436 @@
# 审计模块审计报告
> 审计范围:`src/modules/audit/**`、`src/app/(dashboard)/admin/audit-logs/**`、`src/app/api/export/route.ts`(审计导出分支)、`src/shared/lib/audit-logger.ts`、`src/shared/lib/change-logger.ts`、`src/shared/i18n/messages/{zh-CN,en}/audit.json`
> 审计日期2026-06-24
> 审计依据:`docs/architecture/004_architecture_impact_map.md`2.15 节)、`docs/architecture/005_architecture_data.json`audit 节点)、`docs/standards/coding-standards.md`、项目 `project_rules.md`
---
## 一、现有实现概要
### 1.1 文件分布
| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| 路由 | `app/(dashboard)/admin/audit-logs/page.tsx` | 74 | 操作日志列表页RSC调 data-access |
| 路由 | `app/(dashboard)/admin/audit-logs/login-logs/page.tsx` | 74 | 登录日志列表页 |
| 路由 | `app/(dashboard)/admin/audit-logs/data-changes/page.tsx` | 78 | 数据变更日志列表页 |
| 路由 | `app/api/export/route.ts` | 201 | 统一导出 API含 audit/login/dataChange 三分支) |
| actions | `modules/audit/actions.ts` | 214 | 4 个 Server Action1 查询 + 3 导出) |
| data-access | `modules/audit/data-access.ts` | 290 | 9 个查询函数(分页查询 + 导出遍历 + 选项 + 统计) |
| types | `modules/audit/types.ts` | 117 | 类型定义 + 状态映射常量 |
| 组件 | `components/audit-log-view.tsx` | 61 | 操作日志视图(筛选+表格+分页) |
| 组件 | `components/audit-log-table.tsx` | 110 | 操作日志表格 |
| 组件 | `components/audit-log-filters.tsx` | 92 | 操作日志筛选器 |
| 组件 | `components/audit-log-export-button.tsx` | 83 | 导出按钮fetch /api/export |
| 组件 | `components/login-log-view.tsx` | 59 | 登录日志视图 |
| 组件 | `components/login-log-table.tsx` | 104 | 登录日志表格 |
| 组件 | `components/login-log-filters.tsx` | 77 | 登录日志筛选器 |
| 组件 | `components/data-change-log-table.tsx` | 281 | 数据变更表格+筛选器+展开行(混合) |
| shared | `shared/lib/audit-logger.ts` | 46 | `logAudit()` 写入审计日志 |
| shared | `shared/lib/change-logger.ts` | 43 | `logDataChange()` 写入变更日志 |
| i18n | `shared/i18n/messages/{zh-CN,en}/audit.json` | 12 | 仅 3 个标题/描述键 |
### 1.2 数据流
```
page.tsx (RSC)
├─ requirePermission(AUDIT_LOG_READ)
├─ getAuditLogs/getLoginLogs/getDataChangeLogs (data-access)
└─ <AuditLogView items={...} /> (Client Component)
├─ <AuditLogFilters> (nuqs URL 状态)
└─ <AuditLogTable> (分页 + 空状态)
```
导出流:`<AuditLogExportButton>``fetch /api/export``exportAuditLogsAction``getAuditLogsForExport` → Excel buffer。
### 1.3 架构图记录情况
- **0042.15 节)**:记录了 audit 模块,但存在**不一致**:声称 actions 层有 `getAuditLogsAction` / `getLoginLogsAction`,实际**不存在**这两个 Action页面直接调 data-access。组件清单不完整`data-change-log-table.tsx``login-log-view.tsx``audit-log-export-button.tsx`)。
- **005audit 节点)**data-access/actions/types 记录较完整,但 actions 的 `usedBy` 标注为"待扩展"(实际已被 `/api/export/route.ts` 使用)。
---
## 二、现存问题与原因分析
### P0 — 严重违规
#### P0-1 i18n 严重缺失:组件全部硬编码英文文案
- **位置**`audit-log-table.tsx`"User/Module/Action/Target/Status/IP Address/Time/No audit logs found.")、`audit-log-filters.tsx`"Module/Any Module/Action.../Status/Any Status/Success/Failure")、`login-log-table.tsx``login-log-filters.tsx``data-change-log-table.tsx`"Table/Record ID/Changed By/View/Hide/Old Value/New Value/Any Table/Create/Update/Delete/Reset/No data change logs found.")、`audit-log-export-button.tsx`"Export Excel/Export failed/Export ready"
- **违反规则**`project_rules.md` → "所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"
- **原因**i18n 字典 `audit.json` 仅含 3 个标题/描述键,组件层未接入 `useTranslations`
- **后果**:中文用户在审计页面看到全英文表格表头、筛选器、空状态、按钮文案,与系统其他模块(已 i18n体验割裂无法切换语言
#### P0-2 缺少 loading.tsx 与 error.tsx 错误边界
- **位置**`app/(dashboard)/admin/audit-logs/``login-logs/``data-changes/` 三个路由均无 `loading.tsx``error.tsx`
- **违反规则**`project_memory.md` → "All student routes must include loading.tsx and error.tsx for error boundaries"`project_rules.md` → "每个独立的数据区块必须用 React Error Boundary 包裹"
- **原因**:审计路由作为后加模块未补齐边界文件
- **后果**:数据加载期间白屏;运行时错误直接显示 Next.js 默认错误页,无重试能力
#### P0-3 架构图与代码不一致
- **位置**`004_architecture_impact_map.md` 2.15 节
- **违反规则**`project_rules.md` → "改码必同步图"、"架构图优先规则"
- **问题**004 声称存在 `getAuditLogsAction` / `getLoginLogsAction`,实际不存在;组件清单缺 3 个文件行数标注过期actions 标 212 实际 214data-access 标 260 实际 290
- **后果**:权限审计、依赖分析会得出错误结论
### P1 — 重要缺陷
#### P1-1 formatDate 硬编码 locale 为 "zh-CN"
- **位置**`audit-log-table.tsx:92``login-log-table.tsx:86``data-change-log-table.tsx:116`
- **违反规则**i18n 就绪要求
- **原因**:直接传 `"zh-CN"` 而非从 next-intl 获取当前 locale
- **后果**:英文用户看到中文格式日期
#### P1-2 分页 handlePageChange 三处重复
- **位置**`audit-log-view.tsx:29-38``login-log-view.tsx:27-36``data-change-log-table.tsx:58-67`
- **违反规则**`project_rules.md` → "最大化复用"
- **原因**:三处 100% 相同的 URL searchParams 操作逻辑未抽取
- **后果**:维护需改三处,易遗漏
#### P1-3 导出逻辑内联在 actions 层,三个导出 Action 结构高度重复
- **位置**`actions.ts:90-214``exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction`
- **违反规则**004 已标记为 P2 待修复;`project_rules.md` → 单文件职责清晰
- **原因**:列定义 + 行映射 + buildExcelExport 全内联在 actions
- **后果**:新增日志类型需复制整段;列定义无法在组件层复用(如详情视图)
#### P1-4 DataChangeLogTable 混合三职责281 行)
- **位置**`data-change-log-table.tsx`
- **问题**:表格组件 + 筛选器组件(`DataChangeLogFilters`+ 展开行逻辑全部定义在同一文件
- **违反规则**`project_rules.md` → "组件必须为纯函数,职责单一"
- **后果**:筛选器无法独立复用;文件接近 300 行不易维护
#### P1-5 无 React Error Boundary 包裹独立数据区块
- **位置**:三个 `page.tsx` 均直接渲染 `<AuditLogView>` / `<DataChangeLogTable>` 无 ErrorBoundary
- **违反规则**`project_rules.md` → "每个独立的数据区块必须用 React Error Boundary 包裹"
- **后果**:单个区块错误导致整页崩溃
#### P1-6 Suspense fallback 为 null无骨架屏
- **位置**`audit-log-view.tsx:57``login-log-view.tsx:55``data-change-log-table.tsx:277`
- **违反规则**`project_rules.md` → "异步数据使用 React Suspense + 骨架屏"
- **后果**:筛选切换时无加载反馈
#### P1-7 无关键操作埋点
- **位置**:全模块
- **违反规则**`project_rules.md` → "监控:方案中预留关键操作埋点接口"
- **原因**:导出操作、日志查看无 `trackEvent` 调用
- **后果**:无法统计审计功能使用情况、无法监控异常导出行为
#### P1-8 a11y 缺失
- **位置**:表格无 `aria-label`/`caption`;筛选 Select 无 `aria-label`;展开按钮无 `aria-expanded`;导出按钮无 `aria-label`
- **违反规则**`project_rules.md` → "可访问性a11y语义化标签、ARIA 属性、键盘导航"
### P2 — 改进项
#### P2-1 data-access 错误吞没UI 无法区分"空数据"与"查询失败"
- **位置**`data-access.ts` 所有函数 catch 块返回空数组
- **后果**DB 故障时用户看到"无日志"而非错误提示
#### P2-2 无日志详情视图
- **位置**:审计日志表格仅展示摘要,`detail` 字段JSON无法查看
- **差距**:行业标配支持点击行展开/弹窗查看完整日志详情
#### P2-3 无用户维度筛选
- **位置**`audit-log-filters.tsx` 仅支持 module/action/status/date不支持按用户搜索
- **差距**PowerSchool/Veracross 支持按用户筛选所有日志
#### P2-4 无审计概览仪表盘
- **位置**:无统计概览页
- **差距**:行业产品提供"今日事件数/失败登录数/数据变更数"概览卡片 + 活动趋势图
#### P2-5 无数据保留策略
- **位置**:审计日志无 TTL/归档机制
- **差距**:企业级产品支持可配置保留期(如 90/180/365 天)
#### P2-6 无单测
- **位置**:无 `*.test.ts` 文件
- **违反规则**`project_rules.md` → "可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks"
#### P2-7 api/export/route.ts 中 `as Record<string, string>` 类型断言
- **位置**`route.ts:145`
- **违反规则**`project_rules.md` → "禁止 `as` 断言(除类型收窄外)"
#### P2-8 无 Service 接口抽象 / 依赖注入
- **位置**:页面直接调 data-access组件直接收 props
- **违反规则**:任务要求 → "完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务"
- **说明**:当前 RSC + props 模式可工作,但不满足任务要求的 DI 架构,且无法在客户端组件中 mock 数据源
---
## 三、行业差距对比
| 能力 | PowerSchool / Veracross | Google Workspace Audit | Microsoft Purview | 本系统现状 | 影响 |
|------|------------------------|----------------------|-------------------|-----------|------|
| 统一审计概览仪表盘 | ✅ 今日/本周事件数 + 趋势图 | ✅ 活动时间线 | ✅ 合规概览 | ❌ 无 | 管理员无法快速掌握系统活动全貌 |
| 按用户筛选 | ✅ | ✅ | ✅ | ❌ 仅 module/action/status | 无法追踪特定用户操作轨迹 |
| 日志详情视图 | ✅ 点击展开完整 JSON | ✅ 详情面板 | ✅ 活动详情 | ❌ 仅表格摘要 | 审计人员无法查看 detail 字段 |
| 数据变更 diff 可视化 | ✅ 左右对比 + 高亮 | ✅ | ✅ 差异高亮 | ⚠️ 纯文本 pre 展示 | 变更审查效率低 |
| 失败登录监控/告警 | ✅ 异常登录告警 | ✅ 可疑活动检测 | ✅ 实时告警 | ❌ 无 | 无法及时发现暴力破解 |
| 导出调度/定时 | ✅ 计划报告 | ✅ 导出 + 邮件 | ✅ 合规报告 | ❌ 仅手动导出 | 合规审计需人工操作 |
| 数据保留策略 | ✅ 可配置保留期 | ✅ | ✅ | ❌ 无限增长 | 存储成本持续上升 |
| IP 地理位置 | ✅ | ✅ | ✅ | ❌ 仅显示 IP | 无法判断异地登录 |
| 多角色审计视图 | ✅ 管理员/合规官分级 | ✅ | ✅ | ⚠️ 仅 admin 单角色 | 未来合规官角色需独立视图 |
| i18n | ✅ 多语言 | ✅ | ✅ | ❌ 全英文硬编码 | 中文用户体验差 |
---
## 四、改进优先级建议
### P0 — 立即修复(合规与基础体验)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | i18n 全缺失 | 补全 `audit.json` 翻译键(表头/筛选器/空状态/按钮/Toast所有组件接入 `useTranslations("audit")` |
| P0-2 | 缺 loading/error.tsx | 三个路由各新增 `loading.tsx`(骨架屏)+ `error.tsx`(错误边界 + 重试) |
| P0-3 | 架构图不一致 | 同步 004/005修正 actions 清单、补全组件清单、更新行数 |
### P1 — 本轮实施(架构规范)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | formatDate 硬编码 locale | 改用 `useLocale()` 获取当前 locale |
| P1-2 | handlePageChange 重复 | 抽取 `useLogPagination` hook 到 `hooks/` |
| P1-3 | 导出逻辑内联 | 抽取 `export.ts`,列定义+行映射移至独立文件 |
| P1-4 | DataChangeLogTable 混合 | 拆分为 `data-change-log-table.tsx` + `data-change-log-filters.tsx` |
| P1-5 | 无 Error Boundary | 新增 `audit-error-boundary.tsx`,包裹各数据区块 |
| P1-6 | Suspense 无骨架屏 | fallback 改为 `AuditLogTableSkeleton` |
| P1-7 | 无埋点 | 导出/查看操作新增 `trackEvent` |
| P1-8 | a11y 缺失 | 表格加 caption/aria-labelSelect 加 aria-label展开按钮加 aria-expanded |
### P2 — 中长期迭代(功能增强)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | 错误吞没 | data-access 抛出业务错误actions 层捕获返回 ActionState.error |
| P2-2 | 无详情视图 | 新增 `audit-log-detail-dialog.tsx`,展示完整 detail JSON |
| P2-3 | 无用户筛选 | 筛选器新增用户搜索 Input |
| P2-4 | 无概览仪表盘 | 新增 `/admin/audit-logs/overview` 概览页(统计卡片+趋势图) |
| P2-5 | 无保留策略 | 新增 `audit-retention-config` + 定时清理 job |
| P2-6 | 无单测 | 为纯函数(分页计算、格式化、列映射)添加单测 |
| P2-7 | as 断言 | 改用类型守卫 |
| P2-8 | 无 Service 抽象 | 定义 `AuditService` 接口 + Context Provider 注入 |
---
## 五、架构图同步说明
本次审计发现架构图以下遗漏/不一致,需在实现后同步更新:
### 004_architecture_impact_map.md2.15 节)
1. **修正 actions 清单**:删除不存在的 `getAuditLogsAction` / `getLoginLogsAction`;补充说明页面直接调 data-access
2. **补全组件清单**:新增 `data-change-log-table.tsx``login-log-view.tsx``audit-log-export-button.tsx``data-change-log-filters.tsx`(拆分后)、`audit-error-boundary.tsx`(新增)、`audit-log-table-skeleton.tsx`(新增)
3. **更新行数**actions.ts 214→拆分后data-access.ts 290新增 export.ts、hooks/
4. **新增 hooks 清单**`use-log-pagination.ts`
5. **更新已知问题**:标记 P0-1~P1-8 已修复
### 005_architecture_data.jsonaudit 节点)
1. **修正 actions**:删除 `getAuditLogsAction`/`getLoginLogsAction``usedBy` 更新为 `api/export/route.ts`
2. **补全 components**:新增缺失组件
3. **新增 hooks 节点**`useLogPagination`
4. **新增 export 节点**`export.ts`
---
## 六、重构方案设计
### a. 新文件/目录结构
```
src/modules/audit/
├─ actions.ts # Server Actions编排层精简
├─ data-access.ts # 数据访问层(查询)
├─ export.ts # 🆕 Excel 导出(列定义+行映射+buildExcelExport
├─ types.ts # 类型定义 + 状态映射常量
├─ hooks/
│ └─ use-log-pagination.ts # 🆕 分页 URL 状态 hook消除三处重复
├─ components/
│ ├─ audit-log-view.tsx # 操作日志视图i18n + ErrorBoundary + Suspense
│ ├─ audit-log-table.tsx # 操作日志表格i18n + a11y
│ ├─ audit-log-filters.tsx # 操作日志筛选器i18n + a11y
│ ├─ audit-log-export-button.tsx # 导出按钮i18n + trackEvent
│ ├─ login-log-view.tsx # 登录日志视图
│ ├─ login-log-table.tsx # 登录日志表格
│ ├─ login-log-filters.tsx # 登录日志筛选器
│ ├─ data-change-log-view.tsx # 🆕 数据变更视图(拆分自 table
│ ├─ data-change-log-table.tsx # 数据变更表格(仅表格,不含筛选)
│ ├─ data-change-log-filters.tsx# 🆕 数据变更筛选器(拆分)
│ ├─ audit-error-boundary.tsx # 🆕 错误边界
│ └─ audit-log-table-skeleton.tsx # 🆕 骨架屏
├─ i18n/
│ └─ (由 shared/i18n/messages/{locale}/audit.json 统一管理)
└─ lib/
└─ audit-columns.ts # 🆕 导出列定义(纯函数,可单测)
```
### b. 核心代码示例
#### 数据服务接口定义P2-8中期实施
```typescript
// src/modules/audit/services/audit-service.ts
export interface AuditService {
getAuditLogs(params?: AuditLogQueryParams): Promise<PaginatedResult<AuditLog>>
getLoginLogs(params?: LoginLogQueryParams): Promise<PaginatedResult<LoginLog>>
getDataChangeLogs(params?: DataChangeLogQueryParams): Promise<PaginatedResult<DataChangeLog>>
getAuditModuleOptions(): Promise<string[]>
getDataChangeStats(): Promise<DataChangeStat[]>
getDataChangeTableOptions(): Promise<string[]>
}
// 角色实现示例(中期)
export class AdminAuditService implements AuditService {
// 封装 data-access 调用,权限已在 Server Action 层校验
async getAuditLogs(params?: AuditLogQueryParams) {
return getAuditLogs(params)
}
// ...其他方法委托给 data-access
}
```
#### 通用分页 HookP1-2本轮实施
```typescript
// src/modules/audit/hooks/use-log-pagination.ts
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useCallback } from "react"
export function useLogPagination(): (page: number) => void {
const router = useRouter()
const searchParams = useSearchParams()
return useCallback((newPage: number) => {
const params = new URLSearchParams(searchParams.toString())
if (newPage <= 1) params.delete("page")
else params.set("page", String(newPage))
const query = params.toString()
router.push(query ? `?${query}` : "?")
}, [router, searchParams])
}
```
#### 导出模块抽取P1-3本轮实施
```typescript
// src/modules/audit/export.ts
import { exportToExcel, type ExcelColumn } from "@/shared/lib/excel"
import { formatDateForFile } from "@/shared/lib/utils"
import type { AuditLog, LoginLog, DataChangeLog } from "./types"
export const AUDIT_LOG_COLUMNS: ExcelColumn[] = [
{ header: "User ID", key: "userId", width: 22 },
// ...列定义
]
export function mapAuditLogsToRows(items: AuditLog[]) {
return items.map((r) => ({ /* ...映射 */ }))
}
export async function buildAuditLogExport(items: AuditLog[]) {
const buffer = await exportToExcel({
sheets: [{ name: "Audit Logs", columns: AUDIT_LOG_COLUMNS, rows: mapAuditLogsToRows(items) }],
})
return { buffer, filename: `audit_logs_${formatDateForFile()}.xlsx` }
}
```
#### ErrorBoundary 包裹P1-5本轮实施
```tsx
// src/modules/audit/components/audit-error-boundary.tsx
"use client"
import { Component, type ReactNode } from "react"
import { useTranslations } from "next-intl"
interface Props { children: ReactNode }
interface State { hasError: boolean }
export class AuditErrorBoundary extends Component<Props, State> {
state: State = { hasError: false }
static getDerivedStateFromError(): State { return { hasError: true } }
render() {
if (this.state.hasError) {
return <AuditErrorFallback onRetry={() => this.setState({ hasError: false })} />
}
return this.props.children
}
}
```
#### 角色组装页面(配置驱动,中期)
```tsx
// 未来扩展:通过配置决定渲染哪些 Widget
const AUDIT_WIDGET_CONFIG = {
admin: ["overviewStats", "auditLogTable", "loginLogTable", "dataChangeTable"],
compliance: ["auditLogTable", "dataChangeTable"],
} as const
```
### c. 解耦与测试说明
- **纯函数抽取**:分页计算(`clampPage`/`clampPageSize`)、列映射(`mapAuditLogsToRows`)、状态映射常量已独立于 UI可直接单测
- **Hook 测试**`useLogPagination` 通过 mock `next/navigation``useRouter`/`useSearchParams` 测试
- **组件测试**`AuditLogTable` 接收纯 props传入 mock 数据即可渲染测试
- **Mock service 示例**
```typescript
const mockEmptyService: AuditService = {
getAuditLogs: async () => ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }),
// ...其他返回空数据
}
```
### d. i18n 集成示例
翻译文件结构(`shared/i18n/messages/zh-CN/audit.json`
```json
{
"title": "审计日志",
"description": "追踪系统内所有用户操作,保障安全与合规。",
"table": {
"user": "用户", "module": "模块", "action": "操作",
"target": "目标", "status": "状态", "ipAddress": "IP 地址",
"time": "时间", "userAgent": "用户代理", "tableName": "数据表",
"recordId": "记录 ID", "changedBy": "操作人", "view": "查看", "hide": "隐藏",
"oldValue": "旧值", "newValue": "新值"
},
"filter": {
"anyModule": "任意模块", "anyStatus": "任意状态", "anyAction": "任意操作",
"anyTable": "任意数据表", "actionPlaceholder": "操作...",
"success": "成功", "failure": "失败",
"create": "创建", "update": "更新", "delete": "删除",
"signIn": "登录", "signOut": "登出", "signUp": "注册", "reset": "重置"
},
"empty": {
"audit": "暂无审计日志", "login": "暂无登录日志", "dataChange": "暂无数据变更日志"
},
"export": { "button": "导出 Excel", "success": "导出成功", "failed": "导出失败" },
"error": { "title": "加载失败", "description": "数据加载时发生错误,请稍后重试。", "retry": "重试" },
"loginLogs": { "title": "登录日志", "description": "..." },
"dataChanges": { "title": "数据变更日志", "description": "..." }
}
```
组件使用:
```tsx
const t = useTranslations("audit")
<TableHead>{t("table.user")}</TableHead>
<EmptyTableRow colSpan={7} message={t("empty.audit")} />
```
### e. 错误处理与加载状态示例
```tsx
// page.tsx
<AuditErrorBoundary>
<Suspense fallback={<AuditLogTableSkeleton />}>
<AuditLogView items={result.items} /* ... */ />
</Suspense>
</AuditErrorBoundary>
```
### 最终检查
- [x] 该模块不存在对其他业务模块的直接 import仅依赖 shared/*
- [x] 没有使用 `any` 或硬编码角色字符串
- [x] 所有 actions 包含 `requirePermission` 调用4 个 Action 均有)
- [x] 文件行数未超过建议上限(最大 data-access.ts 290 行 < 800
- [x] 架构影响地图需同步更新(见第五节)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,440 @@
# 班级classes模块审计报告
> 审计时间2026-06-25
> 审计范围:`src/modules/classes/` 全部 32 个文件 + `src/app/(dashboard)/` 下 11 个相关路由分组
> 架构图依据:`docs/architecture/004_architecture_impact_map.md` §2.7、`docs/architecture/005_architecture_data.json` modules.classes 节点
> 审计基线项目规则三层架构、权限校验、i18n、TypeScript 严格模式、文件行数、可测试性)
## 一、现有实现概要
### 1.1 文件分布
classes 模块位于 `src/modules/classes/`,共 **32 个文件**
| 层 | 文件数 | 关键文件 |
|---|---|---|
| Server Actions | 7 | `actions.ts`barrel+ `actions-admin.ts` / `actions-grade.ts` / `actions-teacher.ts` / `actions-invitations.ts` / `actions-schedule.ts` / `actions-shared.ts` |
| Data-access | 7 | `data-access.ts`(聚合)+ `data-access-admin.ts` / `data-access-teacher.ts` / `data-access-students.ts` / `data-access-stats.ts` / `data-access-schedule.ts` / `data-access-invitations.ts` |
| Schema/Types | 2 | `schema.ts`13 个 Zod schema`types.ts`19 个类型) |
| Components | 21 | `admin-classes-view.tsx` / `grade-classes-view.tsx` / `class-list-table.tsx` / `class-form-dialog.tsx` / `class-delete-dialog.tsx` / `class-list-toolbar.tsx` / `class-form-utils.ts` / `class-error-boundary.tsx` / `class-invitation-manager.tsx` / `class-skeleton.tsx` / `my-classes-grid.tsx` / `schedule-view.tsx` / `schedule-filters.tsx` / `students-filters.tsx` / `students-table.tsx` + `class-detail/` 下 7 个 widgetheader/overview-stats/quick-actions/schedule-widget/students-widget/assignments-widget/trends-widget+ `class-detail/edit-class-dialog.tsx` |
| Hooks | 2 | `use-class-data.ts``use-class-filters.ts` |
### 1.2 主要数据流
```
admin/school/classes ─┐
management/grade/classes ─┤ ─▶ actions-admin / actions-grade
│ │
teacher/classes/my ─┤ ▼
teacher/classes/schedule ─┤ data-access-admin / data-access-teacher
teacher/classes/students ─┤ │
│ ▼ (跨模块通过对方 data-access)
student/learning/courses ─┤ homework/data-access-classes作业统计
student/schedule ─┤ scheduling/data-access-class-schedule课表写
parent/children/[id] ─┘ school/data-access年级管理权限校验
```
### 1.3 架构图覆盖完整性评估
| 维度 | 004 文档 | 005 JSON | 一致性 |
|---|---|---|---|
| 模块职责 | ✅ §2.7 | ✅ modules.classes.description | 一致 |
| 依赖关系(出向) | ✅ shared/auth/school/homework/scheduling | ✅ dependencyMatrix 5 条边 | 一致 |
| 被依赖关系(入向) | ⚠️ 列出 10 个,遗漏 elective/error-book/adaptive-practice | ✅ 10 条边覆盖完整 | **不一致** |
| 导出函数 | ✅ 17 actions + 33 data-access | ⚠️ exports.actions 漏 3 个邀请码 actionexports.dataAccess 漏 6 个工具函数 | **不一致** |
| 数据库表 | ✅ classes/classSubjectTeachers/classEnrollments/classInvitationCodes | ⚠️ classInvitationCodes 字段未详细登记classSchedule 的 usedBy 字段未含 scheduling | **部分遗漏** |
| 权限点 | ✅ 6 个 CLASS_* 权限 | ✅ permissions 常量定义完整 | 一致 |
| 路由 | ✅ 11 条路由登记 | ✅ routes 节点登记 | 一致 |
| 文件清单 | ✅ 33 个文件 | ⚠️ modules.classes.files 仅列 21 个,漏 12 个组件文件 | **不一致** |
**结论**:架构图总体覆盖较完整,但存在 7 处需同步更新(详见第五章)。
## 二、现存问题与原因分析
### 2.1 三层架构合规性
#### 问题 A1data-access.ts 与拆分文件 3 对同名函数重复定义(🔴 P0
- **位置**
- [data-access.ts:400](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L400) `getTeacherScopeData` ↔ [data-access-teacher.ts:592](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L592)
- [data-access.ts:428](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L428) `getStudentScopeData` ↔ [data-access-students.ts:309](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L309)
- [data-access.ts:455](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L455) `getGradeIdsForStudentIds` ↔ [data-access-students.ts:336](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L336)
- **现状**`data-access.ts:383-386` 同时 `export * from "./data-access-*"` 与本地 `export const`ES 模块语义下本地定义优先,子文件同名导出对聚合入口而言是死代码。
- **违反规则**架构分层规则「data-access 拆分应避免职责重叠」+ DRY 原则。
- **后果**:两份实现目前逻辑一致,但任何一方修改都不会自动同步。若消费者直接 `import { getStudentScopeData } from "@/modules/classes/data-access-students"`,会得到另一份实现,是高风险维护陷阱。
#### 问题 A2组件使用绝对路径导入本模块 actions🟢 P2
- **位置**[class-invitation-manager.tsx:30-32](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L30)
- **现状**`import { createClassInvitationCodeAction } from "@/modules/classes/actions"`,而 `my-classes-grid.tsx``admin-classes-view.tsx` 等同模块其他组件使用 `"../actions"` 相对路径。
- **违反规则**:编码规范一致性。
- **后果**:模块迁移/重命名成本上升。
### 2.2 权限校验
#### 问题 B1listClassInvitationCodesAction 无班级归属校验(🔴 P0 越权漏洞)
- **位置**[actions-invitations.ts:312-351](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L312)
- **现状**
```ts
export async function listClassInvitationCodesAction(classId: string) {
await requirePermission(Permissions.CLASS_ENROLL)
// ❌ 任何拥有 CLASS_ENROLL 权限的用户都可以列出任意 classId 的所有邀请码
const codes = await listClassInvitationCodes(classId)
}
```
- **违反规则**安全规范「data-access 查询是否结合当前用户权限过滤(防越权)」+ Server Action 规范。
- **后果**:教师 A 可通过传入教师 B 的 classId 枚举其邀请码(含 code 字符串),可能引发越权获取加入凭证,破坏邀请码体系的安全性。
#### 问题 B23 个 schedule action 无班级归属校验(🔴 P0 越权漏洞)
- **位置**[actions-schedule.ts:21-59](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L21)create、[61-101](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L61)update、[103-122](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L103)
- **现状**3 个 action 仅调用 `requirePermission(CLASS_SCHEDULE)` 后直接调用 scheduling 模块 data-access未校验当前用户对 `classId` 的归属。
- **违反规则**:安全规范越权防护。
- **后果**:教师 A 可为教师 B 的班级添加/修改/删除课表项,破坏排课数据完整性。
#### 问题 B3教师 update/delete/enroll action 未在 actions 层做归属校验(🟡 P1
- **位置**[actions-teacher.ts:79-122](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L79)update、[125-146](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L125)delete、[actions-invitations.ts:21-49](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L21)、[353-377](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L353)、[383-440](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L383)
- **现状**actions 层仅 `requirePermission`,依赖 data-access 内部 `getTeacherIdForMutations()` + `eq(classes.teacherId, teacherId)` 校验。
- **违反规则**Server Action 规范「权限校验应在 actions 层完成」。
- **后果**:管理员(拥有 CLASS_UPDATE 权限)调用时,因 data-access 内部强制按 teacherId 过滤而失败,错误信息为 "Teacher not found" 不友好;如未来重构 data-access 暴露 admin 路径,归属校验会被跳过。
#### 问题 B4data-access 中 7 处硬编码角色名字符串(🟡 P1
- **位置**
- [data-access.ts:26](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L26) `eq(roles.name, "teacher")`
- [data-access-admin.ts:346](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L346)、[425](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L425)
- [data-access-teacher.ts:125](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L125)、[308](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L308)、[472](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L472)、[545](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L545)
- **违反规则**命名规范「常量UPPER_SNAKE_CASE」+ DRY。
- **后果**:若角色名变更(中英切换、复数化),需修改 7 处;魔法字符串降低可读性。
#### 问题 B5student 三个页面未调用 requirePermission 显式声明权限点(🟡 P1
- **位置**
- [student/learning/courses/page.tsx:19](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/courses/page.tsx#L19)
- [student/learning/courses/[classId]/page.tsx:40-41](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/courses/[classId]/page.tsx#L40)
- [student/schedule/page.tsx:19](file:///e:/Desktop/CICD/src/app/(dashboard)/student/schedule/page.tsx#L19)
- **现状**:仅 `getCurrentStudentUser()` 软校验学生身份,未显式声明所需权限点;`[classId]` 页面在权限不足时返回 `notFound()`错误语义混淆404 vs 403
- **违反规则**Server Action 必须使用 `requirePermission()`。
- **后果**权限审计与文档化困难404/403 错误语义混淆影响用户体验与监控告警。
### 2.3 国际化i18n
#### 问题 C1class-detail/ 子组件普遍硬编码英文(🟡 P1
- **位置**
- [class-assignments-widget.tsx:40,46,64,84,88,101](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-assignments-widget.tsx#L40)
- [class-overview-stats.tsx:28,33,39,45](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-overview-stats.tsx#L28)
- [class-quick-actions.tsx:26,32,36](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-quick-actions.tsx#L26)
- [class-schedule-widget.tsx:17,102,110](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-schedule-widget.tsx#L17)
- [class-students-widget.tsx:36,41](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-students-widget.tsx#L36)
- [class-trends-widget.tsx:41-55,145,164,174,182,188,282,288,301,392](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-trends-widget.tsx#L41)
- **现状**8 个详情子组件几乎全部使用硬编码英文字符串。
- **违反规则**i18n 强制要求 + 架构图 004:998 已声明"13 个组件全部接入 i18n"——与现状不一致。
- **后果**:详情页完全无法中文化,与 K12 中文产品定位严重冲突;架构图存在错误声明。
#### 问题 C2schedule-view / schedule-filters / students-filters 硬编码英文(🟡 P1
- **位置**
- [schedule-view.tsx:111,117,165,166,180,192,310,343,348,357,369,386,435,448,469,489,505-508](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L111)
- [schedule-filters.tsx:111,117,164,171,180,192](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-filters.tsx#L111)
- [students-filters.tsx:81,110,141,169,173,174,197,204,211](file:///e:/Desktop/CICD/src/modules/classes/components/students-filters.tsx#L81)
- **违反规则**i18n 强制要求。
- **后果**:教师端课表/学生管理页全部英文,破坏产品一致性。
#### 问题 C3所有 actions 返回的 message 为英文硬编码(🟡 P1
- **位置**:全部 7 个 actions 文件
- **示例**[actions-admin.ts:39](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L39) `"Class name, grade and teacher are required"` / [actions-admin.ts:59](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L59) `"Class created successfully"` / [actions-invitations.ts:432](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L432) `` `Imported ${imported} students, ${failed} failed` ``
- **现状**:组件层 `toast.success(res.message)` 直接显示后端字符串。
- **违反规则**i18n 强制要求。
- **后果**:中文用户看到全英文错误提示,体验差。
#### 问题 C430+ 个 error.tsx 使用硬编码中文文案(🟡 P1
- **位置**`src/app/(dashboard)/` 下 30 处 error.tsx含 `admin/school/classes/error.tsx`、`management/grade/classes/error.tsx` 等)
- **示例**[admin/school/classes/error.tsx:12-16](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/classes/error.tsx#L12) `title="页面加载失败"` / `description="抱歉,页面加载时发生了意外错误。请稍后重试。"`
- **违反规则**i18n 强制要求。
- **后果**英文用户在错误页仍看到中文破坏语言一致性30+ 处重复字符串维护成本高。
#### 问题 C5DEFAULT_CLASS_SUBJECTS 与 excludeSubjects 业务常量硬编码中文(🟡 P1
- **位置**
- [types.ts:46](file:///e:/Desktop/CICD/src/modules/classes/types.ts#L46) `export const DEFAULT_CLASS_SUBJECTS = ["语文", "数学", "英语", "美术", "体育", "科学", "社会", "音乐"] as const`
- [data-access-students.ts:41](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L41) `const excludeSubjects = ["体育", "音乐", "美术"]`
- **违反规则**DRY两份科目清单+ i18n业务规则硬编码字符串
- **后果**:与 `subjects` 表查询重复;不同学校配置无法适配;`excludeSubjects` 未在架构图记录。
#### 问题 C6getSubjectColor 用英文匹配中文科目(🔴 P0 功能 bug
- **位置**[schedule-view.tsx:186-195](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L186)
- **现状**
```ts
const getSubjectColor = (subject: string) => {
const s = subject.toLowerCase()
if (s.includes('math')) return 'bg-blue-500/10 ...'
if (s.includes('physics') || s.includes('science')) return '...'
if (s.includes('english') || s.includes('lit')) return '...'
```
- **问题**`DEFAULT_CLASS_SUBJECTS` 是中文("数学"、"英语"等),但 `getSubjectColor` 用英文 'math'/'english' 匹配,所有中文科目都会落到 default 分支,颜色视觉分组完全失效。
- **违反规则**i18n + 功能正确性。
- **后果**:课表颜色视觉分组完全失效,不仅是 i18n 问题。
### 2.4 错误处理
#### 问题 D1catch 块未记录错误,仅显示 toast🟡 P1
- **位置**10 处
- [my-classes-grid.tsx:75-77](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L75)
- [schedule-filters.tsx:65-67](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-filters.tsx#L65)
- [schedule-view.tsx:113-115](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L113)
- [students-filters.tsx:73-75](file:///e:/Desktop/CICD/src/modules/classes/components/students-filters.tsx#L73)
- [admin-classes-view.tsx:52-54](file:///e:/Desktop/CICD/src/modules/classes/components/admin-classes-view.tsx#L52)
- [grade-classes-view.tsx:42-44](file:///e:/Desktop/CICD/src/modules/classes/components/grade-classes-view.tsx#L42)
- [class-invitation-manager.tsx:101-103](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L101)、[263-265](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L263)
- [edit-class-dialog.tsx:55-57](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/edit-class-dialog.tsx#L55)
- [students-table.tsx:41-43](file:///e:/Desktop/CICD/src/modules/classes/components/students-table.tsx#L41)
- **现状**`} catch { toast.error(t("list.failedCreate")) }`
- **违反规则**:错误处理最佳实践。
- **后果**:服务端 500、网络错误、权限错误全部显示同一文案开发者无法从用户截图定位错误线上排查困难。
#### 问题 D2data-access 内 catch 静默返回空数组(🟡 P1
- **位置**
- [data-access-admin.ts:88-124](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L88)getAdminClasses fallback 合理降级)
- [data-access-students.ts:159-177](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L159)getStudentClasses fallback 合理降级)
- [data-access-teacher.ts:70-73](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L70)getTeacherClasses 直接返回 `[]`
- **问题**`getTeacherClasses` 失败时返回空数组,会让教师看到"无班级"假象,区分不出"数据库错误"与"无数据"。
- **违反规则**:错误处理最佳实践。
- **后果**:教师班级列表假性空数据,难以排查。
#### 问题 D3actions 层错误处理两套风格混用(🟡 P1
- **位置**
- 风格 A手动 try/catch + `PermissionDeniedError`[actions-admin.ts:63-66](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L63)、[actions-grade.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-grade.ts)、[actions-invitations.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts)
- 风格 B`handleActionError`[actions-teacher.ts:74-76](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L74)、[actions-schedule.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts)
- **违反规则**:编码规范一致性。
- **后果**:权限拒绝场景下风格 A 会 `throw e` 冒泡到 Next.js 错误边界(用户体验差),风格 B 会转为结构化失败;维护成本高。
#### 问题 D4teacher/classes/my/[id] 缺 loading.tsx 和 error.tsx🟡 P1
- **位置**`src/app/(dashboard)/teacher/classes/my/[id]/`
- **现状**:页面内部 4 个 `Promise.all` 并行数据获取,但完全没有 loading.tsx 和 error.tsx。
- **违反规则**:项目规则「学生路由必须包含 loading.tsx 和 error.tsx」+ 企业级规范。
- **后果**:无骨架屏感知性能差;任一 data-access 抛错冒泡至上层;`notFound()` 触发时展示默认 404 与站点风格不一致。
#### 问题 D510 个页面缺 error.tsx🟡 P1
- **位置**teacher/classes/my、teacher/classes/schedule、teacher/classes/students、student/learning/courses、student/learning/courses/[classId]、student/schedule 等
- **违反规则**:企业级规范要求主要路由必须有 error.tsx。
- **后果**:错误上下文丢失,错误冒泡至上层。
### 2.5 类型安全
#### 问题 E1formData.get 强转 string 应使用类型守卫(🟢 P2
- **位置**[actions-admin.ts:93](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L93)、[actions-grade.ts:98](file:///e:/Desktop/CICD/src/modules/classes/actions-grade.ts#L98)
- **现状**`formData.get("subjectTeachers") as string | null`
- **违反规则**:禁止 `as` 断言(除类型收窄外)。
- **后果**:若前端意外提交 `File` 对象(如通过 FormData.append 上传文件),`parseSubjectTeachers` 会因 `typeof raw !== "string"` 返回 null 而静默丢弃数据。
#### 问题 E2data-access-invitations.ts 中两处 string → union 强转(🟢 P2
- **位置**[data-access-invitations.ts:274](file:///e:/Desktop/CICD/src/modules/classes/data-access-invitations.ts#L274)、[363](file:///e:/Desktop/CICD/src/modules/classes/data-access-invitations.ts#L363)
- **现状**`record.status as ValidationResult["reason"]` / `status: row.status as InvitationCodeStatus`
- **违反规则**:禁止 `as` 断言。
- **后果**DB 中出现意外值(如 "pending")不会触发类型错误,运行时返回错误 reason。
#### 问题 E3class-trends-widget.tsx 中 as string[] 应使用类型守卫(🟢 P2
- **位置**[class-trends-widget.tsx:129](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-trends-widget.tsx#L129)
- **现状**`Array.from(new Set(assignments.map(a => a.subject).filter(Boolean))) as string[]`
- **违反规则**:禁止 `as` 断言。
- **后果**`filter(Boolean)` 在 TypeScript 中不会收窄类型,跳过空值检查。
#### 问题 E414 个组件事件处理函数缺 Promise<void> 返回类型(🟢 P2
- **位置**my-classes-grid.tsx4 处、schedule-filters.tsx、schedule-view.tsx4 处、students-filters.tsx、students-table.tsx、class-invitation-manager.tsx3 处、edit-class-dialog.tsx
- **违反规则**:函数返回值必须显式标注,特别是 `Promise<T>`。
- **后果**:若将来函数内部 `return` 一个值(如返回 boolean 表示是否成功),调用方无类型提示。
### 2.6 文件大小
#### 问题 F1schedule-view.tsx 527 行超出组件 500 行建议(🟡 P1
- **位置**[schedule-view.tsx](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx)
- **现状**:同时承担"周历视图渲染 + 创建对话框 + 编辑对话框 + 删除确认对话框"4 个职责。
- **违反规则**React 组件 ≤ 500 行。
- **后果**:可读性差、修改易引入回归。
#### 问题 F2my-classes-grid.tsx 内含 210 行巨型组件 ClassTicket🟢 P2
- **位置**[my-classes-grid.tsx:208-417](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L208)
- **现状**:单一组件管理 4 段视觉职责(票据左侧信息+邀请码+趋势图+周历嵌入式渲染)。
- **违反规则**:组件规范。
- **后果**:调试困难。
### 2.7 组件复用性
#### 问题 G13 个 view 的 CRUD handler 几乎重复(🟢 P2
- **位置**admin-classes-view.tsx:41-95、grade-classes-view.tsx:31-85、schedule-view.tsx:100-158
- **现状**:相同的 try/catch + toast + router.refresh 模式重复 9 次(每个 view 3 个 handler
- **违反规则**DRY。
- **后果**:错误处理改进(如 D1 加 console.error需修改 9 处。
### 2.8 可测试性
#### 问题 H1纯函数内嵌组件未导出🟢 P2
- **位置**
- [my-classes-grid.tsx:40-48](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L40) `getSeededValue`
- [my-classes-grid.tsx:268-272](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L268) `performanceChange` 计算
- [schedule-view.tsx:160-181](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L160) `getPositionStyle`
- [schedule-view.tsx:186-195](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L186) `getSubjectColor`
- **违反规则**:可测试性「纯逻辑是否与 UI 分离」。
- **后果**:业务计算逻辑(环比变化率、课表块定位、颜色映射)无法独立单测;`getSubjectColor` 还存在 C6 提到的功能 bug但因内嵌而难以被发现。
### 2.9 i18n metadata 缺失
#### 问题 I18 个 teacher/student 页面缺 generateMetadata🟢 P2
- **位置**teacher/classes/my、teacher/classes/my/[id]、teacher/classes/schedule、teacher/classes/students、student/learning/courses、student/learning/courses/[classId]、student/schedule、parent/children/[studentId]
- **违反规则**:页面级 metadata.title 应走 i18n。
- **后果**浏览器标签栏、社交分享卡片等场景文案不本地化SEO 友好度差。
## 三、行业差距对比
参考 Google Classroom、钉钉教育、智学网、ClassDojo、PowerSchool 等 K12 班级管理产品的主流设计模式,对比当前实现差距:
### 3.1 班级列表与详情
| 维度 | 优秀实践 | 当前实现 | 差距 |
|---|---|---|---|
| 班级卡片信息密度 | Google Classroom 卡片含教师头像、学生数、最近活动时间、未读作业数 | `my-classes-grid.tsx` 含邀请码、提交率趋势、周历嵌入,信息密度高但**未提供"最近活动"时间线** | 缺少"班级最近动态"feed |
| 班级封面图 | Google Classroom / ClassDojo 支持自定义班级主题图 | 仅色彩区分 | 视觉识别度弱 |
| 班级详情布局 | 智学网采用"Tab 切换(学生/作业/成绩/课表/设置)+ 顶部 sticky header" | `class-detail/` 采用 widget 网格布局 | widget 平铺在班级数多时滚动疲劳;可考虑 Tab 化 |
| 班级归档 | Google Classroom 支持"归档班级",归档后只读但保留数据 | 无归档功能 | 学年结束后历史班级污染列表 |
| 班级复制 | Google Classroom 支持复制班级(含学生/科目配置) | 无 | 新学年建班成本高 |
### 3.2 学生管理
| 维度 | 优秀实践 | 当前实现 | 差距 |
|---|---|---|---|
| 学生加入方式 | 邀请码 + 邮件 + 批量导入 + 班级链接 | 已实现邀请码 + 邮件 + 批量导入 | 缺少"班级加入链接"(点击即加入) |
| 学生列表筛选 | 智学网支持按科目成绩、出勤率、活跃度多维筛选 | `students-filters.tsx` 仅按班级 + 状态 | 缺少按学业表现筛选 |
| 学生卡片信息 | Google Classroom 显示学生头像、最近提交、整体进度 | `students-table.tsx` 显示头像+科目成绩 | 缺少"最近提交/整体进度"时间维度 |
| 学生迁移 | 钉钉教育支持"批量迁班"(学年升级时) | 无 | 学年升级时手动逐个调整 |
| 学生邀请码状态可视化 | ClassDojo 显示邀请码扫描情况(已加入/待加入) | `class-invitation-manager.tsx` 显示邀请码列表 | 缺少"已扫码未加入"中间状态 |
### 3.3 课表
| 维度 | 优秀实践 | 当前实现 | 差距 |
|---|---|---|---|
| 课表视图 | 智学网周课表 + 日课表 + 月课表三视图切换 | `schedule-view.tsx` 仅周课表 | 缺少日/月视图 |
| 课表冲突检测 | PowerSchool 在添加时自动检测教室/教师/时段冲突 | 依赖 scheduling 模块外部检测 | 当前 actions-schedule 未触发检测B2 |
| 课表颜色编码 | 钉钉教育按科目自动配色 | `getSubjectColor` 中文失效C6 | **功能 bug必须修复** |
| 课表导出/打印 | 智学网支持导出 PDF/Excel、打印 | 无 | 教师打印课表需求未满足 |
| 课表提醒 | Google Classroom 课前 5 分钟推送提醒 | 无 | 缺少课表提醒集成 |
### 3.4 多角色协作
| 维度 | 优秀实践 | 当前实现 | 差距 |
|---|---|---|---|
| 家长视角班级信息 | ClassDojo 家长看到班级公告、教师动态、孩子表现 | parent 仅看到孩子课表 + 班级概览 | 缺少"班级动态 feed" |
| 学生视角班级首页 | Google Classroom 学生首页是"待办作业流" | student/learning/courses 是班级列表 | 缺少"班级作业待办流"聚合视图 |
| 跨班级协作 | 钉钉教育支持"年级主任一键查看所有班级对比" | management/grade 已有 insights | ✅ 已实现,对比图较完善 |
| 班级消息 | Google Classroom 班级内消息流 | messaging 模块支持 `class_members` scope | ✅ 已通过 messaging 模块实现 |
### 3.5 数据洞察
| 维度 | 优秀实践 | 当前实现 | 差距 |
|---|---|---|---|
| 班级健康度评分 | PowerSchool 综合出勤+成绩+参与度给出班级健康分 | `class-trends-widget.tsx` 仅展示提交率/平均分 | 缺少综合健康度评分 |
| 早期预警 | ClassDojo 识别"低参与度学生"自动预警 | 无 | 缺少学生风险预警 |
| 班级对比 | 智学网支持同年级班级多维度对比 | `management/grade/insights` 已有 | ✅ |
| 趋势同比环比 | PowerSchool 提供周/月/学期同比 | `class-trends-widget.tsx` 仅"Latest"指标 | 缺少时间维度对比 |
### 3.6 可访问性与性能
| 维度 | 优秀实践 | 当前实现 | 差距 |
|---|---|---|---|
| 键盘导航 | WCAG 2.1 AA 要求所有交互可键盘操作 | 班级列表/课表键盘可访问但无 focus-visible 样式优化 | a11y 待加强 |
| 流式渲染 | React 18+ Suspense 流式渲染 | 全部 RSC 同步获取,无 Suspense 边界 | 班级详情 4 个并行查询可流式 |
| 骨架屏精确度 | 骨架屏应反映实际布局 | `class-skeleton.tsx` 5 个 skeleton 布局匹配 | ✅ 良好 |
## 四、改进优先级建议
### P0 — 安全/功能阻断(立即修复)
| # | 问题 | 改进方向 |
|---|---|---|
| P0-1 | A1data-access.ts 与拆分文件 3 对同名函数重复定义 | 删除 data-access.ts 中的本地定义,统一从拆分文件导出(保持 barrel 入口兼容) |
| P0-2 | B1listClassInvitationCodesAction 无归属校验 | 在 actions 层调用 `verifyTeacherOwnsClass(classId, ctx.userId)`admin scope 跳过) |
| P0-3 | B23 个 schedule action 无归属校验 | 同 P0-2调用 `verifyTeacherOwnsClass` |
| P0-4 | C6getSubjectColor 中文科目不匹配 | 改用科目 ID 或类型守卫匹配;纯函数抽出到 `schedule-utils.ts` 并补单测 |
| P0-5 | B3教师 update/delete/enroll action 未在 actions 层做归属校验 | actions 层显式判断 `hasAdminScope(ctx)` 否则校验 `classes.teacherId === ctx.userId` |
### P1 — 重要合规性(本期实施)
| # | 问题 | 改进方向 |
|---|---|---|
| P1-1 | C1class-detail/ 8 子组件硬编码英文 | 全部接入 `useTranslations("classes.detail.*")` |
| P1-2 | C2schedule-view / schedule-filters / students-filters 硬编码英文 | 接入 i18n |
| P1-3 | C3所有 actions message 英文硬编码 | 在 actions 层使用 `getTranslations()` 或返回错误码由组件层翻译 |
| P1-4 | C430+ error.tsx 硬编码中文 | 抽取 `shared/components/ErrorState` + i18n key `common.error.boundary.*` |
| P1-5 | C5DEFAULT_CLASS_SUBJECTS 与 excludeSubjects 硬编码 | 改为从 `subjects` 表查询;统一单一来源 |
| P1-6 | B4data-access 中 7 处 roles.name 硬编码 | 抽出 `ROLE_TEACHER` 常量 |
| P1-7 | B5student 三个页面未调用 requirePermission | 补 `requirePermission(Permissions.HOMEWORK_SUBMIT)` 或新增 STUDENT_READ 权限 |
| P1-8 | D110 处 catch 块未记录错误 | 改为 `catch (error) { console.error("[classes] xxx:", error); toast.error(...) }` |
| P1-9 | D2getTeacherClasses 静默返回空数组 | 区分"DB 错误"与"无数据"DB 错误抛出 |
| P1-10 | D3actions 层错误处理两套风格 | 统一为 `handleActionError` |
| P1-11 | D4teacher/classes/my/[id] 缺 loading.tsx 和 error.tsx | 新增 |
| P1-12 | D510 个页面缺 error.tsx | 补齐 |
| P1-13 | F1schedule-view.tsx 527 行超限 | 抽出 3 个对话框子组件 |
### P2 — 工程优化(中长期)
| # | 问题 | 改进方向 |
|---|---|---|
| P2-1 | E1/E2/E35 处 as 断言 | 改用类型守卫 |
| P2-2 | E414 个事件处理函数缺 Promise<void> | 补返回类型 |
| P2-3 | G13 个 view CRUD handler 重复 | 抽 `useClassFormHandlers` hook |
| P2-4 | H1纯函数内嵌组件 | 抽到 `class-stats-utils.ts` / `schedule-utils.ts` 并补单测 |
| P2-5 | F2my-classes-grid ClassTicket 210 行 | 拆分为 `ClassTicketHeader` / `ClassTicketInvitation` / `ClassTicketTrend` |
| P2-6 | A2class-invitation-manager 绝对路径 | 改为相对路径 |
| P2-7 | I18 个页面缺 generateMetadata | 补齐 |
| P2-8 | 行业差距:班级归档/复制 | 中长期产品规划 |
| P2-9 | 行业差距:班级健康度评分/早期预警 | 中长期产品规划 |
| P2-10 | 行业差距:课表多视图/导出/提醒 | 中长期产品规划 |
### 重构设计原则(强制满足)
为达成"完全解耦 + 组合优先 + 国际化就绪 + 最大化复用 + 错误边界 + 可测试 + 可扩展 + 企业级补充"八项原则,本次实施遵循:
1. **完全解耦**classes 模块内部组件不直接 import 其他业务模块exams/grades/homework/scheduling的 actions/data-access跨模块数据通过本模块 data-access 暴露的接口调用(已基本达成,仅需修复 P0-2/P0-3 的越权问题)。
2. **组合优先**:错误处理抽 `useClassFormHandlers` hook纯逻辑抽 `class-stats-utils.ts`、`schedule-utils.ts`;详情 widget 通过配置驱动(`ClassDetailWidgetConfig`)。
3. **国际化就绪**:所有修复项必须使用 i18n key新增翻译键写入 `messages/{locale}/classes.json` 的 `detail.*` / `schedule.*` / `students.*` 命名空间。
4. **最大化复用**admin/grade/teacher/student 共用 `ClassListTable` / `ClassFormDialog` / `ClassDeleteDialog` / `useClassData` / `useClassFilters` / `useClassFormHandlers`。
5. **错误与边界**:每个详情 widget 独立用 `ClassErrorBoundary` 包裹RSC 数据获取用 React Suspense + 骨架屏;空数据/无权限/网络异常状态明确处理。
6. **可测试性**:纯函数导出至 `*.ts` 文件,便于 vitest 单测;接口类型显式标注。
7. **可扩展性**:角色差异通过 `hasAdminScope` / `hasTeacherScope` 抽象,新增角色只改 actions-shared.ts。
8. **企业级补充**:补 a11yfocus-visible、ARIA、性能Suspense 流式、安全actions 层归属校验)、监控(埋点接口预留 `trackClassEvent` 工具函数)。
## 五、架构图同步说明
本次审计发现架构图存在以下遗漏或不一致,需在实施修复时同步更新:
| # | 位置 | 问题 | 修复动作 |
|---|---|---|---|
| 1 | 005 `modules.classes.files` | 仅列 21 个文件,漏 schema.ts/types.ts/12 个组件文件 | 补全至 33 个文件 |
| 2 | 005 `modules.classes.exports.actions` | 漏 `createClassInvitationCodeAction` / `revokeClassInvitationCodesAction` / `listClassInvitationCodesAction` 3 个 v3 邀请码 action | 补全至 20 个 |
| 3 | 005 `modules.classes.exports.dataAccess` | 漏 `compareClassLike` / `isDuplicateInvitationCodeError` / `generateUniqueInvitationCode` / `getAccessibleClassIdsForTeacher` / `getSessionTeacherId` / `getTeacherSubjectIdsForClass` 6 个工具函数 | 补全 |
| 4 | 005 `dbTables` | `classInvitationCodes` 表字段未详细登记 | 补全字段定义 |
| 5 | 005 `dbTables` | `classSchedule` 的 `usedBy` 字段未含 scheduling | 补充 `["classes", "scheduling"]` |
| 6 | 005 `knownIssues.P0-1` | 仍记录原始问题状态,未标注「已修复」 | 更新 status 字段 |
| 7 | 004 §2.7「被依赖」 | 遗漏 `elective` / `error-book` / `adaptive-practice` 三个模块 | 补全至 13 个 |
| 8 | 005 `dependencyMatrix` | `exams → classes` 与 `homework → classes` 边未明确登记 | 补全边定义 |
| 9 | 004 §2.7 已知问题 | P0-2/P0-3/P0-4/P0-5 等本次新增修复 | 同步新增修复记录 |
| 10 | 004 §2.7 文件清单 | 修复后行数变化(如 schedule-view.tsx 拆分) | 更新行数 |

View File

@@ -0,0 +1,262 @@
# 课程计划模块审计报告
> 审计日期2026-06-25
> 审计范围:`src/modules/course-plans/` 全部文件 + `src/app/(dashboard)/{admin,teacher}/course-plans/` 全部页面
> 审计依据:`e:\Desktop\CICD\.trae\rules\project_rules.md`、`docs/architecture/004_architecture_impact_map.md` §2.18、`docs/architecture/005_architecture_data.json` modules.`course-plans`
---
## 一、现有实现概要
### 1.1 文件分布
| 层级 | 文件数 | 主要文件(行数) |
|------|--------|------------------|
| types/schema | 2 | types.ts(97)、schema.ts(180) |
| data-access | 1 | data-access.ts(425) |
| actions | 1 | actions.ts(284) |
| components | 5 | course-plan-list.tsx(160)、course-plan-detail.tsx(243)、course-plan-form.tsx(284)、course-plan-item-editor.tsx(248)、course-plan-progress.tsx(38) |
| 页面 | 6 | admin(4: list/detail/create/edit)、teacher(2: list/detail) |
| i18n | 2 | zh-CN/course-plans.json(15)、en/course-plans.json(15) |
文件行数均在规范范围内(组件 ≤500、actions/data-access ≤800
### 1.2 数据流
```
页面(Server Component)
├─ admin/* → 直接调用 data-access.getCoursePlans / getCoursePlanById无 requirePermission
├─ teacher/* → requirePermission(COURSE_PLAN_READ) → data-access按 teacherId 过滤)
└─ management/grade/dashboard → data-access.getGradeCoursePlanProgress
动态 import classes data-access.getClassesByGradeId
JOIN course_plans + course_plan_items
Client Components
├─ CoursePlanList → usePermission() → 本地筛选
├─ CoursePlanDetail → 直接 import deleteCoursePlanAction
├─ CoursePlanForm → 直接 import create/updateCoursePlanAction
└─ CoursePlanItemEditor → 直接 import item CRUD actions
```
### 1.3 架构图完整性
`docs/architecture/004_architecture_impact_map.md` §2.18 与 `005_architecture_data.json` 已记录该模块的导出函数、文件清单、依赖关系,与实际代码**基本一致**。但存在以下遗漏与不一致:
- `data-access.ts``getSubjectOptions` 函数**未在架构图 exports 中记录**
- `data-access.ts``reorderCoursePlanItems` 函数**未在架构图 exports 中记录**(且无对应 Action / UI属于死代码
- 架构图标注 `getCoursePlansAction`/`getCoursePlanAction``usedBy` 为"待扩展",实际仍无消费方
- 架构图依赖矩阵显示 course-plans → classes/school 为"✅"(通过 data-access但实际 `buildPlanSelect` **直接 JOIN** classes/subjects/users 表,并非通过 data-access 调用——架构图记录与实现不一致
---
## 二、现存问题与原因分析
### 2.1 安全与权限问题P0
#### 问题 1教师详情页未校验计划归属 — 信息泄露漏洞
- **位置**`src/app/(dashboard)/teacher/course-plans/[id]/page.tsx` 第 16-18 行;`data-access.ts` `getCoursePlanById` 第 168-192 行
- **问题**:教师详情页仅调用 `requirePermission(COURSE_PLAN_READ)` 后直接 `getCoursePlanById(id)`**未校验该计划是否属于当前教师**。`getCoursePlanById` 也不接受 `userId` 参数。
- **违反规则**:项目规则 "Parent routes must include permission checks with both parentId and studentId to prevent information leakage"(同理,教师路由也应校验 teacherId 归属);"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"
- **后果**:任何持有 `COURSE_PLAN_READ` 权限的教师,通过枚举/猜测 planId 即可查看全校所有课程计划详情(含其他班级、其他科目的教学进度、大纲、目标),构成信息泄露
#### 问题 2admin 列表页无 requirePermission 调用
- **位置**`src/app/(dashboard)/admin/course-plans/page.tsx` 全文(第 23-49 行)
- **问题**admin 列表页**未调用 `requirePermission()`**,直接调用 `getCoursePlans()` 返回全部数据。对比 `teacher/course-plans/page.tsx` 第 27 行有 `requirePermission` 调用——admin 与 teacher 页面权限处理不一致。
- **违反规则**:项目规则 "所有 Server Action 必须调用 requirePermission() 进行权限校验"(页面层虽非 Action但 data-access 直接被 Server Component 调用时同样需校验);依赖布局层保护属于隐式安全,不符合纵深防御原则
- **后果**若布局层权限配置被误改admin 列表页将完全暴露
#### 问题 3data-access 函数无数据范围DataScope过滤
- **位置**`data-access.ts` `getCoursePlans`(第 144-166 行)、`getCoursePlanById`(第 168-192 行)、`getGradeCoursePlanProgress`(第 335-424 行)
- **问题**:所有查询函数**均不接受 userId / dataScope 参数**,不进行任何归属过滤。`getCoursePlans` 仅靠调用方传入 `teacherId` 参数过滤,但参数可选且可被绕过。
- **违反规则**:项目规则 "所有敏感数据查询必须在 data-access 层结合当前用户权限过滤Server Action 二次校验"
- **后果**:未来新增 parent/student 路由时,若直接复用这些函数将导致越权;当前教师详情页已暴露此问题(见问题 1
### 2.2 国际化严重缺失P0
#### 问题 4组件内大量硬编码文本中英文混杂
- **位置**
- `course-plan-list.tsx`:第 25-47 行 `STATUS_LABEL`/`STATUS_VARIANT`/`FILTER_OPTIONS` 全英文硬编码;第 99/108-112 行 "New Course Plan"/"No course plans"/"There are no course plans yet." 等
- `course-plan-detail.tsx`:第 30-35 行 `STATUS_LABEL` 全中文硬编码("规划中"/"进行中"/"已完成"/"已暂停");第 94/99/106-113/124-134/146-153/161-172/178-183/213 行大量中文硬编码
- `course-plan-form.tsx`:第 98/105/122/139/156/173/186/203/214/225/234/247/257 行全英文硬编码("New Course Plan"/"Class"/"Subject"/"Teacher" 等)
- `course-plan-item-editor.tsx`:第 119/125/136/148/159/172/180/191 行全英文硬编码
- `course-plan-progress.tsx`:第 25/27 行 "Progress"/"hours" 硬编码
- `teacher/course-plans/page.tsx`:第 41-43 行 "My Course Plans"/"View your course teaching plans..." 硬编码
- **违反规则**:项目规则 "所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"
- **后果**
1. 国际化完全不可用——切换语言后课程计划模块仍显示混合中英文
2. 同一模块内 `course-plan-detail.tsx`(中文)与 `course-plan-list.tsx`(英文)状态标签不一致,用户体验割裂
3. 翻译文件 `course-plans.json` 仅含 5 个键title/description/detail/edit/create远不满足组件需要
### 2.3 架构违规问题P1
#### 问题 5跨模块直接 JOIN 其他模块数据库表
- **位置**`data-access.ts` `buildPlanSelect` 第 115-142 行
- **问题**:直接 `leftJoin(classes, ...)``leftJoin(subjects, ...)``leftJoin(users, ...)`,分别查询 classes 模块、school 模块、users 模块拥有的表。架构图却标注为"✅ 通过 data-access"。
- **违反规则**:项目规则 "模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表"
- **后果**classes/school/users 模块的表结构变更将直接影响 course-plans 查询;模块未真正解耦,无法独立测试
#### 问题 6缺少 loading.tsx / error.tsx
- **位置**`src/app/(dashboard)/admin/course-plans/``src/app/(dashboard)/teacher/course-plans/` 全部路由
- **问题**6 个页面路由均**无 loading.tsx 和 error.tsx**。
- **违反规则**:项目规则 "All student routes must include loading.tsx and error.tsx for error boundaries"best practice 推广至所有角色路由)
- **后果**:数据加载期间白屏;运行时错误无边界捕获,导致整页崩溃
#### 问题 7使用原生 `<a>` 标签替代 `<Link>`
- **位置**`course-plan-list.tsx` 第 97 行 `<a href={createHref}>`、第 149 行 `<a key={plan.id} href={href}>`
- **违反规则**:项目规则 "Link navigation must use Next.js `<Link>` component instead of raw `<a>` tags"
- **后果**:点击导航触发整页刷新,丢失客户端状态,无预取优化
### 2.4 代码质量问题P1
#### 问题 8使用 `as` 类型断言
- **位置**
- `course-plan-list.tsx` 第 73 行:`setFilter(value as Filter)`
- `course-plan-form.tsx` 第 174 行:`setSemester(v as "1" | "2")`;第 188 行:`setStatus(v as CoursePlanStatus)`
- `teacher/course-plans/page.tsx` 第 19 行:`(v as CoursePlanStatus)`
- **违反规则**:项目规则 "禁止 as 断言(除类型收窄外)"
- **后果**:运行时类型不安全,应使用类型守卫函数(如 admin 页面已实现的 `isValidStatus`
#### 问题 9基于 URL 字符串判断角色的脆弱逻辑
- **位置**
- `course-plan-detail.tsx` 第 63 行:`backHref?.includes("/teacher/") ? "/teacher/course-plans" : "/admin/course-plans"`
- `course-plan-form.tsx` 第 81 行:同样的 `backHref?.includes("/teacher/")` 模式
- **问题**:通过 URL 路径字符串推断用户角色来决定跳转目标,而非通过权限/角色上下文。
- **违反规则**:项目规则 "前端权限判断统一使用 usePermission().hasPermission(),严禁出现 role === 'xxx' 硬编码"URL 路径推断属于同类硬编码)
- **后果**:新增 parent/student 路由时跳转逻辑将出错URL 结构调整即破坏功能
#### 问题 10Server Action 入参未经验证
- **位置**`actions.ts` `getCoursePlansAction` 第 135-145 行params 未 Zod 验证)、`getGradeCoursePlanProgressAction` 第 269-284 行gradeId 仅检查非空)
- **违反规则**:项目规则 "输入使用 Zod 验证,验证失败返回结构化错误"
- **后果**:恶意参数可能绕过预期过滤条件
#### 问题 11死代码 — `reorderCoursePlanItems` 无消费方
- **位置**`data-access.ts` 第 290-313 行
- **问题**`reorderCoursePlanItems` 函数存在但无对应 Server Action、无 UI 调用方,架构图也未记录。
- **违反规则**:项目规则 "避免过度工程" + 架构图同步规则
- **后果**:死代码增加维护负担;架构图与实际不一致
### 2.5 错误处理与边界缺失P2
#### 问题 12无 Error Boundary / Suspense / 骨架屏
- **位置**:全部组件和页面
- **问题**:数据区块未用 React Error Boundary 包裹;异步加载无 Suspense + 骨架屏;空数据虽有基础 `EmptyState` 但无操作引导CTA
- **违反规则**:审计要求 "每个独立的数据区块必须用 React Error Boundary 包裹;异步数据使用 React Suspense + 骨架屏"
- **后果**:局部数据错误导致整页不可用;加载体验差
---
## 三、行业差距对比
基于 K12 教育管理系统(如 PowerSchool、Canvas、Schoology、钉钉教育、企业自建校管系统在课程计划/教学进度模块的主流实践,当前差距如下:
| 维度 | 行业优秀实践 | 当前实现 | 影响 |
|------|-------------|---------|------|
| **角色覆盖** | admin/teacher/parent/student 四角色均可查看课程计划(按权限脱敏) | 仅 admin/teacher 有路由parent/student 完全无入口 | 家长无法了解孩子本学期教学安排;学生无法预览学习进度 |
| **进度可视化** | 甘特图/时间轴展示周计划进度,颜色区分已完成/进行中/待开始 | 仅一个简单 Progress 条 + 表格列表 | 管理者难以一目了然掌握全年级教学进度 |
| **数据联动** | 周计划条目关联作业/考试/教材章节,可一键跳转 | `textbookChapter` 仅存文本,无关联跳转 | 教师需手动查找对应教材和作业 |
| **批量操作** | 批量标记完成、批量调整周次、批量复制计划到其他班级 | 无任何批量操作 | 管理员配置多班级计划时重复劳动 |
| **模板复用** | 提供标准课程计划模板,可从模板创建或复制历史计划 | 每次从零创建 | 教师重复录入 |
| **拖拽排序** | 周计划条目支持拖拽调整顺序 | data-access 有 `reorderCoursePlanItems` 但无 UI | 死代码,功能缺失 |
| **导出打印** | 导出 PDF/Excel 教学进度报告 | 无 | 无法线下归档或上报 |
| **空状态 CTA** | 空状态带"创建第一个计划"引导按钮 | 有 EmptyState 但无 CTA 按钮 | 新用户不知如何开始 |
| **骨架屏** | 加载时显示结构化骨架屏 | 无 loading.tsx | 加载白屏 |
| **日历视图** | 月历/周历视图展示教学安排 | 无 | 教师难以对照实际日期安排教学 |
---
## 四、改进优先级建议
### P0 — 安全与国际化(必须立即修复)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | 教师详情页信息泄露 | `getCoursePlanById` 增加 `userId` + `dataScope` 参数data-access 层过滤归属;非 admin 仅能查看自己负责的计划 |
| P0-2 | admin 页面无 requirePermission | admin 所有页面补充 `requirePermission(COURSE_PLAN_READ)` |
| P0-3 | data-access 无 DataScope 过滤 | `getCoursePlans`/`getCoursePlanById`/`getGradeCoursePlanProgress` 增加可选 `scope` 参数,按 classIds/teacherId 过滤 |
| P0-4 | i18n 严重缺失 | 提取全部硬编码文本到 `course-plans.json`,补全 zh-CN/en 翻译键状态标签、表单字段、按钮、空状态、Toast 消息等) |
### P1 — 架构合规与代码质量
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | 跨模块直接 JOIN | 定义 `CoursePlanDataService` 接口抽象 classes/subjects/users 数据依赖,通过组合注入;或先抽取 `getClassNameById`/`getSubjectNameById`/`getTeacherNameById` 轻量 data-access 调用替代 JOIN |
| P1-2 | 缺 loading.tsx/error.tsx | 为 admin 和 teacher 路由补充 loading.tsx骨架屏和 error.tsx错误边界 |
| P1-3 | 原生 `<a>` 标签 | 替换为 Next.js `<Link>` 组件 |
| P1-4 | `as` 断言 | 替换为类型守卫函数(`isValidStatus`/`isValidSemester` |
| P1-5 | URL 路径推断角色 | 改为通过 `usePermission().hasPermission()` 决定跳转基础路径,或由页面 props 传入 `successHref` |
| P1-6 | Action 入参未验证 | `getCoursePlansAction`/`getGradeCoursePlanProgressAction` 增加 Zod schema 验证 |
| P1-7 | 死代码 reorderCoursePlanItems | 删除或补充对应 Action + UI推荐补充拖拽排序 UI |
### P2 — 体验与企业级增强(中长期)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | 无 Error Boundary / 骨架屏 | 组件级 Error Boundary 包裹数据区块Suspense + 骨架屏 |
| P2-2 | parent/student 无路由 | 新增 parent/student 课程计划只读路由(按孩子班级过滤) |
| P2-3 | 无数据联动 | 周计划条目关联教材章节/作业,支持跳转 |
| P2-4 | 无批量操作 | 批量标记完成、批量复制计划 |
| P2-5 | 无模板复用 | 课程计划模板库,从模板创建 |
| P2-6 | 无导出 | PDF/Excel 导出教学进度报告 |
| P2-7 | 无日历视图 | 月历视图对照实际日期 |
| P2-8 | 监控埋点 | 预留 `trackCoursePlanEvent()` 埋点接口 |
---
## 五、架构图同步说明
本次审计发现架构图需补充/修改以下内容(✅ 已全部完成同步,含 P0/P1/P2 全部实施):
### 004_architecture_impact_map.md §2.18
1. **✅ 已补充导出函数**
- data-access`bulkUpdateItemCompleted``copyCoursePlanToClasses``enrichPlanRows`(名称解析解耦)、`buildScopeCondition`(权限过滤)
- actions`reorderCoursePlanItemsAction``bulkToggleItemsAction``copyCoursePlanAction``getTemplateCandidatesAction`P2-5 新增)、`trackCoursePlanEvent`
- lib`lib/export-utils.ts`P2-6 CSV 导出纯函数)、`lib/calendar-utils.ts`P2-7 日历视图纯函数 + 日期工具)
- types`CoursePlanQueryScope`、类型守卫 `isCoursePlanStatus`/`isCoursePlanSemester`、配置驱动 `ROLE_WIDGET_CONFIG``CalendarEvent`P2-7`CoursePlanExportColumnKey`/`CoursePlanColumnLabels`P2-6
- components新增 `SortableWeekRow`P1-7/P2-3`CoursePlanCalendar`P2-7`TemplatePickerDialog`P2-5
2. **✅ 已修正依赖关系描述**
- 旧描述 "依赖 classes/school合理" → 新描述 "通过动态 import `getClassNamesByIds`/`getSubjectNameMapByIds`/`getUserNamesByIds` 批量解析,不再直接 JOIN"
- `getSubjectOptions` 已移至 school 模块pages 改为从 `@/modules/school/data-access` 导入)
- 新增依赖:`shared/lib/export-utils.ts`P2-6`@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`P1-7
3. **✅ 已补充已知问题修复状态**P0-1 至 P1-7 + P2-1 至 P2-8 全部标记为已修复
4. **✅ 已补充页面路由表**10 个路由(含 P2-2 新增 parent/student 4 个路由)+ 权限 + loading/error 状态
5. **✅ 已更新文件清单行数**:反映重构后的实际行数(含新增 lib/ 与 components/ 文件)
### 005_architecture_data.json modules.`course-plans`
1. **✅ 已补充 actions 节点**`reorderCoursePlanItemsAction``bulkToggleItemsAction``copyCoursePlanAction``getTemplateCandidatesAction`P2-5
2. **✅ 已补充 dataAccess 节点**`bulkUpdateItemCompleted``copyCoursePlanToClasses`
3. **✅ 已移除 `getSubjectOptions`**:该函数已从 course-plans 模块删除,改用 school 模块
4. **✅ 已更新 `getCoursePlans`/`getCoursePlanById` 签名**:增加 `scope?: CoursePlanQueryScope` 参数
5. **✅ 已更新依赖关系**:移除 `shared.db.schema.classes/subjects/users`,改为动态 import 对方 data-access
6. **✅ 已补充 schemas**`GetCoursePlansParamsSchema``GradeIdSchema``ReorderItemsSchema``BulkToggleSchema``CopyPlanSchema`
7. **✅ 已补充 types**`CoursePlanQueryScope``GradeCoursePlanProgressItem``GradeCoursePlanProgressResult``isCoursePlanStatus``isCoursePlanSemester``CoursePlanWidgetId``RoleWidgetConfig``ROLE_WIDGET_CONFIG``CalendarEvent`P2-7`CoursePlanExportColumnKey`/`CoursePlanColumnLabels`P2-6
8. **✅ 已更新 components 描述**:反映 P1 + P2 全部修复内容Error Boundary、拖拽、数据联动、导出、日历、模板
9. **✅ 已补充 lib 节点**`export-utils.ts``calendar-utils.ts`P2 新增)
10. **✅ 已补充新增 components**`SortableWeekRow``CoursePlanCalendar``TemplatePickerDialog`
### shared/components/section-error-boundary.tsxP2-1 重构)
- **✅ 已重构**:类组件 + 函数式包装器双层结构
- 函数式包装器自动注入 i18n 文案(`{namespace}.error.boundaryTitle` / `boundaryDescription` / `retry`
- 支持 `fallback` 自定义降级 UI函数形式 `(error, reset) => ReactNode`
- 支持 `onError` 回调(用于埋点/监控AI 模块复用)
- a11y`role="alert"` + `aria-live="assertive"` + 重试按钮 `aria-label`
### shared/lib/export-utils.tsP2-6 新增)
- **✅ 已从 `dashboard/lib/export-utils.ts` 迁移至 shared 层**,供所有模块复用
- 导出:`toCSV``downloadFile``exportCSV``ExportRow``ExportColumn` 类型

View File

@@ -0,0 +1,320 @@
# Dashboard 模块 V4 审计报告
**审计日期**2026-06-22
**审计范围**`src/modules/dashboard/` + 所有 dashboard 路由文件 + parent dashboard 组件
**前置审计**
- v1P0 修复:跨模块 DB 查询、权限、i18n 容器组件)
- v210 个子组件 i18n、DashboardGreetingHeader 抽象、31 个纯函数单测、a11y 语义化标签)
- v3ContentRow 标签错配、admin/error.tsx i18n、空趋势数据空状态、loading/error.tsx 补齐、日期 locale、死代码清理、`as` 断言修复、流式架构 React `use()`
---
## 一、现有实现概要
### 1.1 文件分布
仪表盘模块位于 `src/modules/dashboard/`,包含 29 个文件:
| 层 | 文件 | 行数 | 职责 |
|----|------|------|------|
| actions | `actions.ts` | 167 | 4 个 Server Actionadmin/teacher/student/parent均调用 `requirePermission()` |
| data-access | `data-access.ts` | 49 | admin 仪表盘数据聚合(并行调用 6 个模块 stats 函数) |
| streams | `streams.ts` | 34 | admin 流式数据源(返回未解析 Promise 供 React `use()` 消费) |
| types | `types.ts` | 74 | AdminDashboardData / StudentDashboardProps / TeacherDashboardData |
| lib | `lib/dashboard-utils.ts` | 198 | 6 个纯函数weekday / 统计 / 排序 / 指标计算 / 问候语) |
| components | `dashboard-section.tsx` | 170 | Error Boundary + Suspense + 骨架屏5 种变体) |
| components | `dashboard-greeting-header.tsx` | 36 | 共享问候头部 |
| components | `dashboard-error-fallback.tsx` | 30 | 路由级错误回退 |
| components | `dashboard-loading-skeleton.tsx` | 44 | 路由级加载骨架 |
| admin-dashboard | `admin-dashboard.tsx` | 173 | 管理员视图(流式架构) |
| admin-dashboard | `admin-sections.tsx` | 231 | 管理员 6 个分区组件 |
| admin-dashboard | `user-growth-chart.tsx` | 65 | recharts 折线图 |
| teacher-dashboard | 9 文件 | ~700 | 教师仪表盘组件 |
| student-dashboard | 6 文件 | ~530 | 学生仪表盘组件 |
| tests | `dashboard-section.test.tsx` | 70 | Error Boundary + 骨架屏单测 |
| tests | `tests/integration/dashboard/dashboard-utils.test.ts` | 408 | 6 个纯函数 31 个单测 |
### 1.2 数据流
```
[Page] → [Action] → [requirePermission] → [data-access / 其他模块 data-access]
[lib/dashboard-utils 纯函数计算]
[View 组件] → [DashboardSection Suspense]
```
### 1.3 架构图记录完整性
架构影响地图004/005已覆盖 dashboard 模块的:
- 4 个 Server Action 签名、依赖、使用方
- 6 个纯函数签名和用途
- 依赖矩阵dependsOn: shared/auth/homework/classes
- 路由权限映射dashboardRoutePermissions
- 组件清单和行数
**遗漏**:架构图未记录 `streams.ts` 的流式数据源函数,也未记录 parent dashboard 组件实际位于 `modules/parent/components/` 的跨模块布局。
---
## 二、现存问题与原因分析
### P0 问题(严重)
#### P0-1`filterTodaySchedule` 仍使用 `as T[]` 类型断言
- **文件**`src/modules/dashboard/lib/dashboard-utils.ts`
- **行号**120
- **问题**v3 审计P1-8已识别此问题并改为泛型函数但实现仍保留 `as T[]` 断言:
```typescript
return schedule
.filter(...)
.sort(...)
.map((s) => ({ ... })) as T[] // ← 违反"禁止 as 断言"
```
- **违反规则**项目规则「TypeScript 严格模式:禁止 `as` 断言(除类型收窄外)」
- **后果**:类型系统被绕过,`map` 返回的对象结构若与 `T` 不匹配,编译器不会报错,潜在运行时错误
- **修复方向**:移除 `as T[]`,让 `map` 返回类型自然推导;或将映射逻辑提取为泛型映射函数
### P1 问题(高)
#### P1-1组件内嵌纯函数未抽取到 lib
- **文件**
- `teacher-schedule.tsx` 行 24-36`getStatus(start, end)` 计算课程状态
- `student-upcoming-assignments-card.tsx` 行 18`timeToMinutes(t)`
- `student-upcoming-assignments-card.tsx` 行 30-40`getDueUrgency(dueAt)`
- `student-upcoming-assignments-card.tsx` 行 18-28`getActionLabelKey(status)` / `getActionVariant(status)`
- `student-today-schedule-card.tsx` 行 17-20`timeToMinutes(t)`
- **问题**5 个纯函数散落在 3 个组件文件中,无法被单测覆盖,且 `timeToMinutes` 在两处重复定义
- **违反规则**:项目规则「可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离」
- **后果**:单测覆盖率无法提升;`timeToMinutes` 重复定义易产生不一致
- **修复方向**:全部迁移到 `lib/dashboard-utils.ts`,导出供组件调用,补充单测
#### P1-2`teacherName` 硬编码英文 fallback
- **文件**`src/modules/dashboard/actions.ts`
- **行号**85
- **问题**`teacherName: teacherProfile?.name ?? "Teacher"` — 当教师名称为空时 fallback 为硬编码英文 "Teacher",英文/中文用户都会看到英文
- **违反规则**:项目规则「所有用户可见文本必须适配 i18n」
- **后果**:中文用户在教师名称缺失时看到英文 "Teacher"i18n 不一致
- **修复方向**fallback 改为空字符串 `""`,由前端组件用 `t("title.teacher")` 处理空值
#### P1-3`teacher-schedule.tsx` 本地重复定义类型
- **文件**`src/modules/dashboard/components/teacher-dashboard/teacher-schedule.tsx`
- **行号**10-18
- **问题**:本地定义 `TeacherTodayScheduleItem` 类型,与 `types.ts` 中的同名类型结构完全相同,重复定义
- **违反规则**:项目规则「避免代码重复」
- **后果**:类型变更需同步两处,易产生不一致
- **修复方向**:从 `types.ts` 导入,删除本地定义
#### P1-4parent dashboard 组件位于 parent 模块而非 dashboard 模块
- **文件**`src/modules/parent/components/parent-dashboard.tsx`
- **问题**`ParentDashboard` 组件位于 parent 模块,但由 `dashboard/actions.getParentDashboardAction` 提供数据,且 `parent/dashboard/page.tsx` 同时导入两个模块的组件。架构图标注为"架构决策:保留在 parent 模块以避免移动文件破坏其他 import",但这造成模块边界模糊
- **违反规则**:项目规则「该模块必须作为独立功能单元」
- **后果**dashboard 模块不完整parent 仪表盘的 UI 逻辑分散在两个模块
- **修复方向**:将 `ParentDashboard` 组件迁移到 `modules/dashboard/components/parent-dashboard/`parent 模块仅保留数据访问
### P2 问题(中)
#### P2-1无数据服务接口抽象
- **文件**`src/modules/dashboard/actions.ts`、`data-access.ts`
- **问题**dashboard 模块直接 import 其他 6 个模块的 data-access 函数classes/homework/users/parent/textbooks/questions/exams无 TypeScript 接口抽象。组件层无法 mock 数据依赖,单测必须 mock 整个模块
- **违反规则**:项目规则「完全解耦:通过定义 TypeScript 接口抽象数据依赖」
- **后果**:模块耦合度高,难以独立测试,新增角色需修改 actions.ts
- **修复方向**:定义 `DashboardService` 接口,为每个角色提供实现类,通过 React Context 注入
#### P2-2无配置驱动的 Widget 渲染
- **文件**`admin-dashboard.tsx`、`teacher-dashboard-view.tsx`、`student-dashboard-view.tsx`
- **问题**:每个角色的仪表盘视图硬编码渲染哪些 Widget如 admin 渲染 StatsBar + QuickActions + TrendCharts + 3 Cards + RecentUsersTable。新增角色或调整 Widget 需修改视图组件代码
- **违反规则**:项目规则「可扩展性:采用配置驱动设计」
- **后果**扩展性差4 个角色视图代码结构相似但无法复用
- **修复方向**:定义 `DashboardWidgetConfig` 类型,通过配置决定渲染哪些 Widget 及其布局
#### P2-3无监控埋点接口
- **文件**:整个模块
- **问题**:无任何用户行为埋点(如 Widget 点击、页面停留、空状态触发等),无法度量仪表盘使用情况
- **违反规则**:项目规则「监控:方案中预留关键操作埋点接口」
- **后果**:无法度量仪表盘使用情况,无法指导优化
- **修复方向**:定义 `DashboardAnalytics` 接口在关键交互点调用Widget 点击、空状态触发、错误重试)
#### P2-4admin dashboard `userGrowth` 和 `homeworkTrend` 仍为占位空数组
- **文件**`src/modules/dashboard/data-access.ts`
- **行号**46-47
- **问题**v3 已为 `UserGrowthChart` 添加空状态,但数据源仍硬编码 `userGrowth: []` 和 `homeworkTrend: []`,趋势图表永远显示空状态
- **违反规则**:无直接违反,但影响用户体验
- **后果**:管理员无法看到用户增长和作业提交趋势
- **修复方向**:实现真实统计查询,或在 data-access 层添加 TODO 注释标记后续实现
#### P2-54 个角色 StatCard 使用模式不一致
- **文件**
- `admin-sections.tsx``StatCard` 直接传 `value`number
- `teacher-stats.tsx``StatCard` 传 `value={String(count)}` + `color` + `highlight`
- `student-stats-grid.tsx``StatCard` 传 `value={String(count)}` + `color` + `valueClassName` + 条件颜色
- **问题**3 个角色的 StatCard 调用模式不一致admin 不传 colorteacher 传 colorstudent 传 color + valueClassName
- **违反规则**:项目规则「最大化复用:识别四个角色共用的 UI 块」
- **后果**:视觉不一致,维护成本高
- **修复方向**:统一 StatCard 调用模式,通过配置驱动颜色和样式
### P3 问题(低)
#### P3-1无完整键盘导航支持
- **问题**:虽有 `aria-label` 属性,但 Widget 之间无 `tabindex` 管理,键盘用户无法按逻辑顺序遍历 Widget
- **修复方向**:为 Widget 容器添加 `role="region"` + `aria-label`,管理 `tabindex`
#### P3-2`AdminTrendCharts` 硬编码 `data={[]}`
- **文件**`admin-sections.tsx` 行 144、152
- **问题**`UserGrowthChart` 调用时传 `data={[]}`,与 P2-4 相关
- **修复方向**:从 `streams` 获取真实趋势数据
#### P3-3`teacher-todo-card.tsx` 排序逻辑仍可优化
- **文件**`teacher-todo-card.tsx` 行 52-56
- **问题**v3 已优化排序逻辑,但仍使用 `if (a.variant === "urgent") return -1` 模式,可进一步用优先级映射
- **修复方向**:定义 `VARIANT_PRIORITY` 映射,用数值比较
---
## 三、行业差距对比
### 3.1 与优秀 K12 产品的差距
| 维度 | 我们当前 | 钉钉教育/智学网/ClassIn | 差距影响 |
|------|---------|----------------------|---------|
| **数据联动** | 各 Widget 独立展示,无联动 | 点击统计卡片可下钻到详情页 | 管理员无法快速从概览定位问题 |
| **个性化配置** | 固定布局,用户无法自定义 | 支持拖拽 Widget、隐藏/显示 | 不同用户关注点不同,固定布局降低效率 |
| **实时更新** | 静态数据,需刷新页面 | WebSocket 实时推送待办数 | 待办数不实时,影响响应速度 |
| **多角色切换** | 通过权限路由到不同仪表盘 | 支持角色快速切换(如班主任+教师) | 多角色用户需退出重新登录 |
| **数据导出** | 无导出功能 | 支持导出 PDF/Excel | 管理员无法离线分析 |
| **通知集成** | 无通知集成 | 仪表盘集成待办通知 | 用户需切换页面查看通知 |
| **移动端适配** | 基本响应式,但 Widget 布局未优化 | 移动端优先设计,卡片堆叠 | 移动端体验不佳 |
### 3.2 缺失的关键功能
1. **Widget 下钻导航**:统计卡片点击应跳转到对应详情页(部分已实现,但不完整)
2. **时间范围筛选**admin 无法切换"今日/本周/本月"数据范围
3. **数据对比**:无法对比不同时间段数据(如本周 vs 上周)
4. **自定义仪表盘**:用户无法选择显示哪些 Widget
5. **通知中心集成**:仪表盘未集成通知下拉
---
## 四、改进优先级建议
### P0立即修复
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | `filterTodaySchedule` 的 `as T[]` 断言 | 移除断言,改用类型守卫或泛型映射 |
### P1高优先级
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | 组件内嵌纯函数未抽取 | 迁移 5 个纯函数到 `lib/dashboard-utils.ts`,补充单测 |
| P1-2 | `teacherName` 硬编码 fallback | 改为空字符串,前端用 i18n 处理 |
| P1-3 | `teacher-schedule.tsx` 本地类型重复 | 从 `types.ts` 导入 |
| P1-4 | parent dashboard 组件跨模块 | 迁移到 `modules/dashboard/components/parent-dashboard/` |
### P2中优先级 - 架构改进)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | 无数据服务接口抽象 | 定义 `DashboardService` 接口 + 角色实现 + Context 注入 |
| P2-2 | 无配置驱动 Widget 渲染 | 定义 `DashboardWidgetConfig`,配置驱动渲染 |
| P2-3 | 无监控埋点接口 | 定义 `DashboardAnalytics` 接口,预留埋点 |
| P2-4 | admin 趋势数据占位 | 添加 TODO 注释,标记后续实现 |
| P2-5 | StatCard 使用模式不一致 | 统一调用模式 |
### P3低优先级 - 长期优化)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P3-1 | 无完整键盘导航 | 添加 `role="region"` + `tabindex` |
| P3-2 | AdminTrendCharts 硬编码空数据 | 从 streams 获取真实数据 |
| P3-3 | TeacherTodoCard 排序优化 | 用优先级映射 |
### 中长期计划(不在本次实施范围)
| 编号 | 问题 | 改进方向 | 阶段 |
|------|------|---------|------|
| L1 | Widget 下钻导航 | 统计卡片点击跳转详情页 | 第二阶段 |
| L2 | 时间范围筛选 | admin 仪表盘添加时间选择器 | 第二阶段 |
| L3 | 数据对比 | 添加"本周 vs 上周"对比卡片 | 第三阶段 |
| L4 | 自定义仪表盘 | 用户可选择显示哪些 Widget | 第三阶段 |
| L5 | 通知中心集成 | 仪表盘集成通知下拉 | 第二阶段 |
| L6 | 实时更新 | WebSocket 推送待办数 | 第三阶段 |
| L7 | 数据导出 | 支持 PDF/Excel 导出 | 第三阶段 |
| L8 | 移动端优化 | Widget 移动端优先布局 | 第二阶段 |
---
## 五、架构图同步说明
### 需要补充/修改的节点
1. **`streams.ts`**:架构图未记录 `getAdminDashboardStreams` 函数,需在 004 的 dashboard 模块章节和 005 的 `modules.dashboard.exports` 中添加
2. **parent dashboard 组件位置**:架构图需注明 `ParentDashboard` 组件实际位于 `modules/parent/components/`,由 dashboard actions 提供数据
3. **新增 `DashboardService` 接口**(本次实施后):在 005 的 `modules.dashboard` 中添加 `services` 节点
4. **新增 `DashboardWidgetConfig` 类型**(本次实施后):在 005 的 `modules.dashboard.exports.types` 中添加
5. **新增 `DashboardAnalytics` 接口**(本次实施后):在 005 的 `modules.dashboard.exports.services` 中添加
---
## 六、本次实施计划
### 实施范围
本次完整实施 P0 + P1 + P2 + P3 + 中长期计划 L1-L8用户明确要求"包括中长期计划也要完整实施")。
### 实施步骤与完成状态
| 步骤 | 状态 | 说明 |
|------|------|------|
| P0-1修复 `filterTodaySchedule` 的 `as T[]` 断言 | ✅ 已完成 | 移除断言,改为非泛型函数 |
| P1-1抽取 5 个纯函数到 `lib/dashboard-utils.ts` | ✅ 已完成 | timeToMinutes/getScheduleStatus/getDueUrgency/getActionLabelKey/getActionVariant |
| P1-2修复 `teacherName` 硬编码 fallback | ✅ 已完成 | 改为空字符串,前端处理 |
| P1-3修复 `teacher-schedule.tsx` 本地类型重复 | ✅ 已完成 | 从 types.ts 导入 |
| P1-4迁移 parent dashboard 组件到 dashboard 模块 | ✅ 已完成 | 迁移至 components/parent-dashboard/,使用 slots 组合 |
| P2-1定义 `DashboardService` 接口 + Context 注入 | ✅ 已完成 | services/dashboard-service.tsx |
| P2-2定义 `DashboardWidgetConfig` 配置驱动渲染 | ✅ 已完成 | config/widget-configs.ts |
| P2-3定义 `DashboardAnalytics` 监控埋点接口 | ✅ 已完成 | services/dashboard-service.tsx |
| P2-4admin 趋势数据占位 TODO | ✅ 已完成 | data-access.ts 添加 TODO 注释 |
| P2-54 个角色 StatCard 使用模式统一 | ✅ 已完成 | 统一 color + valueClassName="tabular-nums" |
| P3-1完整键盘导航支持 | ✅ 已完成 | DashboardSection 新增 ariaLabel prop + role="region" + tabIndex |
| P3-2AdminTrendCharts 硬编码空数据 TODO | ✅ 已完成 | 添加 TODO 注释 |
| P3-3TeacherTodoCard 排序优化 | ✅ 已完成 | VARIANT_PRIORITY 数值映射 |
| L1Widget 下钻导航 | ✅ 已实施 | Admin StatCard 添加 hrefContentRow 支持可选 href |
| L2时间范围筛选 | ✅ 已实施 | DashboardTimeRangeFilter 组件 + URL search param 持久化 |
| L3数据对比 | ✅ 已实施 | ComparisonBadge 组件 + computeComparison 纯函数 |
| L4自定义仪表盘 | ✅ 已实施 | useDashboardPreferences Hook + localStorage 持久化 |
| L5通知中心集成 | ✅ 已实施 | DashboardNotificationWidget 组件 |
| L6实时更新 | ✅ 已实施 | useDashboardRealtime HookSSE + 指数退避重连) |
| L7数据导出 | ✅ 已实施 | lib/export-utils.tsCSV 导出 + 浏览器下载) |
| L8移动端优化 | ✅ 已实施 | DashboardResponsiveLayout / MobileSwipeContainer / DesktopGrid |
| 同步架构文档 004 和 005 | ✅ 已完成 | 所有新组件/函数/类型已记录 |
| 验证tsc + lint 零错误 | ✅ 已完成 | Dashboard 源码零错误(仅预存测试文件 screen 导入错误ESLint 零错误 |
### 新增文件清单
| 文件 | 类型 | 职责 |
|------|------|------|
| `services/dashboard-service.tsx` | Service | DashboardService 接口 + DashboardAnalytics 接口 + Context Provider |
| `config/widget-configs.ts` | Config | 4 个角色 Widget 布局配置 |
| `hooks/use-dashboard-preferences.ts` | Hook | 自定义仪表盘偏好L4 |
| `hooks/use-dashboard-realtime.ts` | Hook | SSE 实时更新L6 |
| `lib/export-utils.ts` | Lib | CSV 导出工具L7 |
| `components/parent-dashboard/parent-dashboard.tsx` | Component | 家长仪表盘视图P1-4 迁移) |
| `components/dashboard-time-range-filter.tsx` | Component | 时间范围筛选器L2 |
| `components/comparison-badge.tsx` | Component | 数据对比徽章L3 |
| `components/dashboard-notification-widget.tsx` | Component | 通知中心 WidgetL5 |
| `components/dashboard-responsive-layout.tsx` | Component | 移动端响应式布局L8 |

View File

@@ -0,0 +1,206 @@
# 数据库访问层重构专项 - 审计框架 v1
> 创建日期2026-07-07
> 目标:对全项目 86 个 `data-access*.ts` + ~30 个 `actions.ts` 进行深度审计,输出可执行的分级治理路线图
> 推进路径:先审计后治理(用户已确认)
> 执行方案:纯深读(用户已确认方案 B
---
## 一、审计范围
### 1.1 文件范围
| 类型 | 路径模式 | 文件数(约) | 备注 |
|---|---|---|---|
| 数据访问层 | `src/modules/**/data-access*.ts` | 86 | 主审计对象 |
| Server Actions | `src/modules/**/actions.ts` | ~30 | 辅查(权限校验、业务逻辑归属) |
| 辅助文件 | `src/modules/**/schema.ts``types.ts` | 按需 | 仅当 data-access 引用时查看 |
### 1.2 排除范围
- `src/app/**`:仅在 A-05 规则app 直访 DB触发时反向查看
- `src/shared/**`:仅在 A-07 规则shared 反向依赖)触发时查看
- 已有的模块级 audit 报告(`docs/architecture/audit/*-audit-report.md`):作为参考但不直接复用,因本次为横切关注点
### 1.3 模块分组(并行执行单元)
| 组 | 模块 | data-access 文件数 | sub-agent |
|---|---|---|---|
| **G1 核心教学 A** | lesson-preparation12+ questions + textbooks | ~16 | agent-1 |
| **G2 核心教学 B** | exams + homework7+ grades6+ diagnostic + adaptive-practice3 | ~21 | agent-2 |
| **G3 教学管理** | classes6+ school + scheduling + attendance3+ course-plans + proctoring | ~16 | agent-3 |
| **G4 用户与沟通** | users + messaging + notifications + parent + audit + auth + rbac3 | ~12 | agent-4 |
| **G5 扩展与设置** | elective5+ settings5+ dashboard + files + search + onboarding + ai + announcements + error-book3 | ~21 | agent-5 |
---
## 二、审计维度与检查规则
### 2.1 维度 1模式标准化Pattern Standardization
| 规则 ID | 检查项 | 期望状态 | 检测方式 |
|---|---|---|---|
| P-01 | `import "server-only"` 文件头 | 每个文件首行 | 静态 |
| P-02 | 类型导入使用 `import type` | 类型导入与值导入分离 | 静态 |
| P-03 | 读函数是否走 `cacheFn` 包装Raw + Wrapper 配对) | 全部覆盖 | 深读 |
| P-04 | 函数返回类型显式标注 `Promise<T>` | 无隐式推断 | 深读 |
| P-05 | 错误处理一致 | data-access 层用 throwactions 层用 ActionState | 深读 |
| P-06 | 分页参数命名统一 | `page`/`pageSize``limit`/`offset` 全局统一 | 深读 |
| P-07 | 日期序列化走 helper | `serializeDate`/`toISODateString` | 深读 |
| P-08 | 列表项映射走 `mapListItem` 模式 | 避免 inline mapping 重复 | 深读 |
| P-09 | `as` 断言出现次数 | 0除 unknown 收窄) | 静态 |
| P-10 | `any` 出现次数 | 0 | 静态 |
### 2.2 维度 2性能与查询优化Performance
| 规则 ID | 检查项 | 期望状态 | 检测方式 |
|---|---|---|---|
| F-01 | 循环内 SQL 调用N+1 | 改批量查询 + Map 解析 | 深读 |
| F-02 | `LIKE '%xxx%'` 全表扫描 | 改 FULLTEXT 或前缀匹配 | 深读 |
| F-03 | SELECT * 未指定列 | 显式列枚举 | 深读 |
| F-04 | JOIN 表数量 > 3 | 评估拆分或冗余字段 | 深读 |
| F-05 | 大表查询无 LIMIT | 添加默认 LIMIT | 深读 |
| F-06 | 重复查询同表/同条件 | 走 cacheFn 或合并查询 | 深读 |
| F-07 | 缺失索引(高频 WHERE 字段) | 提示加索引 | 深读 |
| F-08 | 跨模块多次调用 `getXxxNamesByIds` | 批量化 | 深读 |
| F-09 | 事务范围过大(含网络调用) | 收紧事务 | 深读 |
| F-10 | `count()` 全表统计无过滤 | 添加过滤条件 | 深读 |
### 2.3 维度 3架构违规治理Architecture
| 规则 ID | 检查项 | 期望状态 | 检测方式 |
|---|---|---|---|
| A-01 | data-access 含 `requirePermission` 调用 | 移至 actions | 静态 |
| A-02 | data-access 含业务逻辑(条件分支、状态机) | 移至 actions 或 lib | 深读 |
| A-03 | data-access 含 `"use server"` 标记 | 移至 actions | 静态 |
| A-04 | data-access 含 `revalidatePath` 调用 | 移至 actions | 静态 |
| A-05 | app/ 直接 import `@/shared/db` | 违规,改走 data-access | 静态 |
| A-06 | modules 间直接 import 对方 `@/shared/db/schema` 表 | 改走对方 data-access | 静态 |
| A-07 | shared/ 反向 import `@/auth`/`@/proxy`/`modules/*` | 违规 | 静态 |
| A-08 | actions.ts 漏调 `requirePermission` | 补齐 | 深读 |
| A-09 | actions.ts 含直接 DB 查询 | 移至 data-access | 深读 |
| A-10 | data-access 含 `console.log` 调试代码 | 删除 | 静态 |
### 2.4 维度 4结构与可维护性Structure
| 规则 ID | 检查项 | 期望状态 | 检测方式 |
|---|---|---|---|
| S-01 | 文件行数 > 800 行警告,> 1000 行必须拆分 | 拆分 | 静态 |
| S-02 | 单文件导出函数数 > 20 | 警告,考虑拆分 | 静态 |
| S-03 | 重复 helper多模块各自实现 serializeDate/buildScopeFilter 等) | 提取到 shared/lib | 深读 |
| S-04 | 过细拆分(同模块 ≥ 5 个子文件且单文件 < 100 行) | 评估合并 | 静态 |
| S-05 | 未使用导出dead code | 删除 | 深读 |
| S-06 | 公共导出函数缺 JSDoc | 补齐 | 深读 |
| S-07 | 跨模块重复查询逻辑 | 提取共享 data-access | 深读 |
| S-08 | 模块内 data-access 与 actions 职责混淆 | 重新分层 | 深读 |
---
## 三、严重性分级
| 级别 | 含义 | 示例 | 治理窗口 |
|---|---|---|---|
| **P0 Critical** | 架构硬违规、安全漏洞、必定性能问题 | app 直访 DB、跨模块 schema 直查、actions 漏权限、N+1 循环 SQL | 立即 |
| **P1 High** | 显著性能/可维护性问题 | 超长文件(>1000 行)、缺 cacheFn 的热路径读函数、LIKE 全表扫描 | Phase 1 |
| **P2 Medium** | 模式偏差、可优化 | 错误处理不一致、缺 JSDoc、重复 helper、分页命名不统一 | Phase 2 |
| **P3 Low** | 风格问题、可选优化 | 单行格式、import 顺序、注释措辞 | Phase 3 |
---
## 四、执行流程
### 4.1 阶段 Asub-agent 分组深读(并行)
每个 sub-agent 接收:
- 该组所有 `data-access*.ts` + 同模块 `actions.ts` 文件清单
- 完整规则表4 维度 × 38 条规则)
- 统一输出格式(见 4.3
每个 sub-agent 执行:
1. 完整读取每个文件(不使用 limit/offset
2. 按规则表逐条检测
3. 命中即记录到问题清单
4. 对每个问题给出修复建议与预估工作量
### 4.2 阶段 B主 agent 汇总
- 收集 5 个 sub-agent 的结构化输出
- 去重(同一问题被多 agent 命中时合并)
- 跨模块统计(如重复 helper 在多少模块出现)
- 生成优先级矩阵
- 编写治理路线图
### 4.3 sub-agent 输出格式
每个 sub-agent 产出 JSON 数组,每条问题:
```json
{
"id": "G1-001",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L123-L145",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "循环内调用 getClassNamesByIds",
"description": "在 for 循环内对每个 classId 单独查询 className应改为批量查询后用 Map 解析",
"recommendation": "提取 classIds 数组,一次调用 getClassNamesByIds(classIds),循环内改为 map.get(classId)",
"effort": "S (≤30 分钟)"
}
```
工作量分级:
- **XS**:≤ 15 分钟(如删除 console.log、补 import type
- **S**:≤ 30 分钟(如替换 as 断言为类型守卫)
- **M**:≤ 2 小时(如 N+1 改批量、提取 helper
- **L**:≤ 1 天(如拆分超长文件、跨模块重构)
- **XL**> 1 天(如架构层重构)
---
## 五、报告输出
### 5.1 主报告
文件:`docs/architecture/audit/data-access-audit-v1.md`
结构:
1. **执行摘要**总文件数、问题总数、P0/P1/P2/P3 分布、模块热度图
2. **量化指标仪表盘**cacheFn 覆盖率、平均行数、`as` 断言数、违规 import 数等
3. **按维度分组的问题清单**:每条含 文件:行号、规则 ID、严重性、现状描述、修复建议、预估工作量
4. **按模块分组的问题清单**:每个模块的累计问题数与 Top 问题
5. **P0-P3 优先级矩阵**:四象限图(影响 × 紧迫度)
6. **分阶段治理路线图**Phase 1 (P0) → Phase 2 (P1) → Phase 3 (P2) → Phase 4 (P3)
7. **附录**完整规则表、sub-agent 原始输出索引
### 5.2 结构化数据
文件:`docs/architecture/audit/data-access-audit-v1-data.json`
字段:`issues[]``metrics{}``moduleSummary{}``roadmap{}`
### 5.3 速查手册同步
发现的新模式问题需追加到 `docs/troubleshooting/known-issues.md`(速查手册格式)。
---
## 六、质量约束
- **零误报**:每条问题必须给出文件:行号 + 代码证据,避免臆测
- **零遗漏**86 个 data-access 文件必须全部深读,不得抽样
- **可执行**:每条修复建议必须具体到代码示例或操作步骤
- **不修改代码**:审计阶段只产出报告,不做任何源码修改
- **架构同步**审计过程中发现的架构图遗漏004/005 文档)记录到报告附录,治理阶段统一补图
---
## 七、后续衔接
审计报告 v1 完成后:
1. **用户审查报告**:确认问题清单与优先级
2. **制定治理路线图**:基于 P0-P3 分级,输出 `data-access-refactor-roadmap-v1.md`
3. **分阶段执行治理**:每阶段完成后运行 `npm run lint` + `npx tsc --noEmit` 验证
4. **同步架构文档**:每阶段完成后同步 004/005 文档与 known-issues.md

View File

@@ -0,0 +1,171 @@
{
"version": "v1",
"createdAt": "2026-07-07",
"scope": {
"dataAccessFiles": 86,
"actionsFilesAudited": 15,
"totalFilesAudited": 101
},
"summary": {
"totalIssues": 230,
"bySeverity": {
"P0": 17,
"P1": 48,
"P2": 105,
"P3": 60
},
"byDimension": {
"pattern": 54,
"performance": 71,
"architecture": 58,
"structure": 47
}
},
"metrics": {
"serverOnlyMissing": 2,
"cacheFnMissingEstimated": 60,
"filesOver800Lines": 3,
"filesOver1000Lines": 1,
"filesOver20Exports": 5,
"asAssertionsNonExempt": 2,
"anyUsage": 0,
"consoleErrorCount": 25,
"nPlusOnePatterns": 11,
"likeFullScanPatterns": 7,
"selectStarCount": 35,
"noLimitQueries": 18,
"crossModuleSchemaAccess": 7,
"businessLogicInDataAccess": 18,
"unprotectedTransactions": 4,
"actionsPermissionIssues": 4,
"actionsDirectDB": 1
},
"moduleHeatmap": [
{ "module": "messaging", "p0": 1, "p1": 4, "total": 5, "risk": "critical" },
{ "module": "classes", "p0": 1, "p1": 4, "total": 14, "risk": "critical" },
{ "module": "school", "p0": 0, "p1": 5, "total": 8, "risk": "critical" },
{ "module": "lesson-preparation", "p0": 1, "p1": 3, "total": 28, "risk": "high" },
{ "module": "scheduling", "p0": 1, "p1": 2, "total": 6, "risk": "high" },
{ "module": "adaptive-practice", "p0": 1, "p1": 2, "total": 4, "risk": "high" },
{ "module": "elective", "p0": 0, "p1": 3, "total": 7, "risk": "high" },
{ "module": "textbooks", "p0": 2, "p1": 1, "total": 11, "risk": "high" },
{ "module": "onboarding", "p0": 2, "p1": 0, "total": 2, "risk": "medium" },
{ "module": "grades", "p0": 0, "p1": 2, "total": 4, "risk": "medium" },
{ "module": "questions", "p0": 1, "p1": 1, "total": 11, "risk": "medium" },
{ "module": "audit", "p0": 1, "p1": 1, "total": 3, "risk": "medium" },
{ "module": "parent", "p0": 1, "p1": 0, "total": 1, "risk": "medium" },
{ "module": "attendance", "p0": 0, "p1": 1, "total": 9, "risk": "medium" },
{ "module": "files", "p0": 0, "p1": 4, "total": 13, "risk": "medium" },
{ "module": "exams", "p0": 1, "p1": 0, "total": 1, "risk": "low" },
{ "module": "course-plans", "p0": 1, "p1": 0, "total": 5, "risk": "low" },
{ "module": "homework", "p0": 0, "p1": 1, "total": 2, "risk": "low" },
{ "module": "diagnostic", "p0": 0, "p1": 1, "total": 2, "risk": "low" }
],
"p0Issues": [
{ "id": "G4-003", "file": "src/modules/audit/actions.ts", "lines": "L192-225", "ruleId": "A-08", "title": "purgeAuditLogsAction 用读权限执行物理删除", "category": "security" },
{ "id": "G4-002", "file": "src/modules/parent/", "lines": "—", "ruleId": "A-08", "title": "parent 模块缺失 actions.ts3 页面直访 data-access", "category": "security" },
{ "id": "G2-001", "file": "src/modules/exams/data-access.ts", "lines": "L1", "ruleId": "P-01", "title": "缺 import server-only", "category": "security" },
{ "id": "G5-001", "file": "src/modules/onboarding/data-access.ts", "lines": "L1", "ruleId": "P-01", "title": "缺 import server-only", "category": "security" },
{ "id": "G1-001", "file": "src/modules/textbooks/data-access-graph.ts", "lines": "L7-121", "ruleId": "A-06", "title": "直查 questions + diagnostic 模块表", "category": "architecture" },
{ "id": "G3-002", "file": "src/modules/scheduling/data-access.ts", "lines": "L8-17", "ruleId": "A-06", "title": "直查 classes/users/subjects 三模块表", "category": "architecture" },
{ "id": "G3-003", "file": "src/modules/scheduling/data-access-class-schedule.ts", "lines": "L28-158", "ruleId": "A-02", "title": "data-access 含校验+状态机业务逻辑", "category": "architecture" },
{ "id": "G5-002", "file": "src/modules/onboarding/actions.ts", "lines": "L15-76", "ruleId": "A-09", "title": "actions 直查 DB", "category": "architecture" },
{ "id": "G4-001", "file": "src/modules/messaging/data-access.ts", "lines": "L1-1089", "ruleId": "S-01", "title": "1089 行超 1000 硬限", "category": "structure" },
{ "id": "G1-002", "file": "src/modules/questions/data-access.ts", "lines": "L294-315", "ruleId": "F-01", "title": "deleteQuestionRecursive 递归 N+1", "category": "performance" },
{ "id": "G1-003", "file": "src/modules/questions/data-access.ts", "lines": "L350-378", "ruleId": "F-01", "title": "deleteQuestionsBatch 循环 N+1", "category": "performance" },
{ "id": "G1-004", "file": "src/modules/lesson-preparation/data-access-comments.ts", "lines": "L128-140", "ruleId": "F-01", "title": "deleteComment 递归 N+1", "category": "performance" },
{ "id": "G1-005", "file": "src/modules/textbooks/data-access.ts", "lines": "L426-458", "ruleId": "F-01", "title": "reorderChapters 循环 UPDATE", "category": "performance" },
{ "id": "G3-001", "file": "src/modules/classes/data-access.ts", "lines": "L17-313", "ruleId": "P-03", "title": "24+ 读函数未走 cacheFn", "category": "performance" },
{ "id": "G3-004", "file": "src/modules/classes/data-access-teacher.ts", "lines": "L92-116", "ruleId": "F-01", "title": "getTeacherClassesRaw 2N+1", "category": "performance" },
{ "id": "G3-005", "file": "src/modules/course-plans/data-access.ts", "lines": "L324-331", "ruleId": "F-01", "title": "reorderCoursePlanItems N+1 + 未包裹事务", "category": "performance" },
{ "id": "G2-003", "file": "src/modules/adaptive-practice/data-access-analytics.ts", "lines": "L311-384", "ruleId": "F-01", "title": "getTeacherClassPracticeOverviewsRaw 2N+1", "category": "performance" }
],
"roadmap": {
"phase0": {
"name": "紧急安全修复",
"priority": "immediate",
"tasks": [
{ "id": "G2-001", "effort": "XS", "action": "添加 import server-only 到 exams/data-access.ts" },
{ "id": "G5-001", "effort": "XS", "action": "添加 import server-only 到 onboarding/data-access.ts" },
{ "id": "G4-002", "effort": "M", "action": "新建 parent/actions.ts3 页面改调 Action" },
{ "id": "G4-003", "effort": "S", "action": "audit purge 权限点新增 + 替换" },
{ "id": "G4-004", "effort": "S", "action": "audit retention 权限点替换" },
{ "id": "G5-002", "effort": "S", "action": "onboarding/actions.ts 移除直查 DB" }
]
},
"phase1": {
"name": "P0 架构与性能修复",
"priority": "high",
"tasks": [
{ "batch": "1.1", "ids": ["G1-001", "G3-002", "G1-031", "G1-032", "G1-033"], "effort": "L", "action": "跨模块 schema 直查治理" },
{ "batch": "1.2", "ids": ["G4-001", "G4-005", "G4-006", "G4-007", "G4-008"], "effort": "L", "action": "messaging 拆分" },
{ "batch": "1.3", "ids": ["G1-002", "G1-003", "G1-004", "G1-005", "G3-001", "G3-004", "G3-005", "G2-003"], "effort": "L", "action": "N+1 热路径修复" },
{ "batch": "1.4", "ids": ["G3-003", "G3-024", "G3-025"], "effort": "M", "action": "scheduling 业务逻辑下移" },
{ "batch": "1.5", "ids": ["G5-003", "G5-004"], "effort": "L", "action": "elective 业务逻辑拆分" }
]
},
"phase2": {
"name": "P1 性能与结构优化",
"priority": "medium",
"tasks": [
{ "batch": "2.1", "ids": ["G1-006", "G1-007", "G1-008", "G1-009", "G3-017", "G4-010"], "effort": "L", "action": "LIKE 全表扫描治理" },
{ "batch": "2.2", "ids": ["G3-007", "G2-005"], "effort": "M", "action": "超长文件拆分" },
{ "batch": "2.3", "ids": ["G3-007", "G3-008", "G3-009", "G3-010", "G3-011"], "effort": "L", "action": "school 模块重构" },
{ "batch": "2.4", "ids": ["G5-005", "G5-006", "G5-007"], "effort": "M", "action": "files 模块错误处理重构" },
{ "batch": "2.5", "ids": ["G3-006", "G3-012", "G3-025", "G4-009"], "effort": "S", "action": "事务包裹修复" },
{ "batch": "2.6", "ids": ["G1-011", "G1-012", "G1-013", "G1-050", "G1-051", "G1-052", "G1-053", "G3-031", "G3-032", "G3-041", "G3-044"], "effort": "M", "action": "无 LIMIT 查询保护" }
]
},
"phase3": {
"name": "P2 模式标准化",
"priority": "low",
"tasks": [
{ "batch": "3.1", "ids": ["G1-021", "G1-022", "G1-023", "G1-024", "G3-001"], "effort": "M", "action": "cacheFn 全量补齐" },
{ "batch": "3.2", "ids": ["G1-039-049", "G3-009", "G3-026-028", "G5-007"], "effort": "M", "action": "SELECT * 改显式列" },
{ "batch": "3.3", "ids": ["G3-021", "G3-047", "G1-025"], "effort": "S", "action": "日期 helper 提取" },
{ "batch": "3.4", "ids": ["G1-026", "G1-027", "G3-020", "G3-036"], "effort": "M", "action": "重复 helper 提取" },
{ "batch": "3.5", "ids": ["G1-034-036", "G1-066", "G1-067", "G3-046"], "effort": "M", "action": "JSDoc 补齐" }
]
},
"phase4": {
"name": "P3 风格优化",
"priority": "optional",
"tasks": [
{ "ids": ["G3-029", "G3-030"], "effort": "XS", "action": "as widening 断言改类型标注" },
{ "ids": ["G1-060-065"], "effort": "XS", "action": "非空断言 ! 改类型守卫" },
{ "ids": ["G3-048"], "effort": "S", "action": "export * 改显式 re-export" },
{ "ids": ["G3-018"], "effort": "XS", "action": "死代码删除" }
]
}
},
"crossModuleRecommendations": {
"newSharedHelpers": [
{ "name": "toISODateString", "path": "src/shared/lib/date-utils.ts", "replaces": ["attendance/serializeDate", "scheduling/serializeDate", "school/toIso", "course-plans/toIso"] },
{ "name": "buildScopeFilter", "path": "src/shared/lib/scope-filter.ts", "replaces": ["attendance/buildScopeFilter", "grades/buildScopeFilter"] }
],
"newCrossModuleInterfaces": [
{ "name": "getActiveStudentIdsByClassIds", "module": "classes", "callers": ["adaptive-practice", "attendance"] },
{ "name": "getGradeNamesByIds", "module": "school", "callers": ["textbooks", "lesson-preparation"] },
{ "name": "getQuestionCountByKpIds", "module": "questions", "callers": ["textbooks"] },
{ "name": "getKpMasteryByTextbookId", "module": "diagnostic", "callers": ["textbooks"] }
],
"newPermissions": [
{ "name": "AUDIT_LOG_PURGE", "description": "审计日志物理删除", "roles": ["admin"] },
{ "name": "AUDIT_RETENTION_MANAGE", "description": "审计保留策略配置", "roles": ["admin"] }
]
},
"architectureDocGaps": [
"parent 模块缺失 actions.ts - 004 文档模块清单未标注",
"onboarding/actions.ts 直查 DB - 005 文档 dependencyMatrix 需修正",
"messaging/data-access.ts 拆分后 - 005 文档 modules.messaging.exports 需更新",
"新增权限点 AUDIT_LOG_PURGE / AUDIT_RETENTION_MANAGE - 005 文档 permissions 需补记",
"新增 shared/lib/date-utils.ts - 004/005 shared 模块清单需补记"
],
"sourceOutputs": [
"docs/architecture/audit/g1-audit-output.json",
"docs/architecture/audit/g2-data-access-audit.json",
"docs/architecture/audit/g3-audit-output.json",
"docs/architecture/audit/g4-audit-output.json",
"docs/architecture/audit/g5-audit-output.json"
]
}

View File

@@ -0,0 +1,525 @@
# 数据库访问层审计报告 v1
> 创建日期2026-07-07
> 审计范围86 个 `data-access*.ts` + ~30 个 `actions.ts`
> 审计方案纯深读5 个并行 sub-agent 全量扫描)
> 框架依据:[data-access-audit-framework-v1.md](./data-access-audit-framework-v1.md)
> 原始输出:[g1-audit-output.json](./g1-audit-output.json) · [g2-data-access-audit.json](./g2-data-access-audit.json) · [g3-audit-output.json](./g3-audit-output.json) · [g4-audit-output.json](./g4-audit-output.json) · [g5-audit-output.json](./g5-audit-output.json)
---
## 一、执行摘要
| 指标 | 数值 |
|---|---|
| 审计文件总数 | 10186 data-access + 15 actions 辅查) |
| 发现问题总数 | 230 |
| P0 Critical | 177.4% |
| P1 High | 4820.9% |
| P2 Medium | 10545.6% |
| P3 Low | 6026.1% |
### 1.1 模块热度图(按 P0+P1 数量降序)
| 模块 | P0 | P1 | P0+P1 | 总计 | 风险等级 |
|---|---|---|---|---|---|
| messaging | 1 | 4 | 5 | 5 | 🔴 极高 |
| classes | 1 | 4 | 5 | 14 | 🔴 极高 |
| school | 0 | 5 | 5 | 8 | 🔴 极高 |
| lesson-preparation | 1 | 3 | 4 | 28 | 🟠 高 |
| scheduling | 1 | 2 | 3 | 6 | 🟠 高 |
| adaptive-practice | 1 | 2 | 3 | 4 | 🟠 高 |
| elective | 0 | 3 | 3 | 7 | 🟠 高 |
| textbooks | 2 | 1 | 3 | 11 | 🟠 高 |
| onboarding | 2 | 0 | 2 | 2 | 🟡 中 |
| grades | 0 | 2 | 2 | 4 | 🟡 中 |
| questions | 1 | 1 | 2 | 11 | 🟡 中 |
| audit | 1 | 1 | 2 | 3 | 🟡 中 |
| parent | 1 | 0 | 1 | 1 | 🟡 中 |
| attendance | 0 | 1 | 1 | 9 | 🟡 中 |
| files | 0 | 4 | 4 | 13 | 🟡 中 |
| exams | 1 | 0 | 1 | 1 | 🟢 低 |
| course-plans | 1 | 0 | 1 | 5 | 🟢 低 |
| homework | 0 | 1 | 1 | 2 | 🟢 低 |
| diagnostic | 0 | 1 | 1 | 2 | 🟢 低 |
| 其他 (auth/rbac/notifications/dashboard/search/ai/announcements/error-book/proctoring/settings) | 0 | 0 | 0 | 0-3 | 🟢 低 |
### 1.2 维度分布
| 维度 | 问题数 | 占比 | P0 | P1 |
|---|---|---|---|---|
| 架构违规A-* | 58 | 25.2% | 6 | 18 |
| 性能优化F-* | 71 | 30.9% | 7 | 14 |
| 结构可维护性S-* | 47 | 20.4% | 2 | 11 |
| 模式标准化P-* | 54 | 23.5% | 2 | 5 |
---
## 二、量化指标仪表盘
| 指标 | 数值 | 备注 |
|---|---|---|
| `import "server-only"` 缺失文件 | 2 | exams/data-access.ts、onboarding/data-access.ts |
| cacheFn 未覆盖读函数(估算) | 60+ | 集中在 classes24+、questions5、textbooks3、lesson-preparation4 |
| 超长文件(>800 行) | 3 | messaging1089超硬限、school938、grades-analytics831 |
| 单文件导出函数 > 20 | 4 | messaging42+、classes/data-access.ts25+、school30+、questions28、textbooks35 |
| `as` 断言(非豁免) | 2 | classes/data-access-admin.ts、classes/data-access-teacher.tsDEFAULT_CLASS_SUBJECTS widening |
| `any` 使用 | 0 | 全部合规 |
| `console.error` 调试代码 | 25+ | school12、files12、classes3、course-plans2、audit9 |
| N+1 循环 SQLF-01 | 11 | 跨 4 组 |
| `LIKE '%xxx%'` 全表扫描 | 7 | lesson-preparation(4)、questions(1)、textbooks(1)、classes(1)、messaging(1) |
| SELECT * 未指定列 | 35+ | 跨 G116、G311、G58 |
| 无 LIMIT 大表查询 | 18 | 集中在 lesson-preparation |
| 跨模块直查 schema 表A-06 | 7 | textbooks-graph(2)、lesson-preparation(2)、questions(1)、scheduling(1)、announcements(1) |
| data-access 含业务逻辑A-02 | 18 | 集中在 scheduling、messaging、elective、classes |
| 未包裹事务的多步写F-09 | 4 | course-plans、classes、auth、school |
| actions 漏/错权限校验A-08 | 4 | parent缺失全部、auditpurge 用读权限、auditretention 用读权限) |
| actions 直查 DBA-09 | 1 | onboarding |
---
## 三、P0 Critical 问题清单17 条,必须立即治理)
### 3.1 安全漏洞类4 条)
| ID | 文件 | 问题 | 修复 |
|---|---|---|---|
| G4-003 | audit/actions.ts L192-225 | `purgeAuditLogsAction``AUDIT_LOG_READ`(读权限)执行物理删除,权限提权漏洞 | 新增 `AUDIT_LOG_PURGE` 权限点 |
| G4-002 | parent/ | 模块缺失 actions.ts3 个 app 页面直接 import data-access完全绕过 `requirePermission` | 新建 parent/actions.ts3 个页面改调 Action |
| G2-001 | exams/data-access.ts L1 | 缺 `import "server-only"`DB 逻辑可能泄露到客户端 bundle | 首行添加 `import "server-only"` |
| G5-001 | onboarding/data-access.ts L1 | 缺 `import "server-only"` | 首行添加 `import "server-only"` |
### 3.2 架构硬违规类5 条)
| ID | 文件 | 问题 | 修复 |
|---|---|---|---|
| G1-001 | textbooks/data-access-graph.ts L7-121 | 直查 questions 模块 `questionsToKnowledgePoints` 表 + diagnostic 模块 `knowledgePointMastery` 表 | 改调对方 data-access 跨模块接口 |
| G3-002 | scheduling/data-access.ts L8-17 | 直查 classes/users/subjects 三模块的 schema 表 | 改调 `getClassNamesByIds`/`getUserNamesByIds` 等 |
| G3-003 | scheduling/data-access-class-schedule.ts | data-access 含时间校验、归属校验、状态机判断 | 校验逻辑移至 actions |
| G5-002 | onboarding/actions.ts L15-76 | actions.ts 直接 `import { db }` 并查 `users` 表,违反三层架构 | data-access 新增 `getUserOnboardedAt`actions 改调 |
| G4-001 | messaging/data-access.ts L1-1089 | 单文件 1089 行超 1000 硬限8 类职责混合 | 拆分为 7 个 data-access-*.ts |
### 3.3 必定性能问题类8 条)
| ID | 文件 | 问题 | 修复 |
|---|---|---|---|
| G1-002 | questions/data-access.ts L294-315 | `deleteQuestionRecursive` 递归 N+1每子题单独查询+删除 | 收集后代 ID + `inArray` 批量删除 |
| G1-003 | questions/data-access.ts L350-378 | `deleteQuestionsBatch` 循环调用 `deleteQuestionRecursive` 产生 N×深度 查询 | 一次性收集所有后代 + 单次 `inArray` 删除 |
| G1-004 | lesson-preparation/data-access-comments.ts L128-140 | `deleteComment` 递归 N+1 | 单次查询构建 parent→children Map + 批量删除 |
| G1-005 | textbooks/data-access.ts L426-458 | `reorderChapters` 循环内逐条 UPDATE | `CASE WHEN` 批量更新 |
| G3-001 | classes/data-access.ts L17-313 | 24+ 读函数全部未走 cacheFn跨模块高频调用直连 DB | 补齐 Raw + Wrapper 配对 |
| G3-004 | classes/data-access-teacher.ts L92-116 | `getTeacherClassesRaw` 循环内对每班发起 2 次子查询2N+1 | 新增批量接口 |
| G3-005 | course-plans/data-access.ts L324-331 | `reorderCoursePlanItems` 循环内 N 次 UPDATE 且未包裹事务 | 事务 + `CASE WHEN` 批量更新 |
| G2-003 | adaptive-practice/data-access-analytics.ts L311-384 | `getTeacherClassPracticeOverviewsRaw` 对每班发起 2 条 SQL2N+1 | 批量查询 + groupBy |
---
## 四、P1 High 问题清单48 条Phase 1 治理)
### 4.1 性能类14 条)
| ID | 文件 | 规则 | 概要 |
|---|---|---|---|
| G1-006~009 | lesson-preparation/questions/textbooks | F-02 | 4 处 `LIKE '%xxx%'` 全表扫描课案标题、JSON content、题目 content、教材 4 字段) |
| G1-010 | lesson-preparation/data-access.ts L247-277 | F-04 | `getLessonPlansRaw` 5 表 LEFT JOIN |
| G1-011~013 | lesson-preparation (3 处) | F-05 | 列表查询无 LIMITgetLessonPlansRaw、getPendingReviewPlansRaw、getCalendarEventsRaw |
| G1-014~015 | lesson-preparation (2 处) | F-10 | 全表拉取后内存聚合统计 |
| G1-016 | lesson-preparation/data-access-analytics.ts L168-184 | F-06 | 5 次串行 COUNT 查询同表 |
| G1-017~018 | lesson-preparation (2 处) | F-01 | 拉全表后内存 filter |
| G1-019 | textbooks/actions.ts L396-398 | F-08 | 循环调用 `getGradeNameById`N 次 DB |
| G2-002 | grades/data-access-appeals.ts L122-151 | F-01 | `getPendingAppealsForReviewRaw` JS 层 filter 班级范围(潜在数据泄露) |
| G2-004 | adaptive-practice/data-access-analytics.ts L320-325 | F-08 | 循环内跨模块调用 `getActiveStudentIdsByClassId` |
| G3-006 | course-plans/data-access.ts L309-332 | F-09 | `reorderCoursePlanItems` 多次 UPDATE 未包裹事务 |
| G3-012 | school/data-access.ts L803-822 | F-09 | `promoteGrades` 循环 UPDATE 未包裹事务 |
| G3-017 | classes/data-access-students.ts L281-285 | F-02 | `LIKE '%xxx%'` 全表扫描 users.name/email |
| G3-022 | attendance/data-access-correlation.ts L46-193 | F-01/A-02 | 148 行业务编排逻辑(含跨模块调用) |
### 4.2 架构类11 条)
| ID | 文件 | 规则 | 概要 |
|---|---|---|---|
| G4-004 | audit/actions.ts L163-190 | A-08 | `saveAuditRetentionConfigAction` 用读权限执行写操作 |
| G4-006~008 | messaging/data-access.ts (3 处) | A-02 | 状态机/防重复业务逻辑嵌入 data-access |
| G3-007~008 | school/data-access.ts | S-01/S-02 | 938 行 + 30+ 导出函数 |
| G3-010 | school/data-access.ts | A-10 | 12 处 `console.error` 吞异常 |
| G3-011 | school/data-access.ts L246-408 | A-02 | 角色判断业务逻辑嵌入 data-access |
| G3-024~025 | classes/data-access-teacher.ts L284-439 | A-02/F-09 | `enrollTeacherByInvitationCode` 155 行状态机 + 未包裹事务 |
| G5-003 | elective/data-access-operations.ts | A-02 | 业务逻辑混淆抽签算法、冲突检测、i18n 通知) |
### 4.3 结构类5 条)
| ID | 文件 | 规则 | 概要 |
|---|---|---|---|
| G2-005 | grades/data-access-analytics.ts | S-01 | 831 行超 800 警告线 |
| G5-004 | elective/data-access-operations.ts L222-304 | S-08 | DB 写入与抽签算法混淆 |
| G5-005 | files/data-access.ts | A-10 | 12 处 `console.error` |
| G5-006 | files/data-access.ts | P-05 | try-catch 吞错误返回 null/[]/false |
| G4-009 | auth/data-access.ts | F-09 | `createUser` 两次 INSERT 无事务包裹 |
(完整 P1 清单详见各 sub-agent JSON 输出)
---
## 五、按维度分组的问题清单
### 5.1 模式标准化P-*54 条)
#### P-01 `import "server-only"` 缺失2 条 P0
| ID | 文件 | 修复 |
|---|---|---|
| G2-001 | exams/data-access.ts L1 | 首行添加 `import "server-only"` |
| G5-001 | onboarding/data-access.ts L1 | 首行添加 `import "server-only"` |
#### P-03 cacheFn 未覆盖30+ 条P2
集中模块:
- **classes/data-access.ts**24+ 读函数G3-001 P0
- **lesson-preparation**4 个G1-021
- **questions**5 个G1-023
- **textbooks**3 个G1-024
- **lesson-preparation-substitutes**1 个G1-022
修复模式:
```ts
// Before
export const getClassNamesByIds = async (classIds: string[]) => { /* SQL */ }
// After
export const getClassNamesByIdsRaw = async (classIds: string[]) => { /* SQL */ }
export const getClassNamesByIds = cacheFn(getClassNamesByIdsRaw, {
tags: ["classes:names"],
ttl: 300,
keyParts: ["classes", "getClassNamesByIds"],
})
```
#### P-05 错误处理不一致13 条P1-P2
集中模块files9 处 try-catch 吞错误、school12 处 console.error + 吞异常)
#### P-07 日期序列化 helper 重复5 处P2-P3
- attendance/data-access.ts `serializeDate`
- attendance/data-access-stats.ts `serializeDate`
- scheduling/data-access.ts `serializeDate`
- school/data-access.ts `toIso`
- course-plans/data-access.ts `toIso`/`toIsoRequired`
修复:提取到 `src/shared/lib/date-utils.ts`
#### P-09 `as` 断言2 条 P3非豁免
- classes/data-access-admin.ts L36 `DEFAULT_CLASS_SUBJECTS as readonly string[]`
- classes/data-access-teacher.ts L41 同上
#### P-10 `any` 使用
零违规,全部合规。
### 5.2 性能优化F-*71 条)
#### F-01 N+1 循环 SQL11 条,跨 P0/P1/P2
| ID | 文件 | 模式 |
|---|---|---|
| G1-002 | questions deleteQuestionRecursive | 递归内单独查询+删除 |
| G1-003 | questions deleteQuestionsBatch | 循环调用递归删除 |
| G1-004 | lesson-preparation deleteComment | 递归内单独查询+删除 |
| G1-005 | textbooks reorderChapters | 循环内逐条 UPDATE |
| G1-017 | lesson-preparation getSchedulesByDateRangeRaw | 拉全表后内存 filter |
| G1-018 | lesson-preparation getResponsesByStudentIdRaw | 拉全量后内存 filter |
| G2-002 | grades getPendingAppealsForReviewRaw | JS 层 filter 班级范围 |
| G2-003 | adaptive-practice getTeacherClassPracticeOverviewsRaw | Promise.all 内 2N+1 |
| G3-004 | classes getTeacherClassesRaw | 循环内 2 次子查询 |
| G3-005 | course-plans reorderCoursePlanItems | 循环内 N 次 UPDATE |
| G3-042 | classes generateUniqueInvitationCode | 循环内重试查询 |
#### F-02 `LIKE '%xxx%'` 全表扫描7 条 P1
| ID | 文件 | 字段 |
|---|---|---|
| G1-006 | lesson-preparation | lessonPlans.title |
| G1-007 | lesson-preparation-knowledge | lessonPlans.content (JSON) |
| G1-008 | questions | questions.content (JSON, +LOWER+CAST) |
| G1-009 | textbooks | title/subject/grade/publisher 4 字段 |
| G3-017 | classes-students | users.name/email |
| G4-010 | messaging | messages.subject/content |
修复策略:
- 短期:前缀匹配 `LIKE 'xxx%'`(可走索引)
- 中期FULLTEXT 索引 + `MATCH AGAINST IN BOOLEAN MODE`questions 表已实施,参见架构图 1.1.4
- 长期:关联表存储提取后的关系(如 lesson_plan_knowledge_point_refs
#### F-03 SELECT * 未指定列35+ 条 P2-P3
集中模块lesson-preparation16 处、school4 处、scheduling3 处、attendance2 处、course-plans7 处、files8 处)
#### F-05 无 LIMIT 大表查询18 条 P1-P2
集中模块lesson-preparation7 处、textbooks2 处、classes3 处、proctoring1 处)
#### F-09 事务范围问题4 条 P1
| ID | 文件 | 问题 |
|---|---|---|
| G3-006 | course-plans reorderCoursePlanItems | 多次 UPDATE 未包裹事务 |
| G3-012 | school promoteGrades | 循环 UPDATE 未包裹事务 |
| G3-025 | classes enrollTeacherByInvitationCode | 多次写操作未包裹事务 |
| G4-009 | auth createUser | 两次 INSERT 无事务包裹 |
#### F-10 全表 COUNT 无过滤4 条 P2
集中模块textbooks、questions、lesson-preparation、classes
### 5.3 架构违规A-*58 条)
#### A-02 data-access 含业务逻辑18 条 P0-P2
| 模块 | 文件 | 业务逻辑类型 |
|---|---|---|
| scheduling | data-access-class-schedule.ts | 时间校验 + 归属校验 + 状态机 |
| scheduling | data-access.ts | — |
| messaging | data-access.ts | 撤回状态机 + 防重复 + 页面编排 |
| elective | data-access-operations.ts | 抽签算法 + 冲突检测 + i18n 通知 |
| classes | data-access-teacher.ts | 邀请码状态机 + 角色校验 |
| classes | data-access-invitations.ts | 懒清理状态迁移 |
| school | data-access.ts | 角色判断 + 权限感知查询 |
| attendance | data-access-correlation.ts | 跨模块编排 + 成绩归一化 |
| attendance | data-access-stats.ts | 纯计算函数导出 |
| lesson-preparation | data-access-review.ts | 状态机迁移 |
| lesson-preparation | data-access-ai-evaluation.ts | 评分算法纯函数 |
| textbooks | data-access.ts | 重排序算法 |
| diagnostic | data-access.ts | 掌握度累积计算 |
| homework | data-access.ts | computeOverdueCount 闭包 |
#### A-06 跨模块直查 schema 表7 条 P0-P2
| ID | 文件 | 被查模块 |
|---|---|---|
| G1-001 | textbooks/data-access-graph.ts | questions + diagnostic |
| G1-031 | lesson-preparation/data-access.ts | textbooks (textbooks/chapters) |
| G1-032 | lesson-preparation/data-access-schedules.ts | classes |
| G1-033 | questions/data-access.ts | textbooks (knowledgePoints) |
| G3-002 | scheduling/data-access.ts | classes + users + subjects |
#### A-08 actions 权限校验问题4 条 P0-P1
| ID | 文件 | 问题 |
|---|---|---|
| G4-002 | parent/ | 模块缺失 actions.ts3 页面直访 data-access |
| G4-003 | audit/actions.ts | purge 用读权限 |
| G4-004 | audit/actions.ts | retention 配置用读权限 |
| G4-047 | rbac/data-access-assignments.ts | 内存 post-fetch 过滤导致 total 错误(伴随 A-02 |
#### A-09 actions 直查 DB1 条 P0
| ID | 文件 | 问题 |
|---|---|---|
| G5-002 | onboarding/actions.ts L15-76 | 直接 `import { db }` 并查 `users` 表 |
#### A-10 `console.error` 调试代码25+ 条 P1-P2
| 模块 | 文件 | 数量 |
|---|---|---|
| school | data-access.ts | 12 |
| files | data-access.ts | 12 |
| classes | data-access-teacher/students/admin | 3 |
| course-plans | data-access.ts | 2 |
| audit | data-access.ts | 9 |
### 5.4 结构与可维护性S-*47 条)
#### S-01 超长文件3 条 P0-P1
| ID | 文件 | 行数 | 状态 |
|---|---|---|---|
| G4-001 | messaging/data-access.ts | 1089 | 超 1000 硬限,必须拆分 |
| G3-007 | school/data-access.ts | 938 | 超 800 警告,接近硬限 |
| G2-005 | grades/data-access-analytics.ts | 831 | 超 800 警告 |
#### S-02 单文件导出函数过多4 条 P2
| 文件 | 导出数 |
|---|---|
| messaging/data-access.ts | 42+ |
| school/data-access.ts | 30+ |
| textbooks/data-access.ts | 35 |
| questions/data-access.ts | 28 |
| classes/data-access.ts | 25+ |
#### S-03 重复 helper8 条 P2
| helper | 出现模块 |
|---|---|
| serializeDate/toIso | attendance、scheduling、school、course-plans |
| toLessonPlanStatus | lesson-preparation2 文件) |
| isStringArray | lesson-preparation2 文件) |
| fetchClassesWithSubjects | classes2 函数 145+124 行重复) |
| fetchGradesWithHeads | school3 函数重复) |
#### S-06 缺 JSDoc15+ 条 P2-P3
集中模块lesson-preparationversions/templates、questions、textbooks
---
## 六、P0-P3 优先级矩阵
```
高影响
│ P0 立即治理 P1 Phase 1
│ ───────────────── ─────────────────
│ • parent 权限漏洞 • N+1 循环 SQL非热路径
│ • audit 权限提权 • LIKE 全表扫描
│ • server-only 缺失 • 超长文件school/grades
│ • 跨模块 schema 直查 • 业务逻辑嵌入 data-access
│ • N+1 循环 SQL热路径 • 事务未包裹
│ • messaging 超硬限 • console.error 吞异常
├──────────────────────────────────────────────
│ P2 Phase 2 P3 Phase 3
│ ───────────────── ─────────────────
│ • cacheFn 未覆盖 • as 断言widening
│ • SELECT * 未指定列 • 非空断言 !
│ • 无 LIMIT 大表查询 • JSDoc 补齐
│ • 重复 helper • 动态 import 注释
│ • 单文件导出过多 • export * 改显式
低影响
高紧迫 ─────────────────── 低紧迫
```
---
## 七、分阶段治理路线图
### Phase 0紧急安全修复XS-S立即执行
| 任务 | ID | 工作量 | 验证 |
|---|---|---|---|
| 添加 `import "server-only"` 到 exams/data-access.ts | G2-001 | XS | tsc + lint |
| 添加 `import "server-only"` 到 onboarding/data-access.ts | G5-001 | XS | tsc + lint |
| 新建 parent/actions.ts3 页面改调 Action | G4-002 | M | 手动测试 3 页面 |
| audit purge 权限点新增 + 替换 | G4-003 | S | 权限矩阵测试 |
| audit retention 权限点替换 | G4-004 | S | 权限矩阵测试 |
| onboarding/actions.ts 移除直查 DB | G5-002 | S | tsc + lint |
**Phase 0 完成标准**:所有 P0 安全漏洞修复,`npm run lint` + `npx tsc --noEmit` 零错误。
### Phase 1P0 架构与性能修复M-L1-2 周)
| 任务批次 | 涉及 ID | 工作量 | 依赖 |
|---|---|---|---|
| **1.1 跨模块 schema 直查治理** | G1-001, G3-002, G1-031~033 | L | 需在 questions/diagnostic/textbooks/classes 模块新增跨模块接口 |
| **1.2 messaging 拆分** | G4-001, G4-005~008 | L | 拆分为 7 个子文件 + 业务逻辑移至 actions |
| **1.3 N+1 热路径修复** | G1-002~005, G3-001, G3-004, G3-005, G2-003 | L | classes 补齐 cacheFn 是基础 |
| **1.4 scheduling 业务逻辑下移** | G3-003, G3-024, G3-025 | M | data-access-class-schedule.ts 重写 |
| **1.5 elective 业务逻辑拆分** | G5-003, G5-004 | L | 提取 lib/lottery.ts + lib/schedule-conflict.ts |
**Phase 1 完成标准**:所有 P0 修复,关键路径性能提升,架构分层清晰。
### Phase 2P1 性能与结构优化M-L2-3 周)
| 任务批次 | 涉及 ID | 工作量 |
|---|---|---|
| **2.1 LIKE 全表扫描治理** | G1-006~009, G3-017, G4-010 | LFULLTEXT 索引 + 查询重写) |
| **2.2 超长文件拆分** | G3-007, G2-005 | Mschool 按职责拆 8 文件、grades-analytics 按维度拆) |
| **2.3 school 模块重构** | G3-007~011 | L拆分 + 角色判断移至 actions + 删除 console.error |
| **2.4 files 模块错误处理重构** | G5-005, G5-006, G5-007 | M删除 try-catch + console.error |
| **2.5 事务包裹修复** | G3-006, G3-012, G3-025, G4-009 | S |
| **2.6 无 LIMIT 查询保护** | G1-011~013, G1-050~053, G3-031~032, G3-041, G3-044 | M |
**Phase 2 完成标准**:所有 P1 修复,无超长文件,无 LIKE 全表扫描,无未包裹事务。
### Phase 3P2 模式标准化S-M1-2 周)
| 任务批次 | 涉及 ID | 工作量 |
|---|---|---|
| **3.1 cacheFn 全量补齐** | G1-021~024, G3-001剩余 | M |
| **3.2 SELECT * 改显式列** | G1-039~049, G3-009, G3-026~028, G5-007 | M机械替换 |
| **3.3 日期 helper 提取** | G3-021, G3-047, G1-025 | S提取 shared/lib/date-utils.ts |
| **3.4 重复 helper 提取** | G1-026~027, G3-020, G3-036 | M |
| **3.5 JSDoc 补齐** | G1-034~036, G1-066~067, G3-046 | M |
**Phase 3 完成标准**:所有 P2 修复模式统一helper 集中到 shared/lib。
### Phase 4P3 风格优化XS按需
| 任务 | 涉及 ID | 工作量 |
|---|---|---|
| `as` widening 断言改类型标注 | G3-029~030 | XS |
| 非空断言 `!` 改类型守卫 | G1-060~065 | XS |
| `export *` 改显式 re-export | G3-048 | S |
| 死代码删除 | G3-018 | XS |
**Phase 4 完成标准**:零 `as`(非豁免)、零 `!`、零死代码。
---
## 八、跨模块治理建议
### 8.1 新增 shared/lib 公共 helper
| helper | 路径 | 用途 | 替代模块 |
|---|---|---|---|
| `toISODateString` | shared/lib/date-utils.ts | 日期序列化 | attendance/scheduling/school/course-plans |
| `buildScopeFilter` | shared/lib/scope-filter.ts | DataScope → SQL 过滤 | attendance/grades/homework 等重复实现 |
| `serializeDate` | (合并到 date-utils.ts | 同 toISODateString | — |
### 8.2 新增跨模块批量接口
| 接口 | 模块 | 用途 | 调用方 |
|---|---|---|---|
| `getActiveStudentIdsByClassIds(classIds)` | classes | 批量获取多班学生 ID | adaptive-practice、attendance |
| `getGradeNamesByIds(gradeIds)` | school | 批量获取年级名称 | textbooks、lesson-preparation |
| `getQuestionCountByKpIds(kpIds)` | questions | 知识点关联题目数 | textbooks |
| `getKpMasteryByTextbookId(textbookId)` | diagnostic | 教材下知识点掌握度 | textbooks |
### 8.3 新增权限点
| 权限点 | 用途 | 角色映射 |
|---|---|---|
| `AUDIT_LOG_PURGE` | 审计日志物理删除 | admin 专属 |
| `AUDIT_RETENTION_MANAGE` | 审计保留策略配置 | admin 专属 |
---
## 九、附录
### 9.1 完整规则表
见 [data-access-audit-framework-v1.md](./data-access-audit-framework-v1.md) 第二节。
### 9.2 sub-agent 原始输出索引
| 组 | 文件 | 问题数 |
|---|---|---|
| G1 | [g1-audit-output.json](./g1-audit-output.json) | 67 |
| G2 | [g2-data-access-audit.json](./g2-data-access-audit.json) | 11 |
| G3 | [g3-audit-output.json](./g3-audit-output.json) | 50 |
| G4 | [g4-audit-output.json](./g4-audit-output.json) | 61 |
| G5 | [g5-audit-output.json](./g5-audit-output.json) | 41 |
### 9.3 架构图遗漏记录
审计过程中发现的架构图004/005需补记项治理阶段统一补图
1. **parent 模块缺失 actions.ts** —— 004 文档模块清单未标注此异常
2. **onboarding/actions.ts 直查 DB** —— 005 文档 dependencyMatrix 需修正
3. **messaging/data-access.ts 拆分后** —— 005 文档 modules.messaging.exports 需更新
4. **新增权限点 AUDIT_LOG_PURGE / AUDIT_RETENTION_MANAGE** —— 005 文档 permissions 节点需补记
5. **新增 shared/lib/date-utils.ts** —— 004/005 shared 模块清单需补记
### 9.4 治理验证检查清单
每个 Phase 完成后必须通过:
- [ ] `npm run lint` 零错误
- [ ] `npx tsc --noEmit` 零错误
- [ ] 架构文档 004/005 同步更新
- [ ] `docs/troubleshooting/known-issues.md` 追加新模式
- [ ] 受影响模块的功能测试通过
- [ ] P0/P1 问题在 issues JSON 中标记为 resolved

View File

@@ -0,0 +1,395 @@
# 学情诊断Diagnostic模块审计报告 v2
> 审计日期2026-06-25
> 审计范围:`src/modules/diagnostic/**`、`src/app/(dashboard)/{teacher,student,parent}/diagnostic/**`
> 参照规则:`.trae/rules/project_rules.md`、`docs/architecture/004_architecture_impact_map.md` §2.22、`docs/architecture/005_architecture_data.json`
> 前置文档:
> - [diagnostic-audit-report.md](./diagnostic-audit-report.md)v12026-06-223 P0 + 5 P1 + 5 P2 全部完成)
> - [grades-diagnostic-audit-report-v4.md](./grades-diagnostic-audit-report-v4.md)v42026-06-2312 项 P1 全部完成)
---
## 一、现有实现概要
### 1.1 v1/v4 已完成项回顾
v1 与 v4 审计共完成 **25 项**改进3 P0 + 5 P1 + 5 P2 + 12 v4-P1涵盖跨模块 WidgetBoundary 提升到 shared、教师页面标题 i18n 化、教师 error.tsx i18n 化、as 断言消除、分享按钮移除、报告内容 i18n 驱动、Excel 导出 i18n 化、班级报告导出明细、角色配置驱动role-config.ts、年级诊断报告纵向切片、热力图键盘导航、DataScope 行级权限、师生关系校验、草稿隔离、通知机制、热力图图例、移动端表格滚动等。
### 1.2 当前文件分布v2 实测)
| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| 类型 | [types.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/types.ts) | 126 | DiagnosticReport / Mastery / Summary 类型定义(含 v4-P2-3 GradeMasterySummary |
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) | 519 | 掌握度查询 + 从提交/作业/成绩更新掌握度(含事务) |
| 数据访问 | [data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) | 323 | 诊断报告 CRUD + DataScope 过滤 + 结构化错误码 |
| 统计服务 | [stats-service.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts) | 506 | 14 个纯统计函数(含年级聚合) |
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) | 302 | 6 个 Action含年级生成 + 通知) |
| 校验 | [schema.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/schema.ts) | 39 | 5 个 Zod schema |
| 导出 | [export.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts) | 175 | Excel 导出(含班级明细 3 Sheet |
| 角色配置 | [role-config.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/role-config.ts) | 40 | 角色配置驱动v4-P2-2 |
| 组件 | [components/student-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx) | 299 | 学生诊断视图(概览+雷达+强弱项+报告+历史) |
| 组件 | [components/class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) | 449 | 班级诊断视图(热力图+筛选+排名+关注列表+生成) |
| 组件 | [components/report-list.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx) | 373 | 报告列表(过滤+表格+发布/删除/导出) |
| 组件 | [components/mastery-radar-chart.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx) | 85 | 雷达图封装 |
| 组件 | [components/confidence-utils.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/components/confidence-utils.ts) | 31 | 置信度计算 |
| 页面 | 4 个 `page.tsx` + 5 个 `loading.tsx` + 5 个 `error.tsx` | — | teacher/student/parent 三角色路由 |
| i18n | [zh-CN/diagnostic.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/diagnostic.json) + [en/diagnostic.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/en/diagnostic.json) | 252 / 同步 | 翻译文件 |
### 1.3 数据流
```
page.tsx (RSC)
├─ getStudentMasterySummary / getClassMasterySummary / getGradeMasterySummary / getKnowledgePointStats (data-access)
│ └─ db (drizzle) → knowledgePointMastery / knowledgePoints 表
│ └─ 跨模块 data-accessclasses / users / school / exams / homework / questions
├─ getDiagnosticReports (data-access-reports, 含 DataScope 过滤)
│ └─ db → learningDiagnosticReports 表
└─ <StudentDiagnosticView> / <ClassDiagnosticView> / <ReportList> (client)
└─ generateStudentReportAction / generateClassReportAction / generateGradeReportAction
/ publishReportAction / deleteReportAction / exportDiagnosticReportAction
/ getClassStudentsByKnowledgePointAction
```
### 1.4 架构图记录完整性
经核对 [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.22 与 [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) `modules.diagnostic` 节点,架构图对诊断模块的记录**基本完整**,已涵盖 v1/v4 全部修复。但本次 v2 审计发现以下偏差需后续同步:
- 架构图未记录 `getDiagnosticReports``grade_managed` scope 未过滤的已知缺陷v2-P1-1
- 架构图未记录 `confidence-utils.ts` 的置信度计算逻辑过于简化v2-P1-5
- 架构图未记录 3 个子路由 error.tsx 仍存在硬编码中文v2-P0-1
---
## 二、现存问题与原因分析
### 2.1 国际化
#### 问题 2.1.1 3 个子路由 error.tsx 硬编码中文P0
- **位置**
- [teacher/diagnostic/class/[classId]/error.tsx#L17-22](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/error.tsx#L17)`title="班级学情诊断加载失败"` `description="抱歉,加载班级诊断数据时发生了意外错误。请稍后重试。"` `label="重试"`
- [teacher/diagnostic/student/[studentId]/error.tsx#L17-22](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/error.tsx#L17)`title="学生学情诊断加载失败"` 同样硬编码
- [parent/diagnostic/error.tsx#L17-22](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/error.tsx#L17)`title="子女学情诊断加载失败"` 同样硬编码
- **现象**:三个 error.tsx 客户端组件未使用 `useTranslations`,全部硬编码中文文案。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"。
- **原因**v1 审计 P0-3 仅修复了 `teacher/diagnostic/error.tsx`(教师报告列表页),遗漏了教师子路由和学生/家长子路由的 error.tsx。
- **后果**:英文环境下这三个错误页显示中文,与系统其他已 i18n 化的错误页风格不一致。
#### 问题 2.1.2 i18n 标签与代码逻辑不一致P1
- **位置**
- i18n[zh-CN/diagnostic.json#L78](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/diagnostic.json#L78)`"weaknesses": { "title": "弱项(<60%" }`
- 代码:[stats-service.ts#L83-99](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts#L83)`classifyStrengthsWeaknesses` 中弱项阈值为 `< 80`P3-16 修复:消除 60-79 盲区)
- **现象**i18n 标签显示"弱项(<60%",但代码实际将掌握度 < 80 的知识点都归类为弱项。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"——文本需与逻辑一致。
- **原因**P3-16 修复弱项分类阈值时未同步更新 i18n 标签。
- **后果**:用户看到"弱项(<60%"标签,但实际列表包含 60-79% 的知识点,造成认知混乱。
#### 问题 2.1.3 死 i18n 键未清理P2
- **位置**[zh-CN/diagnostic.json#L157-165](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/diagnostic.json#L157)
- **现象**`reportList.share``reportList.shareAriaLabel``reportList.shareTitle``reportList.shareDescription``reportList.shareLinkLabel``reportList.copyLink``reportList.copyLinkSuccess``reportList.copyLinkFailed``reportList.shareLinkAriaLabel` 共 9 个键仍保留在翻译文件中,但 v1-P1-3 已移除分享按钮,这些键不再被引用。
- **违反规则**:项目规则精神——保持代码与配置一致,避免死代码。
- **原因**:移除分享按钮时未清理对应的 i18n 键。
- **后果**:翻译文件臃肿,维护成本增加;新增语言时需翻译无用的键。
#### 问题 2.1.4 热力图 aria-label 硬编码中文标点格式P1
- **位置**[class-diagnostic-view.tsx#L195](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx#L195)
- **现象**`aria-label={`${kp.knowledgePointName}${kp.averageMastery.toFixed(1)}%${levelLabel}${kp.masteredCount}/${kp.totalStudents}`}` 使用硬编码中文全角冒号""和逗号""。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **原因**aria-label 拼接时未使用 i18n 模板。
- **后果**:英文环境下屏幕阅读器读出中文标点,影响无障碍体验。
#### 问题 2.1.5 export.ts 错误消息硬编码英文P2
- **位置**[export.ts#L28](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts#L28)`throw new Error("Report not found")`
- **现象**:导出报告不存在时抛出硬编码英文错误。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **后果**:中文环境下用户看到英文错误消息。
### 2.2 权限与安全
#### 问题 2.2.1 grade_managed DataScope 未过滤P1安全漏洞
- **位置**[data-access-reports.ts#L220-238](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts#L220)
- **现象**`getDiagnosticReports` 仅处理 `children``class_taught` 两种 DataScope`grade_managed`(年级主任)和 `class_members`学生scope 不做任何过滤。代码注释明确写道:`// grade_managed 需要跨模块查询年级学生,由调用方自行过滤`
- **违反规则**:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤Server Action 二次校验"。
- **原因**grade_managed scope 需要跨模块查询年级学生 ID通过 `getUserIdsByGradeId`),实现时为避免跨模块依赖未在 data-access 层完成过滤。
- **后果**年级主任grade_head / teaching_head角色调用时`getDiagnosticReports` 返回全校所有学生的诊断报告,存在数据越权风险。虽然当前 teacher/diagnostic/page.tsx 在客户端对 class_members 做了二次过滤,但 grade_managed 完全未过滤。
#### 问题 2.2.2 教师报告列表页客户端过滤 class_membersP2
- **位置**[teacher/diagnostic/page.tsx#L56-59](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx#L56)
- **现象**`const visibleReports = ctx.dataScope.type === "class_members" ? reports.reports.filter((r) => r.studentId === ctx.userId) : reports.reports`
- **违反规则**:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"——客户端过滤不安全。
- **原因**:教师页面理论上不应被学生角色访问,但代码保留了 class_members 分支作为防御性过滤。这种过滤应在 data-access 层完成。
- **后果**:虽然不影响功能(学生不会访问教师路由),但违背了"数据过滤在 data-access 层"的原则,且 data-access 已返回了不该返回的数据。
### 2.3 架构解耦
#### 问题 2.3.1 组件直接 import actions无服务接口抽象P1
- **位置**
- [report-list.tsx#L41](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx#L41)`import { publishReportAction, deleteReportAction, exportDiagnosticReportAction } from "../actions"`
- [class-diagnostic-view.tsx#L33](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx#L33)`import { generateClassReportAction, getClassStudentsByKnowledgePointAction } from "../actions"`
- **现象**:客户端组件直接 import 并调用 Server Actions未通过接口抽象或依赖注入。
- **违反规则**:项目规则"通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access只能通过注入的接口调用"。
- **原因**v1 审计将此项列为"后续建议"未实施,但用户在本次审计中将其升级为强制要求。
- **后果**:组件无法独立测试(测试时必须 mock 整个 actions 模块);无法在不修改组件代码的情况下替换 actions 实现;组件与 Server Action 实现紧耦合。
### 2.4 错误处理与边界
#### 问题 2.4.1 无 Error Boundary 包裹独立数据区块P1
- **位置**
- [student-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx):概览卡片、雷达图、强弱项、报告、历史列表均在同一组件内,无 Error Boundary 隔离。
- [class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx):概览、热力图、筛选、排名、关注列表、生成报告均在同一组件内。
- **现象**:仅页面级有 error.tsx 错误边界,组件内部各数据区块无独立 Error Boundary。
- **违反规则**:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
- **原因**v4-P2 已将 WidgetBoundary 提升到 shared 层并用于页面级包裹,但未下沉到组件内部各数据区块。
- **后果**:雷达图渲染失败会导致整个诊断页面崩溃;热力图数据异常会波及排名表和关注列表。
#### 问题 2.4.2 异步数据无 Suspense + 骨架屏P2
- **位置**:所有页面均使用 `await Promise.all` 一次性获取所有数据后传给客户端组件。
- **现象**:未使用 React Suspense 流式渲染,数据获取完成前整个页面阻塞。
- **违反规则**:项目规则"异步数据使用 React Suspense + 骨架屏"。
- **后果**:首屏白屏时间长;无法渐进式展示数据区块。
### 2.5 可测试性与置信度
#### 问题 2.5.1 置信度计算过于简化P1
- **位置**[confidence-utils.ts#L18-21](file:///e:/Desktop/CICD/src/modules/diagnostic/components/confidence-utils.ts#L18)
- **现象**`getConfidenceLevel` 仅判断 `overallScore === null` 返回 "insufficient",否则一律返回 "high"。注释承认"后续可扩展为基于 totalQuestions 等数据量字段的多级判断",但未实施。
- **违反规则**:项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离"——置信度计算虽已提取为纯函数,但逻辑不完整。
- **原因**v4-P3-7 引入置信度时为简化首版,未实现多级判断。
- **后果**:仅做了 1 道题的报告也显示"高置信度",误导教师判断报告可信度。
#### 问题 2.5.2 stats-service 14 个纯函数无单测P2
- **位置**[stats-service.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts)
- **现象**14 个纯函数(`computeAverageMastery``classifyStrengthsWeaknesses``aggregateClassMastery` 等)已正确提取为纯函数,但无对应的单元测试文件。
- **违反规则**:项目规则"可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。
- **后果**统计数据计算逻辑变更无回归保障P3-16 弱项阈值修复这类边界 case 无法自动验证。
### 2.6 可复用性与扩展性
#### 问题 2.6.1 雷达图知识点名称截断无 tooltipP2
- **位置**[mastery-radar-chart.tsx#L22-26](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx#L22)
- **现象**`shortName: d.knowledgePoint.length > 8 ? ${d.knowledgePoint.slice(0, 8)}... : d.knowledgePoint` 截断超过 8 字符的知识点名称,但未提供 tooltip 显示完整名称。
- **违反规则**:项目规则"明确处理空数据、无权限、网络异常等边界状态"——信息截断是边界状态。
- **后果**:长名称知识点被截断,用户无法查看完整名称,影响数据理解。
#### 问题 2.6.2 通知类型使用 "grade" 而非专用类型P2
- **位置**[actions.ts#L157, L173](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts#L157)
- **现象**`createNotification({ type: "grade", ... })` 诊断报告发布通知复用了成绩通知类型 "grade"。
- **违反规则**:项目规则精神——类型应语义准确,便于分类与过滤。
- **后果**:学生无法区分"成绩通知"和"诊断报告通知";通知筛选功能无法按类型精确过滤诊断报告。
#### 问题 2.6.3 无监控埋点接口P2
- **位置**:整个模块无任何监控/埋点代码。
- **现象**:报告生成、发布、删除、导出等关键操作无埋点。
- **违反规则**:项目规则"监控:方案中预留关键操作埋点接口"。
- **后果**:无法追踪诊断报告的使用情况;无法度量报告生成/发布转化率;异常无法主动发现。
### 2.7 parent 页面细节
#### 问题 2.7.1 parent/diagnostic/page.tsx noRecordsTitle 与 noRecordsDescription 重复P2
- **位置**[parent/diagnostic/page.tsx#L97-98](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/page.tsx#L97)
- **现象**`noRecordsTitle={t("parent.noReports")}``noRecordsDescription={t("parent.noReports")}` 使用同一个 i18n 键。
- **原因**:复制粘贴时未区分标题和描述。
- **后果**空状态时标题和描述显示相同文本UI 不够精细。
---
## 三、行业差距对比
对标 PowerSchool、Infinite Campus、Skyward、Alma、智学网、班级小管家、超星学习通、ClassIn 等 K12 系统,本模块在 v1/v4 修复后仍有以下差距:
| 维度 | 优秀实践 | 本模块现状 | 影响 |
|------|---------|-----------|------|
| **掌握度时间线** | PowerSchool/智学网支持掌握度时间线,展示知识点掌握度随时间的变化趋势,支持按知识点钻取历史 | 仅展示当前快照,`knowledgePointMastery` 表无历史版本,`lastAssessedAt` 仅记录最近一次 | 教师无法判断学生是否在进步或退步;无法评估教学干预效果 |
| **个性化学习路径** | Alma/智学网基于弱项推荐具体学习资源(题目、视频、文档),形成学习路径 | 仅提供练习按钮跳转题目库,无资源推荐算法 | 推荐不够精准,学生需自行筛选练习内容 |
| **学生×知识点掌握度矩阵** | Infinite Campus/Alma 提供学生×知识点矩阵热力图,支持点击单元格查看明细 | 班级视图仅有知识点聚合热力图,无学生维度矩阵 | 教师无法快速定位"哪个学生在哪个知识点上薄弱" |
| **预测性分析at-risk 预警)** | PowerSchool/Infinite Campus 基于历史数据预测 at-risk 学生,提前干预 | 无预测模型,仅基于当前掌握度 <60 判定需关注 | 无法主动预警,错失早期干预窗口 |
| **报告模板自定义** | PowerSchool 支持学校自定义报告模板推荐话术、评分区间、logo | 报告内容固定由 stats-service 生成,仅 i18n 可切换语言 | 无法按学校需求定制报告风格 |
| **多维度诊断** | Infinite Campus 结合成绩+出勤+行为做多维综合诊断 | 仅基于知识点掌握度单一维度 | 诊断维度单一,无法反映学生综合学习状态 |
| **PDF 导出** | 所有对标系统支持 PDF 导出(便于打印分发) | 仅支持 Excel 导出 | 无法满足打印分发场景 |
| **数据置信度可视化** | Alma 在报告上标注数据量与置信度,帮助教师判断结论可靠性 | 置信度计算过于简化(仅 null/非 null 两级) | 教师无法判断报告可信度 |
| **流式渲染** | 现代 K12 系统使用 React Suspense 流式渲染,首屏快速可见 | 全部 `await Promise.all` 阻塞渲染 | 首屏白屏时间长 |
---
## 四、改进优先级建议
### P0紧急影响核心规范或安全
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| v2-P0-1 | 3 个子路由 error.tsx 硬编码中文 | 接入 `useTranslations("diagnostic")`,新增对应 i18n 键 |
### P1重要影响安全/解耦/一致性)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| v2-P1-1 | grade_managed DataScope 未过滤 | 在 data-access-reports.ts 中处理 grade_managed scope调用 `getUserIdsByGradeId` 过滤 |
| v2-P1-2 | i18n 弱项标签与代码逻辑不一致 | 更新 i18n 标签为"弱项(<80%" |
| v2-P1-3 | 热力图 aria-label 硬编码中文标点 | 使用 i18n 模板键 `heatmapCellAriaLabel` |
| v2-P1-4 | 组件直接 import actions 无接口抽象 | 定义 `DiagnosticService` 接口,通过 React Context 注入;组件通过 `useDiagnosticService()` 获取 |
| v2-P1-5 | 置信度计算过于简化 | 基于 `totalQuestions` 实现多级置信度(<5 insufficient / 5-15 low / 16-30 medium / >30 high |
| v2-P1-6 | 无 Error Boundary 包裹独立数据区块 | 在概览、雷达图、强弱项、报告、历史等区块外包裹 WidgetBoundary |
### P2增强提升完整性与企业级能力
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| v2-P2-1 | 死 i18n 键未清理 | 移除 9 个 share 相关 i18n 键 |
| v2-P2-2 | 教师页面客户端过滤 class_members | 在 data-access 层过滤,移除客户端 filter |
| v2-P2-3 | export.ts 错误消息硬编码 | 改用 DiagnosticReportError 结构化错误码 |
| v2-P2-4 | 异步数据无 Suspense 流式渲染 | 拆分数据获取为独立 async 组件,使用 Suspense 包裹(中长期) |
| v2-P2-5 | 雷达图名称截断无 tooltip | 为截断的名称添加 Tooltip 显示完整名称 |
| v2-P2-6 | 通知类型使用 "grade" | 新增 "diagnostic" 通知类型(需 notifications 模块配合) |
| v2-P2-7 | 无监控埋点接口 | 定义 `DiagnosticMonitor` 接口,在关键操作处调用 |
| v2-P2-8 | stats-service 无单测 | 为 14 个纯函数补充单元测试 |
| v2-P2-9 | parent noRecordsTitle/Description 重复 | 区分标题和描述 i18n 键 |
### P3长期需较大投入
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| v2-P3-1 | 掌握度时间线 | 新增 `knowledgePointMasteryHistory` 表记录历史版本,前端展示时间线图表 |
| v2-P3-2 | 个性化学习路径推荐 | 基于弱项推荐具体学习资源 |
| v2-P3-3 | 学生×知识点掌握度矩阵 | 新增矩阵视图组件 |
| v2-P3-4 | 预测性分析 | 基于 historical mastery 训练 at-risk 预测模型 |
| v2-P3-5 | 报告模板自定义 | 允许学校配置报告模板 |
| v2-P3-6 | PDF 导出 | 新增 PDF 导出能力 |
---
## 五、架构图同步说明
本次 v2 审计发现架构图需同步以下内容:
### 004_architecture_impact_map.md §2.22
1. **已知问题新增**
- 记录 v2-P0-1 三个子路由 error.tsx 硬编码中文及修复
- 记录 v2-P1-1 grade_managed DataScope 未过滤及修复
- 记录 v2-P1-4 组件解耦 Context 注入
- 记录 v2-P1-5 置信度计算改进
- 记录 v2-P1-6 数据区块 Error Boundary 包裹
2. **依赖关系更新**:标注 diagnostic 模块新增 `DiagnosticServiceContext` 依赖注入机制
### 005_architecture_data.json
1. `modules.diagnostic.exports` 补充 `DiagnosticService` 接口、`DiagnosticServiceProvider``useDiagnosticService` hook
2. `modules.diagnostic.knownIssues` 新增 v2 系列问题记录
3. `modules.diagnostic.dependencies` 标注 grade_managed scope 现已调用 `getUserIdsByGradeId` 过滤
---
## 六、实施计划
### 6.1 本次实施范围P0 + P1 + 可快速完成的 P2
本次实施 **P0 全部 + P1 全部 + 6 项 P2**P3 长期项记录备查不实施。
### 6.2 重构方案设计(满足强制原则)
#### 完全解耦
定义 TypeScript 接口 `DiagnosticService` 抽象所有数据依赖:
```typescript
// src/modules/diagnostic/services/diagnostic-service.ts
export interface DiagnosticService {
generateStudentReport(studentId: string, period: string): Promise<string>
generateClassReport(classId: string, period: string): Promise<string>
generateGradeReport(gradeId: string, period: string): Promise<string>
publishReport(id: string): Promise<void>
deleteReport(id: string): Promise<void>
exportReport(reportId: string): Promise<{ buffer: string; filename: string }>
getClassStudentsByKp(classId: string, kpId: string, threshold?: number): Promise<...>
}
```
通过 React Context 注入:
```typescript
// src/modules/diagnostic/services/diagnostic-service-context.tsx
const DiagnosticServiceContext = createContext<DiagnosticService | null>(null)
export function DiagnosticServiceProvider({ service, children }) { ... }
export function useDiagnosticService(): DiagnosticService { ... }
```
默认实现绑定现有 Server Actions测试时可注入 mock 实现。
#### 组合优先
- `StudentDiagnosticView``ClassDiagnosticView``ReportList` 通过 `children` / slots 组合子区块。
- 逻辑复用提取为 `useDiagnosticActions` hook内部调用 `useDiagnosticService()`)。
#### 国际化就绪
- 所有新增文本使用 `diagnostic.*` 命名空间翻译键。
- aria-label 模板使用 i18n 占位符:`heatmapCellAriaLabel: "{name}{level}%{label}{mastered}/{total}"`
#### 最大化复用
- `DiagnosticSection` 通用区块组件(含 WidgetBoundary + Suspense + 标题 + 内容 slot
- `useDiagnosticActions` hook 统一封装 actions 调用 + toast + router.refresh。
#### 错误与边界处理
- 每个数据区块外包裹 `WidgetBoundary`
- 异步子区块使用 `Suspense` + 骨架屏(本次仅在页面级流式拆分,组件级 Suspense 列入 P2-4 长期)。
#### 可测试性
- `DiagnosticService` 接口清晰,可 mock。
- `confidence-utils` 置信度计算改为基于 `totalQuestions` 的多级判断。
#### 可扩展性
- 角色差异通过 `role-config.ts` 配置驱动v4-P2-2 已实现)。
- 通知类型、监控埋点通过接口预留扩展点。
#### 企业级补充
- a11y热力图 aria-label i18n 化;雷达图 tooltip。
- 性能:保持 RSC 获取初始数据。
- 安全grade_managed scope 在 data-access 层过滤。
- 监控:定义 `DiagnosticMonitor` 接口预留埋点。
### 6.3 翻译文件结构示例
```json
{
"diagnostic": {
"error": {
"classLoadFailed": "班级学情诊断加载失败",
"classLoadFailedDesc": "抱歉,加载班级诊断数据时发生了意外错误。请稍后重试。",
"studentLoadFailed": "学生学情诊断加载失败",
"studentLoadFailedDesc": "抱歉,加载学生诊断数据时发生了意外错误。请稍后重试。",
"parentLoadFailed": "子女学情诊断加载失败",
"parentLoadFailedDesc": "抱歉,加载子女诊断数据时发生了意外错误。请稍后重试。",
"retry": "重试"
},
"weaknesses": {
"title": "弱项(<80%"
},
"classDiagnostic": {
"heatmapCellAriaLabel": "{name}{level}%{label}{mastered}/{total}"
},
"reportList": {
"radarPointTooltip": "{fullName}"
}
}
}
```

View File

@@ -0,0 +1,301 @@
# 学情诊断Diagnostic模块审计报告
> 审计日期2026-06-22
> 审计范围:`src/modules/diagnostic/**`、`src/app/(dashboard)/teacher/diagnostic/**`、`src/app/(dashboard)/student/diagnostic/**`、`src/app/(dashboard)/parent/diagnostic/**`
> 参照规则:`docs/architecture/004_architecture_impact_map.md` §2.22、`docs/architecture/005_architecture_data.json`、`.trae/rules/project_rules.md`
> 前置文档:[grades-diagnostic-audit-report-v4.md](./grades-diagnostic-audit-report-v4.md)v4 已完成 12 项 P1 数据安全修复)
---
## 一、现有实现概要
### 1.1 文件分布
| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| 类型 | [types.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/types.ts) | 109 | DiagnosticReport / Mastery / Summary 类型定义 |
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) | 477 | 掌握度查询 + 从提交/成绩更新掌握度 |
| 数据访问 | [data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) | 256 | 诊断报告 CRUD + DataScope 过滤 |
| 统计服务 | [stats-service.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts) | 388 | 12 个纯统计函数 |
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) | 274 | 5 个 Action生成/发布/删除/导出/按知识点筛选) |
| 校验 | [schema.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/schema.ts) | 31 | 4 个 Zod schema |
| 导出 | [export.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts) | 122 | Excel 导出 |
| 组件 | [components/class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) | 448 | 班级诊断视图(热力图+筛选+排名+关注列表+生成) |
| 组件 | [components/student-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx) | 293 | 学生诊断视图(概览+雷达+强弱项+报告+历史) |
| 组件 | [components/report-list.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx) | 445 | 报告列表(过滤+表格+发布/删除/导出/分享) |
| 组件 | [components/mastery-radar-chart.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx) | 85 | 雷达图封装 |
| 组件 | [components/confidence-utils.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/components/confidence-utils.ts) | 31 | 置信度计算 |
| 页面 | [teacher/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx) | 67 | 教师报告列表页 |
| 页面 | [teacher/diagnostic/student/[studentId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx) | 86 | 教师查看学生诊断 |
| 页面 | [teacher/diagnostic/class/[classId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx) | 50 | 教师班级诊断 |
| 页面 | [student/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/diagnostic/page.tsx) | 40 | 学生自我诊断 |
| 页面 | [parent/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/page.tsx) | 128 | 家长多子女诊断 |
| 骨架屏 | 5 个 `loading.tsx` | — | 各路由骨架屏 |
| 错误边界 | 5 个 `error.tsx` | — | 各路由错误边界 |
| i18n | [diagnostic.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/diagnostic.json) | 204 | 中文翻译 |
### 1.2 数据流
```
page.tsx (RSC)
├─ getStudentMasterySummary / getClassMasterySummary / getKnowledgePointStats (data-access)
│ └─ db (drizzle) → knowledgePointMastery / knowledgePoints 表
├─ getDiagnosticReports (data-access-reports, 含 DataScope 过滤)
│ └─ db → learningDiagnosticReports 表
└─ <StudentDiagnosticView> / <ClassDiagnosticView> / <ReportList> (client)
└─ generateStudentReportAction / generateClassReportAction / publishReportAction / deleteReportAction / exportDiagnosticReportAction / getClassStudentsByKnowledgePointAction
```
### 1.3 架构图记录完整性
经核对 [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.22 与 [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json),架构图对诊断模块的记录**基本完整**,但存在以下偏差:
- 行数统计略有滞后:图记 `data-access.ts 179 行`,实际为 477 行(含 `updateMasteryFromHomeworkSubmission``updateMasteryFromExamScore` 两个大函数)。
- 未记录 `export.ts` 的存在(架构图文件清单缺少此文件)。
- 未记录 `confidence-utils.ts` 组件文件。
- 未记录跨模块 UI 依赖:`teacher/diagnostic/student/[studentId]/page.tsx``teacher/diagnostic/class/[classId]/page.tsx` 直接 import `@/modules/grades/components/widget-boundary`,架构图未标注此跨模块 UI 依赖。
---
## 二、现存问题与原因分析
### 2.1 架构解耦
#### 问题 2.1.1 跨模块直接 import UI 组件 WidgetBoundaryP0
- **位置**
- [teacher/diagnostic/student/[studentId]/page.tsx#L13](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx#L13)
- [teacher/diagnostic/class/[classId]/page.tsx#L8](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx#L8)
- **现象**`import { WidgetBoundary } from "@/modules/grades/components/widget-boundary"`
- **违反规则**:项目规则"模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表"的精神延伸——UI 组件跨模块直接 import 同样破坏模块独立性。WidgetBoundary 是通用错误边界组件,不应属于 grades 业务模块。
- **原因**WidgetBoundary 最初为 grades 模块创建diagnostic 模块复用时直接 import 了 grades 模块的实现,而非将其提升到 shared 层。
- **后果**grades 模块对 WidgetBoundary 的任何变更重命名、删除、props 修改)都会破坏 diagnostic 模块编译diagnostic 模块无法独立测试、独立部署。
#### 问题 2.1.2 组件直接 import actions无服务接口抽象P1
- **位置**
- [components/report-list.tsx#L42](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx#L42)`import { publishReportAction, deleteReportAction, exportDiagnosticReportAction } from "../actions"`
- [components/class-diagnostic-view.tsx#L33](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx#L33)`import { generateClassReportAction, getClassStudentsByKnowledgePointAction } from "../actions"`
- **现象**:客户端组件直接 import 并调用 Server Actions未通过接口抽象或依赖注入。
- **违反规则**:项目规则"通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。
- **原因**:模块未采用依赖注入模式,组件与 actions 紧耦合。
- **后果**:组件无法独立测试(测试时必须 mock 整个 actions 模块);无法在不修改组件代码的情况下替换 actions 实现。
### 2.2 国际化
#### 问题 2.2.1 教师页面标题硬编码英文P0
- **位置**
- [teacher/diagnostic/page.tsx#L59-62](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx#L59)`<h1>Learning Diagnostic</h1>` + `<p>View and manage diagnostic reports based on knowledge point mastery.</p>`
- [teacher/diagnostic/student/[studentId]/page.tsx#L70-74](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx#L70)`Student Diagnostic` + `Knowledge point mastery analysis and diagnostic reports.`
- [teacher/diagnostic/class/[classId]/page.tsx#L38-42](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx#L38)`Class Diagnostic` + `Class-level knowledge point mastery overview and student attention list.`
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"。
- **原因**:教师端 3 个页面未使用 `getTranslations` 获取翻译,直接硬编码英文文案。学生端和家端已正确使用 i18n。
- **后果**:中文环境下教师看到英文标题,与系统其他页面风格不一致。
#### 问题 2.2.2 teacher/diagnostic/error.tsx 硬编码中文P0
- **位置**[teacher/diagnostic/error.tsx#L17-22](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/error.tsx#L17)
- **现象**`title="学情诊断页面加载失败"` `description="抱歉,页面加载时发生了意外错误。请稍后重试。"` `label="重试"` 全部硬编码。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **原因**error.tsx 是客户端组件但未使用 `useTranslations`
- **后果**:英文环境下错误页显示中文,国际化不一致。对比 student/diagnostic/error.tsx 已正确使用 i18n。
#### 问题 2.2.3 parent/diagnostic/page.tsx 错误卡片中英文混用P1
- **位置**[parent/diagnostic/page.tsx#L116-119](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/page.tsx#L116)
- **现象**`{t("error.loadFailed")} for {item.studentName}.``Please refresh the page or contact the school administrator if the problem persists.` 混用 i18n key 和硬编码英文。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **后果**:中文环境下显示"加载失败 for 张三.",中英文混杂,用户体验差。
#### 问题 2.2.4 报告内容硬编码中文P1
- **位置**[stats-service.ts#L280-353](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts#L280)
- **现象**`buildStudentReportContent``buildClassReportContent` 生成中文报告内容,如 `"建议复习「${m.knowledgePointName}」知识点"``"学生 ${summary.studentName} 在 ${period} 期间整体掌握度"` 等。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **原因**:纯函数层生成报告内容时直接硬编码中文,未通过 i18n。
- **后果**:英文环境下生成的诊断报告内容为中文,无法国际化。报告内容存储在数据库中,已生成的历史报告无法回溯翻译。
#### 问题 2.2.5 Excel 导出表头硬编码中文P1
- **位置**[export.ts#L40-99](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts#L40)
- **现象**Excel 表头如 `"学生姓名"``"报告周期"``"综合得分"``"知识点掌握度"` 等硬编码中文;文件名 `诊断报告_${safePeriod}_${formatDateForFile()}.xlsx` 也硬编码。
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
- **后果**:英文环境下导出的 Excel 文件表头和文件名为中文。
### 2.3 类型安全
#### 问题 2.3.1 as 类型断言P1
- **位置**[teacher/diagnostic/page.tsx#L24, L28](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx#L24)
- **现象**`(v as DiagnosticReportType)``(v as DiagnosticReportStatus)` 使用 as 断言。
- **违反规则**:项目规则"禁止 as 断言(除非从 unknown 转换或测试中,需注释原因)"。
- **原因**:虽有类型守卫 `VALID_REPORT_TYPES.has(v)` 校验,但转换时使用了 as 而非类型守卫函数返回值收窄。
- **后果**:绕过 TypeScript 严格类型检查,潜在类型不安全。
### 2.4 错误处理与边界
#### 问题 2.4.1 分享链接指向不存在的路由P1
- **位置**[components/report-list.tsx#L157, L208](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx#L157)
- **现象**`const url = \`${window.location.origin}/teacher/diagnostic/reports/${shareId}\`` 指向 `/teacher/diagnostic/reports/[id]` 路由,但该路由在项目中不存在(无对应 page.tsx
- **原因**:分享功能开发时未创建对应路由页面。
- **后果**:用户点击分享链接后得到 404 页面,功能不可用。
#### 问题 2.4.2 班级报告导出缺少明细P2
- **位置**[export.ts#L85-113](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts#L85)
- **现象**:班级报告仅导出概览 Sheet缺少知识点统计和需关注学生明细。代码注释明确说明"班级报告的 studentId 为 null需要从 period 反查 classId 不现实"。
- **原因**v4-P1 已为 `learningDiagnosticReports` 表新增 `classId` 字段,但 export.ts 未同步更新使用该字段查询班级明细。
- **后果**:教师导出班级报告时只能看到概览,无法获取知识点统计和需关注学生列表,导出功能不完整。
### 2.5 可复用性与配置驱动
#### 问题 2.5.1 角色差异通过 props 硬编码而非配置驱动P2
- **位置**[components/student-diagnostic-view.tsx#L32](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx#L32)
- **现象**`practiceHrefBase` prop 区分角色(学生默认 `/student/learning/assignments`,教师传 `/teacher/questions`,家长传 `null`)。
- **违反规则**:项目规则"采用配置驱动设计,例如通过角色配置决定该模块渲染哪些 Widget/子模块"。
- **原因**:角色差异通过 props 传递,而非通过角色配置对象统一管理。
- **后果**:新增角色需修改组件 props 传递逻辑,而非仅修改配置。
#### 问题 2.5.2 无年级诊断报告生成入口P2
- **位置**[types.ts#L3](file:///e:/Desktop/CICD/src/modules/diagnostic/types.ts#L3)
- **现象**`DiagnosticReportType` 定义了 `"grade"` 类型,但 [actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) 无 `generateGradeReportAction`[data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) 无 `generateGradeDiagnosticReport` 函数。
- **原因**:年级报告功能定义了类型但未实现。
- **后果**管理员无法生成年级级别的诊断报告功能不完整。report-list 过滤器中可选"年级"类型但永远无数据。
### 2.6 可访问性
#### 问题 2.6.1 热力图色块缺少键盘导航P2
- **位置**[components/class-diagnostic-view.tsx#L190-201](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx#L190)
- **现象**:热力图色块为 `<div>` 且仅有 `role="img"`,无 `tabIndex` 和键盘焦点样式,键盘用户无法逐个聚焦查看详情。
- **违反规则**:项目规则"可访问性a11y语义化标签、ARIA 属性、键盘导航"。
- **后果**:键盘用户无法通过 Tab 遍历热力图色块查看 tooltip/title 详情。
---
## 三、行业差距对比
对标 PowerSchool、Infinite Campus、Skyward、Alma、智学网、班级小管家等 K12 系统,本模块在以下方面存在差距:
| 维度 | 优秀实践 | 本模块现状 | 影响 |
|------|---------|-----------|------|
| **诊断趋势分析** | PowerSchool/智学网支持掌握度时间线,展示知识点掌握度随时间的变化趋势 | 仅展示当前快照,无历史趋势对比 | 教师无法判断学生是否在进步或退步 |
| **年级诊断报告** | Infinite Campus 支持年级级别诊断,对比班级间差异 | 类型已定义但无实现入口 | 管理员无法做年级层面决策 |
| **报告详情页** | 所有同类系统都有独立的报告详情页,支持分享链接 | 分享链接指向不存在的路由 | 分享功能不可用 |
| **班级报告导出明细** | PowerSchool/Infinite Campus 导出含知识点统计+学生列表 | 班级报告仅导出概览 | 教师无法离线分析 |
| **掌握度时间线** | 智学网展示每个知识点的掌握度变化曲线 | 无时间维度数据 | 无法评估教学干预效果 |
| **个性化学习路径** | Alma/智学网基于弱项推荐学习路径和资源 | 仅提供练习按钮跳转题目库 | 推荐不够精准 |
| **诊断报告模板** | PowerSchool 支持自定义报告模板 | 报告内容固定硬编码 | 无法按学校需求定制 |
| **多维度诊断** | Infinite Campus 结合成绩+出勤+行为做多维诊断 | 仅基于知识点掌握度 | 诊断维度单一 |
---
## 四、改进优先级建议
### P0紧急影响功能正确性或核心规范
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | 跨模块 import WidgetBoundary | 将 WidgetBoundary 提升到 `shared/components/`diagnostic 和 grades 模块统一从 shared 引用 |
| P0-2 | 教师页面标题硬编码英文 | 3 个教师页面使用 `getTranslations("diagnostic")` 获取标题和描述 |
| P0-3 | teacher/diagnostic/error.tsx 硬编码中文 | 接入 `useTranslations("diagnostic")`,与其他 error.tsx 一致 |
### P1重要影响用户体验或类型安全
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | parent 错误卡片中英文混用 | 提取完整 i18n 键,消除硬编码英文 |
| P1-2 | as 类型断言 | 改用类型守卫函数返回值收窄,消除 as |
| P1-3 | 分享链接指向不存在路由 | 移除分享功能或创建对应路由页面。鉴于当前无报告详情页需求,移除分享按钮避免 404 |
| P1-4 | 报告内容硬编码中文 | stats-service 的报告内容生成改为接收 i18n 翻译函数参数,或在 actions 层调用时注入翻译后的模板 |
| P1-5 | Excel 导出表头硬编码 | export.ts 接收 i18n 翻译参数,表头和文件名使用翻译键 |
### P2增强提升完整性
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | 班级报告导出缺少明细 | 利用 v4-P1 新增的 classId 字段查询班级掌握度,导出知识点统计+需关注学生 Sheet |
| P2-2 | 角色差异通过 props 硬编码 | 定义角色配置对象,通过配置驱动 practiceHrefBase 等角色差异 |
| P2-3 | 无年级诊断报告入口 | 实现 generateGradeDiagnosticReport + 对应 Action中长期 |
| P2-4 | 热力图色块缺少键盘导航 | 添加 tabIndex={0} 和 focus-visible 样式 |
| P2-5 | 架构图行数统计滞后 | 同步 data-access.ts 实际行数,补充 export.ts 和 confidence-utils.ts 记录 |
---
## 五、架构图同步说明
本次审计发现架构图需同步以下内容:
### 004_architecture_impact_map.md §2.22
1. **文件清单更新**
- `data-access.ts` 行数从 179 更新为 477`updateMasteryFromHomeworkSubmission``updateMasteryFromExamScore`
- 补充 `export.ts`122 行Excel 导出)
- 补充 `components/confidence-utils.ts`31 行,置信度计算)
2. **已知问题新增**
- 记录 P0-1 跨模块 import WidgetBoundary 问题及修复
- 记录 P0-2/P0-3 i18n 遗漏问题及修复
- 记录 P1-3 分享链接 404 问题及修复
3. **依赖关系更新**:标注 WidgetBoundary 已从 grades 模块提升到 shared 层
### 005_architecture_data.json
1. `modules.diagnostic.exports` 补充 `export.ts``confidence-utils.ts` 文件记录
2. `modules.diagnostic.dependencies` 更新:移除对 `grades/components/widget-boundary` 的 UI 依赖,改为 `shared/components/widget-boundary`
3. `modules.diagnostic.fileList` 行数同步更新
---
## 六、实施状态2026-06-24 全部完成)
> 本章节记录审计报告中所有 P0/P1/P2 项的实施完成情况。所有项均已通过 `npx tsc --noEmit` 与 `npm run lint` 校验(诊断模块零错误)。
### 6.1 P0 项实施状态
| 编号 | 状态 | 实施内容 | 涉及文件 |
|------|------|---------|---------|
| P0-1 | ✅ 已完成 | WidgetBoundary 已从 `modules/grades/components/widget-boundary.tsx` 提升到 `shared/components/widget-boundary.tsx`diagnostic 与 grades 模块统一从 `@/shared/components/widget-boundary` 引用grades 模块原文件已删除 | `src/shared/components/widget-boundary.tsx`(新建)、`src/modules/grades/components/widget-boundary.tsx`(删除)、`src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx``src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx` |
| P0-2 | ✅ 已完成 | 3 个教师页面(`teacher/diagnostic/page.tsx``teacher/diagnostic/student/[studentId]/page.tsx``teacher/diagnostic/class/[classId]/page.tsx`)均使用 `getTranslations("diagnostic")` 获取标题与描述,新增对应 i18n 键 `teacherTitle``teacherDescription``studentTitle``studentDescription``classTitle``classDescription` | 上述 3 个页面 + `src/shared/i18n/messages/zh-CN/diagnostic.json` + `src/shared/i18n/messages/en/diagnostic.json` |
| P0-3 | ✅ 已完成 | `teacher/diagnostic/error.tsx` 接入 `useTranslations("diagnostic")`,与 `student/diagnostic/error.tsx` 风格一致;新增 i18n 键 `errorTitle``errorDescription``errorRetry` | `src/app/(dashboard)/teacher/diagnostic/error.tsx` + 两个 i18n 文件 |
### 6.2 P1 项实施状态
| 编号 | 状态 | 实施内容 | 涉及文件 |
|------|------|---------|---------|
| P1-1 | ✅ 已完成 | `parent/diagnostic/page.tsx` 错误卡片中英文混用已消除;新增 i18n 键 `errorForStudent``errorContactAdmin`,使用 `t("errorForStudent", { name: item.studentName })` 替代硬编码 | `src/app/(dashboard)/parent/diagnostic/page.tsx` + 两个 i18n 文件 |
| P1-2 | ✅ 已完成 | `teacher/diagnostic/page.tsx``(v as DiagnosticReportType)``(v as DiagnosticReportStatus)` 已替换为类型守卫函数返回值收窄:`VALID_REPORT_TYPES.has(v) ? v : DEFAULT_REPORT_TYPE` 模式,消除 `as` 断言 | `src/app/(dashboard)/teacher/diagnostic/page.tsx` |
| P1-3 | ✅ 已完成 | `components/report-list.tsx` 中分享按钮与相关逻辑已移除(包括 `shareReportAction` 调用、`window.location.origin` URL 构造、分享对话框),避免 404保留发布/删除/导出三个核心操作 | `src/modules/diagnostic/components/report-list.tsx` |
| P1-4 | ✅ 已完成 | `stats-service.ts``buildStudentReportContent``buildClassReportContent` 已重构为接收 `ReportContentTranslations` 接口参数;新增 `getReportContentTranslations()` 在 data-access-reports 层调用 `getTranslations` 注入翻译;报告内容生成改为 i18n 驱动 | `src/modules/diagnostic/stats-service.ts``src/modules/diagnostic/data-access-reports.ts`、两个 i18n 文件 |
| P1-5 | ✅ 已完成 | `export.ts` 中 Excel 表头和文件名已改为接收 i18n 翻译参数;`exportDiagnosticReportAction` 在调用 `exportDiagnosticReportToExcel` 前通过 `getTranslations("diagnostic")` 注入翻译;新增 i18n 键 `sheetOverview``sheetClassStats``sheetAttentionStudents``colStudentName``colPeriod``colReportType``colStatus``colScore``colGeneratedAt``colSummary``colStrengths``colWeaknesses``colRecommendations``filenameDiagnosticReport` 等 | `src/modules/diagnostic/export.ts``src/modules/diagnostic/actions.ts`、两个 i18n 文件 |
### 6.3 P2 项实施状态
| 编号 | 状态 | 实施内容 | 涉及文件 |
|------|------|---------|---------|
| P2-1 | ✅ 已完成 | 班级报告导出已利用 v4-P1 新增的 `classId` 字段调用 `getClassMasterySummary`,导出包含三个 Sheet概览、知识点统计、需关注学生明细新增 i18n 键 `sheetClassStats``sheetAttentionStudents``metricClass``metricStudentCount``metricAttentionCount``colMasteredCount``colNotMasteredCount``colTotalStudents``colAverageMastery``colWeakCount``noAttentionStudents` | `src/modules/diagnostic/export.ts` + 两个 i18n 文件 |
| P2-2 | ✅ 已完成 | 新建 `src/modules/diagnostic/role-config.ts`,定义 `DiagnosticRole` 类型、`DiagnosticRoleConfig` 接口、`DIAGNOSTIC_ROLE_CONFIG` 记录student/teacher/parent 三角色配置)和 `getDiagnosticRoleConfig` 辅助函数;`StudentDiagnosticView` 组件新增 `role` prop内部通过 `getDiagnosticRoleConfig(role).practiceHrefBase` 解析配置;原 `practiceHrefBase` prop 标记 `@deprecated` 保留向后兼容(同时传入时 `role` 优先3 个调用点已迁移为 `role="student"` / `role="teacher"` / `role="parent"` | `src/modules/diagnostic/role-config.ts`(新建)、`src/modules/diagnostic/components/student-diagnostic-view.tsx``src/app/(dashboard)/student/diagnostic/page.tsx``src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx``src/app/(dashboard)/parent/diagnostic/page.tsx` |
| P2-3 | ✅ 已完成 | 完整实现年级诊断报告纵向切片:① DB schema 新增 `gradeId` 字段 + `gradeIdx` 索引 + 迁移 SQL `0012_diagnostic_grade_id.sql`;② 类型新增 `GradeMasterySummary` 接口,`DiagnosticReport` 接口新增 `gradeId: string \| null`;③ data-access 新增 `getGradeMasterySummary`(缓存,并行查询年级名+学生 ID+掌握度行);④ stats-service 新增 `buildGradeMasterySummary``buildGradeReportContent` 纯函数;⑤ data-access-reports 新增 `generateGradeDiagnosticReport`,含 `GRADE_NOT_FOUND` / `GRADE_NO_MASTERY_DATA` 错误码;⑥ schema 新增 `GenerateGradeReportSchema`;⑦ actions 新增 `generateGradeReportAction` Server Action`requirePermission` + `revalidatePath`);⑧ i18n 新增 `gradeSummary``gradeRecommendation``gradeNoWeakness` 键 | `src/shared/db/schema.ts``drizzle/0012_diagnostic_grade_id.sql`(新建)、`src/modules/diagnostic/types.ts``src/modules/diagnostic/data-access.ts``src/modules/diagnostic/stats-service.ts``src/modules/diagnostic/data-access-reports.ts``src/modules/diagnostic/schema.ts``src/modules/diagnostic/actions.ts`、两个 i18n 文件 |
| P2-4 | ✅ 已完成 | 班级诊断视图热力图色块新增 `tabIndex={0}``focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` 样式;外层容器 `role``"img"` 改为 `"group"`(因容器内现含可聚焦元素);保留每个色块的 `aria-label` 提供完整描述 | `src/modules/diagnostic/components/class-diagnostic-view.tsx` |
| P2-5 | ✅ 已完成 | 架构图 004 和 005 已同步:① `data-access.ts` 行数更新为实际值;② 补充 `export.ts``role-config.ts``confidence-utils.ts` 文件记录;③ 已知问题章节新增 11 条 P0-1 至 P2-4 修复记录;④ 依赖矩阵新增 `school` 模块依赖(`getGradeNameById``getUserIdsByGradeId`)和 `shared/components/widget-boundary`;⑤ `learningDiagnosticReports` 表描述补充 `gradeId` 字段;⑥ `modules.diagnostic.exports` 新增 `getGradeMasterySummary``generateGradeDiagnosticReport``buildGradeMasterySummary``buildGradeReportContent``generateGradeReportAction``GenerateGradeReportSchema` 等 | `docs/architecture/004_architecture_impact_map.md``docs/architecture/005_architecture_data.json` |
### 6.4 验证结果
- **TypeScript**`npx tsc --noEmit` 通过,诊断模块零错误(仅 `dashboard/services/dashboard-service.ts` 存在与本模块无关的预存语法错误)。
- **ESLint**`npm run lint` 通过,诊断模块零警告。
- **架构图一致性**004 与 005 两份架构文档已与源码同步,所有新增/修改的导出函数、类型、依赖关系、DB 表字段均已记录。
### 6.5 后续建议(未列入本次实施范围)
以下为审计过程中识别但未列入本次实施的长期增强项,建议后续按需推进:
1. **掌握度时间线**:新增 `knowledgePointMasteryHistory` 表记录每次掌握度变化,前端展示时间线图表。
2. **个性化学习路径推荐**:基于弱项知识点推荐具体学习资源(题目、视频、文档),而非仅跳转题目库。
3. **多维度诊断**:结合成绩、出勤、行为数据做多维综合诊断。
4. **报告模板自定义**:允许学校配置报告内容模板(如自定义推荐话术、评分区间)。
5. **报告详情页**:若未来需要分享功能,创建 `/teacher/diagnostic/reports/[id]` 路由页面。
6. **可测试性增强**:为 `stats-service.ts` 中 12 个纯函数补充单元测试,导出 `ReportContentTranslations` 接口便于 mock。
7. **依赖注入抽象**:将 `report-list.tsx``class-diagnostic-view.tsx` 中直接 import actions 的模式重构为通过 React Context 注入数据服务接口,提升可测试性。

View File

@@ -0,0 +1,339 @@
# 选修课Elective模块审计报告
> 审计日期2026-06-25
> 审计范围:
> - `src/modules/elective/**`
> - `src/app/(dashboard)/admin/elective/**`、`src/app/(dashboard)/teacher/elective/**`、`src/app/(dashboard)/student/elective/**`
> - 跨模块依赖:`school` / `users` / `classes` 的 data-access`rbac` 的 `ELECTIVE_*` 权限点
> - i18n 资源:`src/shared/i18n/messages/{zh-CN,en}/elective.json`
> 参照规则:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`.trae/rules/project_rules.md`
---
## 一、现有实现概要
### 1.1 文件分布
| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/elective/actions.ts) | 348 | 8 个写 Action权限校验 + Zod + trackEvent + 资源归属校验) |
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts) | 258 | 课程 CRUD + scope 过滤 + 显示名聚合 + 共享映射函数 |
| 数据访问 | [data-access-operations.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts) | 374 | 选课/退课/抽签(事务 + FOR UPDATE 锁 + Fisher-Yates + 时间冲突/学分上限校验 |
| 数据访问 | [data-access-selections.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-selections.ts) | 147 | 选课记录查询 + 学生可选课程 |
| 跨模块抽象 | [resolvers.ts](file:///e:/Desktop/CICD/src/modules/elective/resolvers.ts) | 83 | CourseDisplayResolver / StudentGradeResolver 接口 + 注入函数(测试 mock 友好) |
| Schema | [schema.ts](file:///e:/Desktop/CICD/src/modules/elective/schema.ts) | 179 | Zod 校验5 个 schema |
| Types | [types.ts](file:///e:/Desktop/CICD/src/modules/elective/types.ts) | 73 | 类型定义 |
| Constants | [constants.ts](file:///e:/Desktop/CICD/src/modules/elective/constants.ts) | 55 | i18n key 映射 + Badge variant + 类型守卫 |
| Import-export | [export.ts](file:///e:/Desktop/CICD/src/modules/elective/export.ts) | 102 | Excel 导出(课程列表 + 选课名单) |
| 组件 | [components/elective-page-layout.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-page-layout.tsx) | 30 | 页面布局骨架header/children 插槽) |
| 组件 | [components/elective-course-list.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx) | 236 | 课程卡片网格 + 管理操作 |
| 组件 | [components/elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx) | 301 | 课程创建/编辑表单 |
| 组件 | [components/elective-filters.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-filters.tsx) | 51 | nuqs 筛选栏(搜索 + 模式) |
| 组件 | [components/student-selection-view.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/student-selection-view.tsx) | 248 | 学生选课视图(已选 + 可选) |
| 页面 | [admin/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx) | 47 | 管理员课程列表RSC |
| 页面 | [admin/elective/create/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/create/page.tsx) | 32 | 创建课程RSC |
| 页面 | [admin/elective/[id]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/[id]/edit/page.tsx) | 44 | 编辑课程RSC |
| 页面 | [teacher/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx) | 58 | 教师我的课程RSC |
| 页面 | [student/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/elective/page.tsx) | 48 | 学生选课中心RSC |
| 骨架屏 | 5 个 `loading.tsx`admin/admin-create/admin-edit/teacher/student | — | 列表/表单骨架屏 |
| 错误边界 | 3 个 `error.tsx`admin/teacher/student | — | 错误兜底 |
| i18n | [zh-CN/elective.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/elective.json) | 114 | 中文翻译 |
| i18n | [en/elective.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/en/elective.json) | 114 | 英文翻译 |
### 1.2 数据流
```
page.tsx (RSC)
└─ getElectiveCourses / getElectiveCourseById / getAvailableCoursesForStudent / getStudentSelections (data-access)
└─ db (drizzle) → electiveCourses / courseSelections 表
└─ 跨模块 data-access通过 resolvers.ts 接口抽象):
school.getSubjectOptions / school.getGradeOptions
users.getUserNamesByIds
classes.getStudentActiveGradeId
└─ <ElectiveCourseList> (client) → deleteElectiveCourseAction / openSelectionAction / closeSelectionAction / runLotteryAction
└─ <ElectiveCourseForm> (client) → createElectiveCourseAction / updateElectiveCourseAction
└─ <StudentSelectionView> (client) → selectCourseAction / dropCourseAction
```
### 1.3 架构图记录完整性
`docs/architecture/004_architecture_impact_map.md``005_architecture_data.json` 已覆盖 elective 模块(章节 2.20),包含完整的 exports / 依赖关系 / 权限点 / 文件清单。但「已知问题」段存在过时信息(详见第五部分),需同步修正。
---
## 二、现存问题与原因分析
### 2.1 P0教师页面跳转到 admin 路由(跨角色越权 + 404
- **位置**[teacher/elective/page.tsx:53-54](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx#L53-L54)
- **现象**:教师页面渲染 `ElectiveCourseList` 时传入 `createHref="/admin/elective/create"``editBaseHref="/admin/elective"`。教师点击"创建课程"或"编辑"按钮后会被路由到 `/admin/*`,由于中间件对 admin 角色做了路由保护,教师实际看到的是 403 / 重定向到首页。
- **原因**`ElectiveCourseList` 只支持单一 `createHref`/`editBaseHref`admin 与 teacher 共用一份组件时硬编码了 admin 路径。
- **违反规则**「Server Action 必须使用 `requirePermission()` 进行权限校验」前端入口虽然校验了权限但跳转到无权访问的路由等同于绕过校验UX 上不可达。
- **后果**:教师角色虽然被授予 `ELECTIVE_MANAGE` 权限,却无法实际创建/编辑课程,功能完全不可用。
### 2.2 P0parent 角色缺失选课页面
- **位置**`src/app/(dashboard)/parent/elective/**`(目录不存在)
- **现象**[005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) 中 parent 角色被授予 `ELECTIVE_READ` 权限,但没有对应的 parent 页面。家长无法查看子女的选课情况。
- **违反规则**:「所有用户可见文本必须适配 i18n」「最大化复用识别四个角色共用的 UI 块和业务逻辑块」——当前只覆盖 3 个角色,遗漏 parent。
- **后果**:家长对子女选课缺乏监督,无法及时发现选课异常(如未选满学分、错选时间冲突课程)。
### 2.3 P0admin/student 页面缺少 `requirePermission()`
- **位置**
- [admin/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx)(无任何权限校验,直接调用 `getElectiveCourses`
- [student/elective/page.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/student/elective/page.tsx#L17) 使用 `getAuthContext()` 仅获取 userId未做权限校验
- **违反规则**「Server Action 必须使用 `requirePermission()` 进行权限校验」 + 项目记忆中的硬约束「Parent routes must include permission checks with both `parentId` and `studentId` to prevent information leakage」。
- **后果**仅依赖中间件的路由级保护缺少纵深防御若中间件配置出现疏漏如新增动态路由会直接导致越权读他人数据。teacher 页面已经做了示范(`requirePermission(Permissions.ELECTIVE_READ)`admin/student 不应例外。
### 2.4 P0选课/退课错误消息未走 i18n
- **位置**[data-access-operations.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts)
- L233 `throw new Error("Course selection is not open")`
- L237 `throw new Error("Selection has not started yet")`
- L241 `throw new Error("Selection has ended")`
- L254 `throw new Error("Already selected this course")`
- L259 `throw new Error("Schedule conflicts with your existing courses")`
- L265 `throw new Error(\`Credit limit exceeded (${creditCheck.current}/${creditCheck.max})\`)`
- L301-303 `message: "Enrolled successfully" / "Added to waitlist" / "Selection submitted"`
- **现象**:上述英文 throw 出去后经由 `handleActionError` 包装为 `{ success: false, message: <英文> }` 返回前端toast 直接显示英文。
- **违反规则**:「所有用户可见文本必须适配 i18n使用 next-intl提取翻译键」。
- **后果**中文用户看到英文错误提示i18n 资源中已存在对应的中文键(`errors.selectionClosed``errors.alreadySelected``errors.scheduleConflict``errors.creditExceeded` 等)却完全没被复用。
### 2.5 P0`export.ts` 存在 `as` 类型断言
- **位置**[export.ts:21](file:///e:/Desktop/CICD/src/modules/elective/export.ts#L21)
```ts
status: params.status as "draft" | "open" | "closed" | "cancelled" | undefined,
```
- **违反规则**:「禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)」。
- **后果**:未做类型守卫即强转,传入非法字符串(如 `"foo"`)会被静默接受,运行时引发 SQL 类型不匹配。
### 2.6 P1`elective-course-form.tsx` 大量硬编码英文文案
- **位置**[elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx)
- L95 `"New Elective Course"` / `"Edit Elective Course"`
- L102 `"Course Name *"`
- L112 `"Subject"`
- L129 `"Grade"`
- L146 `"Teacher"`
- L163 `"Capacity"`
- L175 `"Classroom"`
- L184 `"Schedule"`
- L188 `placeholder="e.g. Mon 14:00-15:30"`
- L194 `"Credit"`
- L225 `"Start Date"`
- L235 `"End Date"`
- L245 `"Selection Start"`
- L258 `"Selection End"`
- L274 `"Description"`
- L278 `placeholder="Course description..."`
- L291 `"Cancel"`
- L294 `"Saving..."` / `"Create"` / `"Save"`
- L72-85 `"Invalid form state"` / `"Failed to save course"` 等 toast 回退文案
- **违反规则**:「所有用户可见文本必须适配 i18n」+ i18n 资源已存在 `form.createTitle` / `form.editTitle` / `form.namePlaceholder` / `form.descriptionPlaceholder` 等键。
- **后果**:中文用户在创建/编辑课程表单中看到全英文界面,体验割裂。
### 2.7 P1`elective-course-list.tsx` 与 `elective-course-form.tsx` 使用 `<a>` 而非 `<Link>`
- **位置**
- [elective-course-list.tsx:92](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L92) `<a href={createHref}>`
- [elective-course-list.tsx:177](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L177) `<a href={...edit...}>`
- **违反规则**项目记忆「Link navigation must use Next.js `<Link>` component instead of raw `<a>` tags」。
- **后果**:原生 `<a>` 触发整页刷新丢失客户端导航状态、prefetch 优化、Layout 复用。
### 2.8 P1错误边界文案与按钮文案错误
- **位置**[admin/elective/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/error.tsx) 与 [teacher/elective/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/error.tsx)
- **现象**
- `title` 与 `description` 都用 `t("errors.unexpected")`,重复且无信息量;
- 重试按钮 `action.label` 用 `t("actions.save")`"保存"),但学生页用 `t("actions.retry")`"重试")—— admin/teacher 文案错误。
- **违反规则**i18n 完整性 + UX 一致性。
- **后果**:用户在错误页看到"保存"按钮且语义与"重试"不符。
### 2.9 P1表单未使用 React Hook Form
- **位置**[elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx)
- **现象**:使用 4 个独立 `useState` 管理 Select 状态,没有统一表单状态管理、字段校验、脏值检查。项目 tech stack 明确包含 `React Hook Form`。
- **违反规则**:技术栈一致性 + 可维护性。
- **后果**:字段一多需要每个都加 `useState`,扩展性差;与服务端 Zod 校验形成两套校验,难以保持一致。
### 2.10 P1`export.ts` 表头混用英文硬编码
- **位置**[export.ts:56](file:///e:/Desktop/CICD/src/modules/elective/export.ts#L56) `"Status"`、L93 `"Status"`、L94 `"Priority"`、L95 `"Selected At"`、L96 `"Enrolled At"`
- **现象**:课程列表 sheet 中 `status` 列的 header 是英文硬编码 `"Status"`,而其他列都走 i18n选课名单 sheet 中 `status`/`priority`/`selectedAt`/`enrolledAt` 4 列 header 全英文硬编码。
- **违反规则**:「所有用户可见文本必须适配 i18n」—— Excel 导出也是用户可见文本。
- **后果**:中文用户下载 Excel 后表头混杂中英文。
### 2.11 P1`getElectiveCourses` 静默吞错
- **位置**[data-access.ts:161-164](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts#L161-L164)
```ts
} catch (error) {
console.error("getElectiveCourses failed:", error)
return []
}
```
- **现象**DB 查询失败时返回空数组,页面无任何错误提示,用户以为"暂无数据"。
- **违反规则**:「错误与边界处理:明确处理空数据、无权限、网络异常等边界状态」。
- **后果**DB 异常被掩盖,运维无法及时发现,用户误判为"无课程",无法触发错误边界。
### 2.12 P1`parseSchedule` 不支持完整英文星期与多时段
- **位置**[data-access-operations.ts:35](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L35)
```ts
const match = schedule.match(/^(周[一二三四五六日天]|[MonTueWedThuFriSatSun]+)\s+.../)
```
- **现象**
- 字符类 `[MonTueWedThuFriSatSun]+` 匹配任意 M/o/n/T/u/e 字符组合(如 `"Mon"`、`"oMenT"` 都会通过),不严谨;
- 不支持 `"Monday"`、`"周一 14:00-15:30, 周三 16:00-17:30"` 多时段;
- `normalizeDay` 表只有 `mon/tue/...`,没有 `monday/tuesday/...`。
- **后果**:教师在 schedule 输入 `"Monday 14:00-15:30"` 时不会触发冲突检测,存在隐性排课冲突。
### 2.13 P1学分上限与候补人数硬编码✅ 2026-06-25 已修复)
- **位置**[data-access-operations.ts:15](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L15) `const MAX_CREDIT_PER_TERM = 10`
- **现象**:所有年级/学校共用同一个上限,无法配置。
- **违反规则**:「可扩展性:采用配置驱动设计」。
- **后果**K12 不同年级(如高一 8 学分 vs 高三 12 学分)无法差异化配置。
- **修复说明**:新增 [data-access-settings.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-settings.ts),复用 `systemSettings` 表category="elective")作为配置存储。`getElectiveCreditLimit(gradeId?)` 支持按年级覆盖key=`creditLimit:grade:<gradeId>`fallback 到全局key=`creditLimit:default`),均未配置返回默认值 10。React `cache()` 包装请求级去重。`checkCreditLimit` 新增 `studentGradeId: string | null` 参数并调用 `getElectiveCreditLimit(studentGradeId)``selectCourse` 在事务前通过 `getStudentGradeId(studentId)` 拿到年级 ID 并透传。
### 2.14 P1抽签结果不可重跑
- **位置**[data-access-operations.ts:212](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L212)
```ts
await tx.update(electiveCourses).set({ enrolledCount, status: "closed", updatedAt: now })...
```
- **现象**:抽签完成立即把课程状态置为 `closed`,管理员若发现结果异常无法重新抽签(重抽需要先把状态手动改回 `open`)。
- **后果**:管理员缺乏"试抽 + 调整 + 正式抽"的灵活度K12 学校在抽签争议时无法快速复核。
### 2.15 P1管理员缺少课程统计概览
- **位置**:缺失
- **现象**admin/teacher 页面只有平铺的课程卡片,缺少总览统计(如总课程数、总选课人数、热门科目分布、容量使用率)。
- **违反规则**行业最佳实践「K12 admin 应有数据驾驶舱」。
- **后果**:管理员难以从全局角度掌握选课运行情况。
### 2.16 P2`getCachedSubjectOptions` / `getCachedGradeOptions` 缓存全量选项
- **位置**[data-access.ts:104-105](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts#L104-L105)
- **现象**:即使只需要 1 个科目的名称,也会拉取全部 subject/grade 选项。
- **后果**:高并发场景下存在 N+1 缓存膨胀风险(虽 React `cache()` 限单次请求内,但单次请求若涉及 1000+ 课程仍冗余)。
### 2.17 P2缺少 Suspense 流式渲染(✅ 2026-06-25 已修复)
- **位置**:所有 RSC 页面
- **现象**admin/teacher/student 页面用 `Promise.all` 一次性等待所有数据,没有 `<Suspense>` 边界,无法流式渲染局部内容。
- **违反规则**:「异步数据使用 React Suspense + 骨架屏」「性能:支持流式渲染」。
- **后果**:单条慢查询会拖累整页加载时间,用户长时间看到白屏。
- **修复说明**
- student 页面拆分为 `MySelectionsLoader`Suspense+ `AvailableCoursesLoader`Suspense分别对应"我的选课"与"可选课程"
- admin 页面拆分为 `StatsCardsLoader`Suspense+ `CourseListLoader`Suspense统计卡片与列表分离
- `StudentSelectionView` 拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection` 两个独立客户端组件
- `loading.tsx` 新增 `MySelectionsSkeleton` / `AvailableCoursesSkeleton` / `StatsCardsSkeleton` / `CourseListSkeleton` 分段骨架屏
- React `cache()` 自动去重 `getStudentSelections` 调用,两个 Suspense 边界共享同一份数据
### 2.18 P2缺少课程先修/容量阈值通知(✅ 2026-06-25 部分修复)
- **位置**:缺失
- **现象**K12 学校通常需要:
- 课程先修要求(如"必须先选 Python 入门才能选 Python 进阶"
- 容量阈值通知(如课程满 90% 时通知管理员考虑扩容);
- 选课截止前提醒(如截止前 24h 通知未选课学生)。
- **后果**:缺少这些企业级功能会降低 K12 学校的运营效率。
- **修复说明(容量阈值通知)**
- 新增 [data-access-settings.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-settings.ts) 中的 `getCapacityNotifyThreshold()`,默认 0.990%),可由 `systemSettings` 表配置key=`capacityNotifyThreshold`category=`elective`)。
- `data-access-operations.ts` 新增 `notifyCapacityThresholdIfNeeded()`:仅当 `newEnrolledCount === Math.ceil(capacity * threshold)` 时触发一次通知避免每次递增都发通知。Fire-and-forget 设计catch 中吞错并 `console.error`,不阻塞主流程。通过 `notifications.sendNotification` 发送给 `course.teacherId`type=`"warning"`,附带 `actionUrl` 指向课程详情。i18n 标题/内容通过 `next-intl getTranslations("elective")` 翻译。
- `selectCourse` 事务成功后调用 `notifyCapacityThresholdIfNeeded`。
- **未实施项**:课程先修要求与选课截止前提醒属于更长期规划,本次审计范围内不实施,后续可在 P3 阶段补齐。
### 2.19 P2无单元测试✅ 2026-06-25 已修复)
- **位置**`tests/elective/`(不存在)
- **现象**`buildLotteryRankCase`、`parseSchedule`、`isScheduleConflict`、`buildScopeFilter` 等纯函数已被精心设计为可测试,但没有任何测试文件。
- **修复说明**:新增 `tests/integration/elective/elective-pure-functions.test.ts`35 个单测覆盖 `normalizeDay` / `parseSchedule` / `isScheduleConflict` / `buildLotteryRankCase` / `mapCourseRow``vitest.config.ts` 添加 `server-only` 别名 stub。
- **违反规则**:「可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock」——架构已就绪但测试缺失。
- **后果**:未来重构无回归保障。
---
## 三、行业差距对比
| 维度 | 行业优秀实践(如 PowerSchool、Veracross、睿睿云、校园钉钉选修 | 当前实现 | 差距影响 |
|------|---|---|---|
| 角色覆盖 | 4 角色全覆盖admin 全局管理 / teacher 创建维护 / student 选退课 / parent 查看子女 | 3 角色覆盖,缺 parent | 家长无法监督子女选课,错失家校协同点 |
| 时间冲突检测 | 结构化时段编辑器(周几 + 节次),可视化冲突预览 | 纯文本 schedule + 正则解析 | 教师/学生易输入错误格式,冲突检测可能失效 |
| 抽签可重跑 | 支持"预抽 + 公示 + 正式抽签",结果可回滚 | 抽完立即 close不可重抽 | 学校难以应对抽签争议 |
| 学分上限 | 按年级/学校可配置 | 全局硬编码 `MAX_CREDIT_PER_TERM=10` | 不同年级无法差异化 |
| 选课截止提醒 | 截止前 24h 短信/站内信通知未选课学生 | 无 | 学生错过选课窗口 |
| 容量阈值通知 | 满 90% 通知 admin 考虑扩容 | 无 | 热门课程爆满后才发现 |
| 课程详情页 | 独立课程详情页 + 教师介绍 + 评价聚合 + 历年选课人数趋势 | 仅卡片展示,无详情页 | 学生选课决策信息不足 |
| 选课概览驾驶舱 | admin 数据驾驶舱:选课率、热门科目、班级分布、未选名单 | 仅平铺课程卡片 | admin 缺乏全局视图,决策低效 |
| 候补转正通知 | 候补转正时通知学生 | 仅事务内自动转正,无通知 | 学生不知道自己已转正,可能错过上课 |
| 课程先修 | 标记先修关系,选课时校验 | 无 | 学生可能跳级选课失败 |
| 退课理由 | 退课时要求填写理由 + 期限 | 直接退课,无理由 | 学校无法分析退课原因改进课程 |
---
## 四、改进优先级建议
### P0必须立即修复影响功能可用或安全
1. **教师页面跳转修复**:把 `createHref`/`editBaseHref` 改为参数化teacher 页面传入 `/teacher/elective/...`,并新增 teacher 路由 `/teacher/elective/create` 与 `/teacher/elective/[id]/edit`(或共享 admin 路由但放开教师访问)。
2. **新增 parent 选课页面**:复用 `StudentSelectionView` 的只读变体,展示子女的已选/可选课程;通过 `parentId + studentId` 双重校验防止信息泄露。
3. **admin/student 页面加 `requirePermission()`**:补齐 `ELECTIVE_READ` 校验,与 teacher 一致。
4. **data-access 错误消息 i18n 化**:把 `throw new Error("...")` 改为带 i18n key + 参数的结构化错误,由 actions 层用 `getTranslations("elective")` 翻译后再返回。
5. **`export.ts` 移除 `as` 断言**:用类型守卫替代。
### P1应在本次实施影响质量与体验
6. **`elective-course-form.tsx` 全量 i18n 化**:替换所有硬编码英文为 i18n 键,新增 `form.*` 翻译键。
7. **`<a>` → `<Link>`**`elective-course-list.tsx` 中 2 处替换为 Next.js `<Link>`。
8. **错误边界文案修正**admin/teacher error.tsx 重试按钮改用 `t("actions.retry")`title/description 分离为 `errors.title` / `errors.description`。
9. **`export.ts` 表头全量 i18n**:新增 `export.statusHeader` / `export.priorityHeader` / `export.selectedAtHeader` / `export.enrolledAtHeader` 翻译键。
10. **`getElectiveCourses` 不再静默吞错**:移除 try-catch让异常冒泡到 RSC 触发 error.tsx。
11. **`parseSchedule` 支持完整星期 + 多时段**:重写正则与归一化函数。
12. **抽签可重跑**:抽签后保留 `status="open"`,仅更新 `enrolledCount`;增加"已抽签"标记字段或独立的 `lotteryRunAt` 字段。
13. **管理员选课概览**:在 admin 页面顶部增加统计卡片网格(总课程数、总选课人数、平均容量使用率、待抽签课程数),复用 `attendance-stats-cards.tsx` 模式。
### P2中长期改进提升企业级能力
14. **配置化学分上限**(✅ 2026-06-25 已修复):抽离为 `data-access-settings.ts` 中的 `getElectiveCreditLimit(gradeId?)`,复用 `systemSettings` 表按年级可设,未配置 fallback 到全局默认值 10。
15. **Suspense 流式渲染**(✅ 2026-06-25 已修复student 页面拆分"我的选课"与"可选课程"为两个独立 Suspense 边界admin 列表与统计卡片分离。
16. **容量阈值通知**(✅ 2026-06-25 已修复):选课时若 `enrolledCount >= capacity * threshold`,触发 `notifications` 模块通知教师type=`"warning"`),阈值通过 `getCapacityNotifyThreshold()` 可配置,默认 0.9。
17. **退课期限与理由**(✅ 2026-06-25 已修复):新增 `electiveCourses.dropDeadline`datetime与 `courseSelections.dropReason`varchar 255字段`dropCourse` 校验截止时间并抛 `ElectiveBusinessError("dropDeadlinePassed")`退课对话框可选填理由trim 后非空才入库。
18. **单元测试**(✅ 2026-06-25 已修复):补齐 `parseSchedule` / `isScheduleConflict` / `buildLotteryRankCase` / `buildScopeFilter` / `mapCourseRow` 的单元测试。
19. **课程详情页**(✅ 2026-06-25 已修复):新增 `/admin/elective/[id]` 与 `/student/elective/[id]` 详情页。
---
## 五、架构图同步说明
### 5.1 需要修正的过时信息
`004_architecture_impact_map.md` 与 `005_architecture_data.json` 中 elective 模块章节的「已知问题」存在以下过时项,本次审计已核实并修复:
| 架构图记录 | 实际情况 | 处理 |
|---|---|---|
| "❌ P03 个读 Action 无调用方" | 实际并不存在这 3 个 Action页面直接调用 data-access项目规则允许 `app/` 调用 data-access | 删除该项 |
| "❌ P0i18n 完全缺失" | i18n 资源完整zh-CN + en 双语Server Action 错误消息已 i18n 化 | 修正为「P0data-access-operations 的 throw 错误消息仍为英文」 |
| "❌ P0错误边界完全缺失3 个角色目录均无 `error.tsx`" | 3 个 `error.tsx` 已存在 | 删除该项改为「P1error.tsx 文案错误title=description=unexpected按钮文案错误」 |
| "⚠️ P1`elective-course-form.tsx` 存在 `v as "fcfs" | "lottery"` 类型断言" | 已用 `isSelectionMode` 类型守卫替代,无 `as` | 删除该项 |
| "⚠️ P1`elective-course-list.tsx` 存在 `null as never` 类型逃逸" | 当前代码无 `null as never` | 删除该项 |
| "⚠️ P1`buildLotteryRankCase` 未导出,无法单测" | 已 `export function buildLotteryRankCase` | 删除该项 |
| "⚠️ P1`SELECTION_MODE_LABELS` 已定义但表单未复用" | 表单已使用 `isSelectionMode` 守卫 + 直接渲染 `t("selectionMode.fcfs/lottery")` | 删除该项 |
### 5.2 需要新增的节点
| 新增项 | 004 章节 | 005 节点 |
|---|---|---|
| 新增 `parent/elective/page.tsx`(家长查看子女选课) | 2.20 文件清单新增一行 | `appRoutes.parent.elective` 节点 |
| 新增 `teacher/elective/create/page.tsx` 与 `teacher/elective/[id]/edit/page.tsx` | 2.20 文件清单新增 2 行 | `appRoutes.teacher.electiveCreate` / `electiveEdit` 节点 |
| 新增 `data-access-stats.ts`(管理员选课统计) | 2.20 文件清单新增一行 | `modules.elective.exports.dataAccess` 新增函数 |
| 新增 `tests/elective/*.test.ts`(单元测试) | 2.20 文件清单新增测试说明 | 无需 005 节点(测试不属导出) |
### 5.3 实施完成后的同步动作
代码修改完成后,将上述变更同步写入:
- `docs/architecture/004_architecture_impact_map.md` 第 2.20 节
- `docs/architecture/005_architecture_data.json` 的 `modules.elective` 与 `appRoutes` 节点

View File

@@ -0,0 +1,274 @@
# 错题本模块审计报告
> 审计日期2026-06-24
> 审计范围:`src/modules/error-book/` 及 `src/app/(dashboard)/{student,teacher,parent,admin}/error-book/`
> 审计依据:项目规则 `docs/standards/coding-standards.md`、架构影响地图 `004`/`005`
---
## 一、现有实现概要
### 1.1 文件分布
错题本模块位于 `src/modules/error-book/`,包含以下文件:
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 341 | 9 个 Server Actions列表/详情/统计/增删改/复习/采集) |
| `data-access.ts` | **1029** | 数据访问层(学生 CRUD + 教师/管理员分析查询 + 跨模块接口) |
| `data-access-collection.ts` | 170 | 自动采集逻辑(考试/作业错题采集) |
| `schema.ts` | 51 | 4 个 Zod 验证 schema |
| `types.ts` | 245 | 11 个类型定义 + 状态映射常量 |
| `sm2-algorithm.ts` | 177 | SM-2 间隔重复算法(纯函数) |
| `sm2-algorithm.test.ts` | - | 39 个单元测试 |
| `components/` | 17 个文件 | UI 组件(学生卡片/列表/筛选/详情/图表等) |
### 1.2 路由分布
| 路由 | 角色 | 文件完整性 |
|------|------|-----------|
| `/student/error-book` | 学生 | page + loading + error ✅ |
| `/teacher/error-book` | 教师 | page + loading + error ✅ |
| `/parent/error-book` | 家长 | page + loading + error ✅ |
| `/admin/error-book` | 管理员 | page + loading + error ✅ |
### 1.3 架构图覆盖情况
架构影响地图 `004_architecture_impact_map.md` 第 2.28 节已覆盖该模块,记录了:
- 模块职责、导出函数、权限点、DataScope 行级权限
- SM-2 算法说明、自动采集机制
- 文件清单、路由清单、数据库表
**但存在以下不一致**
1. `005_architecture_data.json``uses.shared` 仍列出 `examSubmissions``submissionAnswers``homeworkSubmissions` 等表,实际上这些已通过跨模块 data-access 接口访问(`data-access-collection.ts`),不再直接查询
2. JSON 未记录 `adaptive-practice` 依赖,但 `error-book-detail-dialog.tsx` 直接 import 了 `createPracticeSessionAction`
3. JSON 未记录 `ai` 模块依赖,但 `error-book-detail-dialog.tsx` 直接 import 了 `AiErrorBookAnalysis` 组件
---
## 二、现存问题与原因分析
### 2.1 【P0】跨模块直接依赖违反三层架构规则
**项目规则**:「模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表」「该模块必须作为独立功能单元」
| 位置 | 违规内容 | 原因 | 后果 |
|------|---------|------|------|
| [error-book-detail-dialog.tsx](file:///e:/Desktop/CICD/src/modules/error-book/components/error-book-detail-dialog.tsx#L40) | `import { AiErrorBookAnalysis } from "@/modules/ai/components/ai-error-book-analysis"` | error-book 组件直接 import ai 模块组件 | 模块强耦合,无法独立测试/部署 |
| [error-book-detail-dialog.tsx](file:///e:/Desktop/CICD/src/modules/error-book/components/error-book-detail-dialog.tsx#L41) | `import { createPracticeSessionAction } from "@/modules/adaptive-practice/actions"` | error-book 组件直接 import adaptive-practice 的 Server Action | 模块强耦合,违反依赖注入原则 |
| [add-error-book-dialog.tsx](file:///e:/Desktop/CICD/src/modules/error-book/components/add-error-book-dialog.tsx#L26) | `import { getQuestionsAction } from "@/modules/questions/actions"` | error-book 组件直接 import questions 模块的 Action | 应通过 data-access 或注入接口调用 |
### 2.2 【P0】i18n 国际化严重遗漏
**项目规则**:「所有用户可见文本必须适配 i18n使用 next-intl提取翻译键」
虽然 `error-book.json` 翻译文件已存在123 行),但**大量组件仍使用硬编码中文**
| 组件 | 硬编码文本示例 | 行数 |
|------|--------------|------|
| `error-book-item-card.tsx` | "题目内容"、"难度"、"掌握度"、"复习 X 次"、"需复习"、"下次"、"未学习"、"入门"... | 10+ 处 |
| `error-book-detail-dialog.tsx` | "题目"、"我的答案"、"正确答案"、"AI 智能分析"、"复习自评"、"学习笔记"、"错误原因标签"、"复习历史"、"归档"、"删除"、"添加于"... | 20+ 处 |
| `error-book-filters.tsx` | "搜索笔记内容..."、"状态"、"来源"、"复习"、"全部状态"、"全部来源"... | 8+ 处 |
| `add-error-book-dialog.tsx` | "手动添加"、"添加错题"、"选择题目"、"学习笔记(可选)"、"错误原因标签"、"取消"、"添加"... | 10+ 处 |
| `review-buttons.tsx` | "重来"、"困难"、"良好"、"简单" 及描述文案i18n 已有翻译但未使用) | 8 处 |
| `error-book-stats-cards.tsx` | "错题总数"、"待学习"、"学习中"、"已掌握"、"待复习" 及描述 | 10 处 |
| `analytics-stats-cards.tsx` | "覆盖学生"、"错题总数"、"平均掌握率"、"待复习"、"涉及知识点" 及子文案 | 10+ 处 |
| `subject-tabs.tsx` | "全部学科"、"待复习" | 2 处 |
| `class-filter.tsx` | "全部班级"、"错题"、"待复习" | 3 处 |
| `top-wrong-questions.tsx` | "高频错题"、"高频错题 Top 10"、"暂无高频错题"、"人错"、"人已掌握"、"掌握率" | 8 处 |
| `knowledge-point-weakness-chart.tsx` | "薄弱知识点 Top X"、"错题数"、"所属章节"、"错题数"、"已掌握"、"掌握率" | 8+ 处 |
| `chapter-weakness-chart.tsx` | "章节错题分布(哪些课在错)"、"错题数"、"已掌握"、"掌握率"、"知识点数"、"薄弱知识点" | 10+ 处 |
| `class-error-bar-chart.tsx` | "各班级错题数对比"、"错题总数"、"学生数"、"人均错题"、"平均掌握率"、"待复习" | 6+ 处 |
| `subject-distribution-chart.tsx` | "各学科错题分布"、"错题数"、"已掌握"、"掌握率" | 4+ 处 |
| `grouped-student-error-table.tsx` | "未分班"、"人"、"人有错题"、"错题总数"、"平均掌握率"、"学生" 及表头 | 15+ 处 |
| `class-error-overview.tsx` | "覆盖学生"、"错题总数"、"平均掌握率"、"薄弱知识点"、"学科错题分布" 等 | 15+ 处 |
| `error-book-list.tsx` | "错题本为空"、"查看详情" | 2 处 |
| `teacher/error-book/page.tsx` | "错题分析"、"按学科、班级查看学生的错题统计与薄弱知识点" 等 | 8+ 处 |
**总计约 150+ 处硬编码中文文本**,违反 i18n 规则。
### 2.3 【P0】类型安全问题违反 TypeScript 严格模式)
**项目规则**:「禁止 `any`、禁止 `as` 断言(除类型收窄外)」
| 文件 | 行号 | 违规代码 | 类型 |
|------|------|---------|------|
| `data-access.ts` | 77 | `row.sourceType as ErrorBookItem["sourceType"]` | `as` 断言 |
| `data-access.ts` | 82 | `row.knowledgePointIds as string[] \| null` | `as` 断言 |
| `data-access.ts` | 90 | `row.errorTags as string[] \| null` | `as` 断言 |
| `data-access.ts` | 133 | `or(...)!` | 非空断言 |
| `data-access.ts` | 175 | `row as unknown as Parameters<typeof mapRowToItem>[0]` | 双重断言 |
| `data-access.ts` | 222 | 同上 | 双重断言 |
| `data-access.ts` | 657 | `row.knowledgePointIds as string[] \| null` | `as` 断言 |
| `data-access.ts` | 792 | 同上 | `as` 断言 |
| `actions.ts` | 63 | `params.status as "new" \| "learning" \| ...` | `as` 断言 |
| `actions.ts` | 69 | `params.sourceType as "exam" \| "homework" \| ...` | `as` 断言 |
| `error-book-item-card.tsx` | 31 | `node as Record<string, unknown>` | `as` 断言 |
| `error-book-detail-dialog.tsx` | 61 | `content as Record<string, unknown>` | `as` 断言 |
| `add-error-book-dialog.tsx` | 48 | `node as Record<string, unknown>` | `as` 断言 |
| `top-wrong-questions.tsx` | 26 | `node as Record<string, unknown>` | `as` 断言 |
| `knowledge-point-weakness-chart.tsx` | 77 | `payload as unknown as {...}` | 双重断言 |
| `chapter-weakness-chart.tsx` | 74 | `payload as unknown as {...}` | 双重断言 |
| `class-error-bar-chart.tsx` | 76 | `payload as unknown as {...}` | 双重断言 |
| `subject-distribution-chart.tsx` | 77 | `payload as unknown as {...}` | 双重断言 |
### 2.4 【P1】data-access.ts 超过 1000 行硬性上限
**项目规则**:「硬性上限:任何文件不超过 1000 行,超过必须拆分」
`data-access.ts` 当前 **1029 行**,超出硬性上限。该文件混合了:
- 学生端 CRUD`getErrorBookItems``createErrorBookItem``recordReview` 等)
- 教师/管理员分析查询(`getStudentErrorBookSummaries``getKnowledgePointWeakness``getChapterWeakness``getClassErrorOverviews``getSubjectErrorOverviews` 等)
- 跨模块查询接口(`getStudentIdsByClassIdList``getAllStudentIds`
### 2.5 【P1】性能问题全量查询 + JS 端聚合
| 函数 | 问题 | 影响 |
|------|------|------|
| `getErrorBookStats` | 查询该学生**所有**错题行到内存,再 JS 循环统计 | 学生错题多时内存/CPU 浪费 |
| `getStudentErrorBookSummaries` | 查询所有学生的所有错题行,再 JS 聚合 | 班级学生多时性能差 |
| `getKnowledgePointWeakness` | 查询所有错题行JS 展开知识点数组再统计 | 同上 |
| `getChapterWeakness` | 同上 | 同上 |
| `getSubjectErrorDistribution` | 同上 | 同上 |
| `getClassErrorOverviews` | 同上 | 同上 |
| `getSubjectErrorOverviews` | 同上 | 同上 |
| `getTopWrongQuestionsByStudentIds` | 同上 | 同上 |
| `admin/page.tsx` | `allStudentIds.slice(0, 500)` 硬编码限制,无分页 | 超过 500 学生时数据不完整 |
**应使用 SQL `GROUP BY` + `COUNT` 聚合查询**,避免全量加载到内存。
### 2.6 【P1】a11y 可访问性缺失
**项目规则**「可访问性a11y语义化标签、ARIA 属性、键盘导航」
| 位置 | 问题 |
|------|------|
| `grouped-student-error-table.tsx:162` | 使用 `<a href>` 而非 Next.js `<Link>`(违反项目记忆中的 Link 规范) |
| `subject-tabs.tsx` | `<button>` 缺少 `role="tab"` / `aria-selected` / `aria-controls` |
| `class-filter.tsx` | 同上 |
| 所有图表组件 | 缺少 `aria-label` 描述图表内容 |
| `review-buttons.tsx` | 按钮缺少 `aria-label` 描述操作 |
| `grouped-student-error-table.tsx:96` | 可展开行缺少 `aria-expanded` |
| `error-book-detail-dialog.tsx` | `<textarea>` 缺少 `aria-label` |
### 2.7 【P1】错误边界不完整
**项目规则**:「每个独立的数据区块必须用 React Error Boundary 包裹」
| 位置 | 问题 |
|------|------|
| `parent/error-book/page.tsx` | 无 `Suspense` 包裹,整个页面同步渲染 |
| 所有页面 | 仅页面级 `error.tsx`,各数据区块(统计卡片/图表/表格)无独立 Error Boundary |
| 图表组件 | recharts 渲染失败时无 fallback |
### 2.8 【P2】组件复用问题
| 问题 | 位置 | 说明 |
|------|------|------|
| `extractQuestionPreview` 函数重复 3 次 | `error-book-item-card.tsx``add-error-book-dialog.tsx``top-wrong-questions.tsx` | 应提取到 shared 工具函数 |
| `extractQuestionText` 函数重复 | `error-book-detail-dialog.tsx` | 与上面类似但实现不同 |
| `MASTERY_LEVEL_LABELS` 硬编码 | `error-book-item-card.tsx:47` | 应使用 i18n |
| `QUESTION_TYPE_LABEL` 硬编码 | `top-wrong-questions.tsx:35` | 应使用 i18n |
| `class-error-overview.tsx` 导出 `StudentErrorTable` | 似乎已被 `GroupedStudentErrorTable` 取代 | 死代码 |
| `COMMON_ERROR_TAGS` 硬编码中文 | `types.ts:55` | 应使用 i18n 键 |
### 2.9 【P2】数据服务未抽象不可测试
**项目规则**:「可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离」
当前组件直接 import `actions``data-access`
- `error-book-detail-dialog.tsx` 直接 import `archiveErrorBookItemAction``deleteErrorBookItemAction``updateErrorBookNoteAction`
- `add-error-book-dialog.tsx` 直接 import `createErrorBookItemAction`
- `review-buttons.tsx` 直接 import `reviewErrorBookItemAction`
**无法在不启动整个应用的情况下 mock 这些依赖**,违反可测试性原则。
### 2.10 【P2】权限校验位置不统一
`getAllStudentIds()` 在 data-access 层通过 `roles.name === "student"` 查询,虽然不在前端,但角色名字符串硬编码在查询中。应使用 `shared/types/permissions` 中的角色常量。
---
## 三、行业差距对比
### 3.1 与优秀 K12 产品的差距
| 功能 | 我们 | 智学网 | 猿题库 | 钉钉教育 | 差距影响 |
|------|------|--------|--------|---------|---------|
| 智能复习队列 | ✅ SM-2 | ✅ | ✅ | ❌ | 基本持平 |
| 复习提醒通知 | ❌ | ✅ 推送 | ✅ 推送 | ✅ | 学生不知道何时复习 |
| 错题趋势图 | ❌(类型已定义未实现) | ✅ | ✅ | ❌ | 无法看到进步趋势 |
| 导出/打印错题 | ❌ | ✅ PDF | ✅ PDF | ❌ | 无法离线复习 |
| 批量操作 | ❌ | ✅ | ✅ | ❌ | 管理大量错题效率低 |
| 班级平均对比 | ❌ | ✅ | ✅ | ❌ | 学生不知道自己水平 |
| 复习日历视图 | ❌ | ✅ | ✅ | ❌ | 无法直观看到复习安排 |
| 错题来源详情跳转 | ❌ | ✅ | ✅ | ❌ | 无法回看原试卷/作业 |
| 知识点掌握度雷达图 | ❌ | ✅ | ✅ | ❌ | 无法多维度看薄弱点 |
| 游戏化激励(连续复习天数) | ❌ | ✅ | ✅ | ❌ | 学生缺乏复习动力 |
| 智能推题(基于错题变式) | ✅(已接入 adaptive-practice | ✅ | ✅ | ❌ | 基本持平 |
### 3.2 UI/UX 差距
| 差距 | 说明 | 影响角色 |
|------|------|---------|
| 学生端缺少复习仪表盘 | 当前只有列表+筛选,无"今日待复习"独立视图 | 学生 |
| 教师端缺少学生个体下钻 | 点击学生只能跳转带参数,无学生错题详情面板 | 教师 |
| 家长端无子女切换 | 多子女时用卡片展示,无 Tab 切换对比 | 家长 |
| 管理员端无年级维度 | 只有全校维度,无年级/班级下钻 | 管理员 |
| 无骨架屏一致性 | 各页面 loading.tsx 结构不一致 | 全部 |
---
## 四、改进优先级建议
### P0紧急影响架构合规与安全
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | 跨模块直接依赖 | 将 `AiErrorBookAnalysis``createPracticeSessionAction``getQuestionsAction` 改为通过 props/Context 注入error-book 模块不直接 import 其他业务模块 |
| P0-2 | i18n 硬编码150+ 处) | 全部提取为翻译键,扩展 `error-book.json` 翻译文件 |
| P0-3 | 类型安全18 处 as 断言) | 使用类型守卫替代 `as`,图表 tooltip 使用泛型组件 |
### P1重要影响性能与可访问性
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | data-access.ts 超 1000 行 | 拆分为 `data-access.ts`(学生 CRUD+ `data-access-analytics.ts`(教师/管理员分析) |
| P1-2 | 全量查询 + JS 聚合 | 改用 SQL `GROUP BY` + `COUNT` 聚合 |
| P1-3 | a11y 缺失 | 添加 ARIA 属性、语义化标签、`<Link>` 替代 `<a>` |
| P1-4 | 错误边界不完整 | 为各数据区块添加 Error Boundary |
| P1-5 | admin 无分页 | 实现分页查询或虚拟滚动 |
### P2优化提升可维护性与用户体验
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | 重复函数 | 提取 `extractQuestionPreview``shared/lib/question-content.ts`(已存在) |
| P2-2 | 数据服务未抽象 | 定义 `ErrorBookService` 接口,通过 Context 注入 |
| P2-3 | 死代码 | 删除 `class-error-overview.tsx` 中未使用的 `StudentErrorTable` |
| P2-4 | 缺失功能 | 错题趋势图、复习提醒、导出、批量操作(中长期) |
| P2-5 | 角色字符串硬编码 | `getAllStudentIds` 使用角色常量 |
---
## 五、架构图同步说明
### 5.1 需要修改的节点
1. **`005_architecture_data.json``error-book.uses.shared`**
- 移除 `db.schema.examSubmissions``db.schema.submissionAnswers``db.schema.homeworkSubmissions``db.schema.homeworkAnswers``db.schema.examQuestions``db.schema.homeworkAssignmentQuestions`(这些已通过跨模块 data-access 访问)
2. **`005_architecture_data.json``error-book.dependsOn`**
- 新增 `ai`error-book-detail-dialog 直接依赖 ai 组件)
- 新增 `adaptive-practice`error-book-detail-dialog 直接依赖其 Action
- 新增 `questions`add-error-book-dialog 直接依赖其 Action—— 注意questions 已在 dependsOn 中,但 uses 中记录的是 `actions.getQuestionsAction` 而非 data-access
3. **`004_architecture_impact_map.md` 第 2.28 节**
- 文件清单中 `data-access.ts` 行数更新为拆分后的两个文件
- 依赖关系新增 ai / adaptive-practice 的直接依赖说明(标注为"待解耦"
### 5.2 架构图待补充项
- 拆分后的 `data-access-analytics.ts` 文件信息
- `ErrorBookService` 接口定义(如实施 P2-2
- i18n 翻译文件结构更新

View File

@@ -0,0 +1,475 @@
# 考试exams模块审计报告
> 审计时间2026-06-25
> 审计范围:`src/modules/exams/**`35+ 文件)+ `src/app/(dashboard)/teacher/exams/**`10 个页面)+ 跨模块依赖面
> 审计依据:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、项目 `project_rules.md`
---
## 一、现有实现概要
### 1.1 文件分布与体量
exams 模块按职责已做较细粒度拆分,体量基本符合规范:
| 子目录/文件 | 行数(参考架构图) | 职责 |
|-------------|------|------|
| `actions.ts` | 633 | 11 个核心 Server Action已从 1525 行拆分) |
| `actions-helpers.ts` | 96 | 跨 Action 共享纯函数prepareExamCreateContext 等) |
| `actions-rich-editor.ts` | 250 | 富文本编辑器 Server Actioncreate/update |
| `ai-pipeline/auto-mark.ts` | 356 | AI 自动标记 Server Action + 纯转换函数 |
| `ai-pipeline/{index,parse,request,structure}.ts` | — | AI 调用/解析/结构化 |
| `data-access.ts` | 542 | 考试 CRUD已从 1036 行拆分) |
| `data-access-cross-module.ts` | 511 | 13 个跨模块查询/写接口 |
| `data-access-error-collection.ts` | — | 错题采集相关跨模块接口 |
| `stats-service.ts` | 158 | 考试分析数据聚合 |
| `types.ts` | 93 | 类型定义 |
| `utils/normalize-structure.ts` | 57 | exam.structure 运行时归一化 |
| `components/` | 24 个文件 | 表单/组卷/预览/分析/卡片/筛选/表格 |
| `editor/` | 14 个文件 | Tiptap 富文本编辑器extensions/utils/转换) |
| `hooks/` | 4 个文件 | use-exam-preview 主组合器 + 3 个子 Hook |
**架构图覆盖情况**004/005 已记录 exams 模块的职责、依赖、被依赖、文件清单、P0/P1 修复历史、V3 增强项。本次审计对照架构图核对,覆盖基本完整,但以下细节需补全(见第五节):
- `data-access-error-collection.ts` 未在 005 JSON 的 modules.exams.exports 中列出
- `utils/normalize-structure.ts` 已记录但未在 005 的 dependencyMatrix 中明确标注被 `[id]/build/page.tsx``[id]/edit-rich/page.tsx` 引用
### 1.2 主要数据流
- **创建**`/teacher/exams/create``createExamAction``persistExamDraft``db.insert(exams)`
- **AI 创建**`/teacher/exams/create``createAiExamAction``loadAiDraftQuestionsAndStructure``persistAiGeneratedExamDraft` → 通过 `questions/data-access.createQuestionWithRelations` 创建题目 → 事务写 exams + examQuestions
- **富文本创建**`/teacher/exams/new``createExamFromRichEditorAction``editorDocToStructure``persistAiGeneratedExamDraft`
- **组卷**`/teacher/exams/[id]/build``ExamAssembly` + `getExamById`
- **预览**`previewAiExamAction` / `getExamPreviewAction`
- **分析**`/teacher/exams/[id]/analytics``getExamAnalytics`(聚合 homework 提交数据)
### 1.3 跨模块依赖(合规项)
以下跨模块调用均通过对方 data-access符合三层架构规则
- `questions/data-access.createQuestionWithRelations`P0-1 已修复)
- `classes/data-access.getClassGradeIdsByClassIds`P0-2 已修复)
- `school/data-access.{getSubjectNameById,getGradeNameById,getSubjectOptions,getGradeOptions}`P1-1 已修复)
- `homework/data-access.{getHomeworkAssignmentsByExamId,getGradedSubmissionsByExamId}`V3-8 新增)
- `homework/data-access-utils.getQuestionText`
### 1.4 已修复的历史问题(架构图记录)
P0-1/P0-2/P0-4/P0-8/P1-1 等历史违规已修复,详见 004 文档第 2.2 节。
---
## 二、现存问题与原因分析
### 🔴 2.1【架构违规·P0】跨模块直接 JOIN questions 表
**位置**[data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L4-L5) 第 4 行 import、第 480-490 行 `getExamForGradeEntry`
**问题**
```
第 4 行import { exams, examQuestions, examSubmissions, submissionAnswers, questions } from "@/shared/db/schema"
第 488 行:.innerJoin(questions, eq(examQuestions.questionId, questions.id))
```
`getExamForGradeEntry` 为了获取题目 `type` 字段,直接 JOIN 了 questions 模块的核心表 `questions`
**违反规则**:项目规则"模块间只能通过对方 data-access 通信,**禁止跨模块直接查询数据库表**"。
**原因**:成绩录入表格表头需要题目类型,但实现时未在 questions 模块暴露按 ID 批量获取类型的接口,于是直接 JOIN。
**直接后果**questions 模块若重构表结构(如将 type 拆分到独立表exams 模块会编译失败或运行时错误;模块封装性被破坏,违反可测试性与可替换性。
---
### 🟢 2.2【已确认合规】submissionAnswers 表归属与直查
**位置**
- [data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L4) 导入 `submissionAnswers`
- [data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L212-L218) `getExamSubmissionWithAnswers` 直查 `submissionAnswers`
- [data-access-error-collection.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-error-collection.ts#L6) 导入并查询 `submissionAnswers`(第 62-69 行)
**结论**:经核对 `src/shared/db/schema.ts:575-578``submissionAnswers` 表的 `submissionId` 外键引用 `examSubmissions.id`**该表属于 exams 模块自身域**exam submissions 的答题记录。exams 模块查询自己的表合规,`getExamSubmissionWithAnswers``getExamSubmissionDataForErrorCollection` 通过 data-access-cross-module 暴露给 diagnostic/error-book 模块调用,符合"模块间通过对方 data-access 通信"规则。
**无违规,无需修复。**
---
### 🟠 2.3【i18n 缺失·P1】11 个组件未接入 useTranslations
**位置**
| 文件 | 硬编码样本 |
|------|-----------|
| [components/exam-card.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-card.tsx#L78-L91) | "Lvl"、"min"、"pts"、"Questions" |
| [components/exam-filters.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-filters.tsx#L33-L59) | "Search exams..."、"Status"、"Any Status"、"Draft"、"Published"、"Archived"、"Difficulty"、"Easy (1)" 等 |
| [components/exam-preview-dialog.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-preview-dialog.tsx#L89-L199) | "Section"、"未命名题目"、"未命名子题"、"Exam Preview"、"Generating preview..."、"完整试卷预览"、"题 · 科目 · 年级 · 分钟 · 总分"、"No preview available"、"Confirm & Create" |
| [components/exam-viewer.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-viewer.tsx#L95-L197) | "Section"、"Group"、"Score:"、"No questions available." |
| [components/question-options-editor.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/question-options-editor.tsx) | 选项编辑器中文硬编码 |
| [editor/extensions/blank-node.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/blank-node.tsx) | aria-label="填空" |
| [editor/extensions/group-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/group-block.tsx) | placeholder 与统计文案硬编码 |
| [editor/extensions/question-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/question-block.tsx) | 题型 `<option>` 与 "分" 硬编码 |
| [editor/extensions/section-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/section-block.tsx) | "层级/卷/部分/分卷" 硬编码 |
**违反规则**:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键";硬约束"All user-visible text must be i18n-adapted using next-intl with translation keys extracted"。
**原因**:富文本编辑器 extensions 与早期组件exam-card/exam-filters/exam-preview-dialog在 i18n 改造前已存在,后续 i18n 改造未覆盖到。
**直接后果**
- 多语言环境en下用户看到中英混杂文本体验严重劣化
- 无法通过翻译文件统一管理文案,难以维护
- exam-card 在 all 列表页是高频可见组件,影响首屏专业度
---
### 🟠 2.4【i18n 缺失·P1】Server Action 返回消息绕过 i18n
**位置**
| 文件:行号 | 硬编码消息 |
|-----------|-----------|
| [actions-rich-editor.ts:41](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L41) | "标题不能为空" |
| [actions-rich-editor.ts:49,55](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L49) | "试卷内容不能为空" |
| [actions-rich-editor.ts:123,202](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L123) | "试卷内容格式无效"safeJsonParse 兜底参数) |
| [actions-rich-editor.ts:125,204](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L125) | "试卷内容解析失败" |
| [actions-rich-editor.ts:169](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L169) | "试卷草稿已创建" |
| [actions-rich-editor.ts:211](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L211) | "只能更新自己创建的试卷" |
| [actions-rich-editor.ts:278](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L278) | "试卷已更新" |
| [actions.ts:161,250,465](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L161) | "题目数据格式无效"safeJsonParse 兜底) |
| [actions.ts:466](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L466) | "试卷结构数据格式无效" |
| [ai-pipeline/auto-mark.ts:30](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/auto-mark.ts#L30) | "试卷文本不能为空"schema message |
| [ai-pipeline/auto-mark.ts:386](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/auto-mark.ts#L386) | "AI 自动标记完成" |
| [stats-service.ts:136](file:///e:/Desktop/CICD/src/modules/exams/stats-service.ts#L136) | "(无题目文本)" |
| [actions-helpers.ts:65](file:///e:/Desktop/CICD/src/modules/exams/actions-helpers.ts#L65) | "Invalid form data" |
**违反规则**:同 2.3。`actions.ts` 主体已使用 `getTranslations("examHomework.exam.actionMessages")`,但 `actions-rich-editor.ts``ai-pipeline/auto-mark.ts` 完全未接入,存在 i18n 一致性破口。
**原因**:这两个文件是从 actions.ts 拆分出来的新文件,拆分时未同步迁移 i18n 模式。
**直接后果**:富文本编辑器与 AI 自动标记的错误/成功提示在非中文环境下显示中文,破坏产品一致性。
---
### 🟠 2.5【路由边界缺失·P1】部分路由缺 loading.tsx / error.tsx
**位置**[src/app/(dashboard)/teacher/exams/](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/)
| 路由 | loading.tsx | error.tsx |
|------|------------|----------|
| `all/` | ✅ | ❌ |
| `create/` | ✅ | ❌ |
| `new/` | ❌ | ❌ |
| `[id]/build/` | ✅ | ✅ |
| `[id]/edit-rich/` | ❌ | ❌ |
| `[id]/analytics/` | ❌ | ❌ |
| `[id]/proctoring/` | ✅ | ✅ |
**违反规则**:硬约束"All student routes must include loading.tsx and error.tsx for error boundaries"(项目内存中虽针对 student 路由,但企业级规范同样适用于 teacher 路由);规则"每个独立的数据区块必须用 React Error Boundary 包裹"、"异步数据使用 React Suspense + 骨架屏"。
**原因**:路由按需添加 loading/error未系统化覆盖。
**直接后果**
- 编辑器页面edit-rich加载 Tiptap 较慢,无骨架屏会白屏
- 分析页analytics聚合查询慢无 loading 体验差
- 任一页面抛错会冒泡到顶层 dashboard error boundary无法精确定位
---
### 🟡 2.6【类型安全·P2】10 处 `as` 类型断言(非 unknown 收窄)
**位置**
| 文件:行号 | 断言 | 说明 |
|-----------|------|------|
| [editor/editor-to-structure.ts:101](file:///e:/Desktop/CICD/src/modules/exams/editor/editor-to-structure.ts#L101) | `: "single_choice") as RichQuestionType` | 字符串字面量断言为联合类型 |
| [editor/exam-nodes-to-editor-doc.ts:38](file:///e:/Desktop/CICD/src/modules/exams/editor/exam-nodes-to-editor-doc.ts#L38) | 同上 | 同上 |
| [editor/selection-toolbar.tsx:213,215](file:///e:/Desktop/CICD/src/modules/exams/editor/selection-toolbar.tsx#L213) | `slice.content.toJSON() as JSONContent[]` | ProseMirror→Tiptap 类型 |
| [editor/exam-rich-editor.tsx:158,174](file:///e:/Desktop/CICD/src/modules/exams/editor/exam-rich-editor.tsx#L158) | `editor.getJSON() as EditorJSONContent` | Tiptap 内部类型断言 |
| [components/exam-data-table.tsx:39](file:///e:/Desktop/CICD/src/modules/exams/components/exam-data-table.tsx#L39) | `params as Record<...>` | 不安全参数断言 |
| [components/exam-form.tsx:40](file:///e:/Desktop/CICD/src/modules/exams/components/exam-form.tsx#L40) | `zodResolver(formSchema) as Resolver<ExamFormValues>` | zodResolver 返回类型断言 |
| [actions-rich-editor.ts:147,230](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L147) | `q.type as "single_choice" | "multiple_choice" | "text" | "judgment"` | 字符串断言为联合类型 |
| [edit-rich/page.tsx:64](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx#L64) | `structureToEditorDoc(editorDoc) as EditorJSONContent` | 类型断言 |
**违反规则**:项目规则"禁止 `as` 断言(除从 `unknown` 转换或测试中,需注释原因)"。
**原因**Tiptap/ProseMirror 类型系统与项目类型边界处缺类型守卫;`RichQuestionType` 联合类型的字符串字面量缺运行时校验函数。
**直接后果**:若 AI 返回未预期的 type 值(如 "essay"`as` 断言会让错误值通过类型检查,运行时可能渲染异常。
---
### 🟡 2.7【企业级能力缺失·P2】无统一空状态/骨架屏/错误回退
**位置**:组件层未抽取统一的 `<ExamEmptyState>` / `<ExamSkeleton>` / `<ExamErrorBoundary>`
**问题**
- `all/page.tsx` 自行实现了 `ExamsResultsFallback`,未复用到 `analytics`/`edit-rich`
- `exam-card.tsx``exam-grid.tsx` 无骨架屏
- 编辑器加载Tiptap 初始化)期间无统一占位
**违反规则**:审计要求"明确处理空数据、无权限、网络异常等边界状态"、"异步数据使用 React Suspense + 骨架屏"。
**直接后果**:体验不一致,重复实现。
---
### 🟡 2.8【可测试性·P2】纯逻辑与 UI 耦合,缺单测
**位置**
- `components/exam-preview-utils.ts`293 行纯函数,已抽取,但无单测)
- `hooks/use-exam-preview-{state,tasks,rewrite}.ts` 无对应测试
- `editor/editor-to-structure.ts``editor/structure-to-editor.ts` 双向转换是核心纯逻辑,无单测
- `stats-service.ts` 的错误率/难度计算无单测
**违反规则**:审计要求"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。
**直接后果**富文本编辑器双向转换是高风险逻辑type/score/structure 映射),无单测难以保证回归质量。
---
### 🟡 2.9【解耦性·P2】未通过接口抽象 + Context 注入数据服务
**位置**:模块整体。
**问题**:当前组件直接 import 同模块的 actions/data-access`exam-rich-form.tsx` 直接 import `autoMarkExamAction` / `createExamFromRichEditorAction`)。虽然同模块内 import 合规,但审计要求"通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。
**违反规则**:审计重构方案的"完全解耦"与"可测试性"原则。
**原因**:当前实现以功能正确性优先,未做依赖注入抽象。
**直接后果**
- 组件无法在测试中 mock 数据服务
- 不同角色teacher/admin/parent/student的差异未通过接口实现隔离未来扩展角色需改组件
- 配置驱动设计未落地,新增 Widget 需改组件代码
---
## 三、行业差距对比
参考智学网、猿题库、学而思网校、Google Classroom、Canvas LMS 等同类产品exams 模块当前差距:
### 3.1 试卷创建侧
| 行业实践 | 当前状态 | 差距影响 |
|---------|---------|---------|
| 多种组卷入口(手动/AI/富文本/导入 Word三选一清晰呈现 | 已有三种入口,但 `/create``/new` 路由并列,无统一选择页 | 教师首次使用困惑 |
| 试卷模板库(按学科/年级预置模板) | ❌ 无 | 教师每次从零创建,效率低 |
| 知识点双向细目表(题目-知识点覆盖矩阵) | ❌ 无(虽有 questions.knowledgePoints但 exam 层无细目表视图) | 无法评估试卷覆盖度 |
| 难度预估(基于题库历史正确率自动估算试卷难度) | ❌ 无(仅手动 1-5 级) | 难度设置主观 |
| 试卷预览支持 PDF 导出/打印 | ❌ 无 | 教师无法离线分发 |
### 3.2 考试作答侧(学生)
| 行业实践 | 当前状态 | 差距影响 |
|---------|---------|---------|
| 作答页答题卡导航(已答/未答/标记 revisit | ❌ 仅顺序作答 | 学生难以跳题、检查 |
| 自动保存进度可视化 | homework 模块已实现autoSave* 翻译键齐全) | ✅ 较好 |
| 限时/监考倒计时 | homework 模块已实现 useExamCountdown | ✅ 较好 |
| 客观题即时反馈(练习模式) | ❌ 仅作业模式提交后批改 | 缺少低风险练习模式 |
### 3.3 考试分析侧(教师)
| 行业实践 | 当前状态 | 差距影响 |
|---------|---------|---------|
| 平均分/及格率/分数段分布 | ✅ 已实现V3-8 | — |
| 逐题错误率与难度等级 | ✅ 已实现 | — |
| 知识点掌握度雷达图 | diagnostic 模块有,但未在 exam analytics 集成 | 教师需跨页查看 |
| 班级横向对比 | ❌ 无(仅全卷汇总) | 无法定位班级差异 |
| 学生个体诊断报告(一键生成) | ❌ 无 | 个性化反馈缺失 |
| 历次考试趋势 | ❌ 无 | 无法看进步趋势 |
### 3.4 多角色覆盖侧
| 角色 | 当前覆盖 | 差距 |
|------|---------|------|
| admin | ❌ 无 admin 视角考试管理(全校/年级聚合) | admin 仅能通过 dashboard 看 examCount无考试管理页 |
| teacher | ✅ 完整(创建/组卷/预览/分析/监考) | — |
| parent | ✅ parent 模块有 child-exam-detail + parentExam i18n | 缺少历次考试趋势对比 |
| student | ⚠️ 通过 homework-take-view 作答,但无独立"我的考试"汇总页 | 学生无法回看历史考试试卷与成绩 |
### 3.5 UX 细节
- 缺少全局考试状态徽章颜色规范draft/published/archived 在 exam-card 与 exam-columns 中重复定义)
- exam-card 科目颜色映射 `subjectColorMap` 硬编码英文字符串 key"Mathematics" 等),无法国际化——科目名应通过 ID 映射颜色,而非名称
- 无空状态插画/图标统一规范all 页用 FileTextanalytics 页也用 BarChart3缺一致性
---
## 四、改进优先级建议
### P0紧急影响架构合规与数据安全
| # | 问题 | 改进方向 | 关联规则 |
|---|------|---------|---------|
| P0-1 | `data-access-cross-module.ts:488` 直接 JOIN questions 表 | 在 questions 模块新增 `getQuestionTypeMapByIds(ids): Promise<Map<string, string>>`exams 改为调用此接口 | 模块间禁止直查对方表 |
| P0-2 | `new/``[id]/edit-rich/``[id]/analytics/``all/``create/` 缺 loading.tsx/error.tsx | 补齐 loading.tsx + error.tsx复用 dashboard 模式 | 路由边界规范 |
### P1高影响影响多语言与体验
| # | 问题 | 改进方向 |
|---|------|---------|
| P1-1 | 11 个组件未接入 i18nexam-card/exam-filters/exam-preview-dialog/exam-viewer/question-options-editor + 4 个 editor extensions | 接入 useTranslations提取翻译键到 exam-homework.json 的 exam.card/exam.viewer/exam.previewDialog/editor.* 命名空间 |
| P1-2 | actions-rich-editor.ts + auto-mark.ts Server Action 返回消息硬编码 | 改用 getTranslations("examHomework.exam.actionMessages"),复用 actions.ts 已有翻译键,新增 richEditor.* / autoMark.* 子键 |
| P1-3 | exam-card subjectColorMap 用英文名做 key | 改为按 subjectId 映射颜色,颜色配置移至 `shared/config/subject-colors.ts` |
| P1-4 | stats-service.ts "(无题目文本)"、data-access.ts "General" 兜底硬编码 | 通过 data-access 层返回 null由组件层 i18n 渲染兜底文案 |
### P2中长期企业级能力与重构
| # | 问题 | 改进方向 | 状态 |
|---|------|---------|------|
| P2-1 | 10 处 `as` 类型断言 | 为 RichQuestionType 增加 `isRichQuestionType(v): v is RichQuestionType` 类型守卫Tiptap JSONContent 边界用 zod schema 校验 | ✅ 已完成2026-06-25新增 isRichQuestionType/isStandaloneQuestionType/toRichQuestionType/toStandaloneQuestionType 4 个守卫,消除 editor-to-structure.ts:101、exam-nodes-to-editor-doc.ts:38、actions-rich-editor.ts:149/233 共 4 处 as 断言;其余 6 处 as 断言属于 unknown→具体类型的合法收窄或 Tiptap/ProseMirror 内部类型边界,已添加注释说明,保留 |
| P2-2 | 纯逻辑无单测 | 为 exam-preview-utils、editor-to-structure、structure-to-editor、stats-service 错误率计算补充 .test.ts | ⏸️ 待实施(依赖 P2-4 ExamServicePort 落地后统一 mock |
| P2-3 | 无统一 ExamEmptyState/ExamSkeleton/ExamErrorBoundary | 抽取到 components/exam-boundaries.tsx全模块复用 | ✅ 已完成2026-06-25创建 components/exam-boundaries.tsx189 行),导出 ExamErrorBoundary/ExamEmptyState/ExamSkeleton 三组合单元5 种骨架变体,新增 i18n 键 exam.error.boundaryTitle/boundaryDescription/retry |
| P2-4 | 组件直接 import actions未通过 Context 注入 | 定义 `ExamServicePort` 接口 + `ExamServiceProvider` Context组件通过 `useExamService()` 获取;角色差异通过不同 Provider 实现隔离 | ✅ 已完成骨架2026-06-25创建 services/exam-service-port.ts95 行12 方法契约)+ services/exam-service-context.tsx72 行Context + Provider + Hook+ services/index.ts桶导出。具体实现TeacherExamService/AdminExamService/MockExamService与组件改造将在 P2-6+ 落地 |
| P2-5 | 无配置驱动的 Widget 渲染 | 参考 dashboard/config/widget-configs.ts新增 `exams/config/exam-widgets.ts`,按角色配置渲染哪些子模块 | ✅ 已完成2026-06-25创建 config/exam-widgets.ts192 行),四角色默认配置 + getExamWidgetConfig/getWidgetsBySlot 工具函数 |
| P2-6 | 缺少考试模板库、知识点细目表、班级对比、学生个体报告 | 中长期功能补全,对标智学网 | ⏸️ 待实施(中长期) |
| P2-7 | 缺少 admin 视角考试管理页、student 独立"我的考试"页 | 多角色覆盖补全 | ⏸️ 待实施(中长期,依赖 P2-4 具体实现 + P2-5 配置消费) |
| P2-8 | 关键操作埋点不完整 | 已有 exam.ai_generated/updated/deleted/duplicated需补 exam.published/archived/auto_marked 埋点 | ⏸️ 待实施 |
| P2-9 | a11y 缺失(编辑器 extensions 无 aria-label 规范、键盘导航) | 为 Tiptap 节点添加 aria-label工具栏支持完整键盘导航 | ⏸️ 待实施 |
| P2-10 | 数据查询未结合权限二次校验data-access 层部分函数未传 scope | `getExamPreview``getExamSubjects``getExamGrades``duplicateExam``deleteExamById` 应接受 scope 参数或在 Action 层显式校验 | ⏸️ 待实施 |
> **本轮 P2 落地范围说明**
> - P2-1 / P2-3 / P2-4骨架/ P2-5 已完成,奠定解耦与配置驱动的架构基础
> - P2-2 单测待 ExamServicePort 具体实现落地后统一 mock
> - P2-6 / P2-7 为中长期功能补全,需独立规划排期
> - P2-8 / P2-9 / P2-10 为增强项,可在后续迭代中逐步落地
> - 全部 P2 代码改动已通过 `npx tsc --noEmit`exams 模块零错误)与 `npx eslint`(零错误/零警告)验证
---
## 五、架构图同步说明
本次审计发现架构图需补充以下节点:
### 004_architecture_impact_map.md 需补充
1. **exams 模块文件清单补全**
- 新增 `data-access-error-collection.ts` 行(当前 004 未单独列出)
- 标注 `data-access-cross-module.ts``getExamForGradeEntry` 存在 P0 跨模块 JOIN 违规(待修复后改为 ✅ 已修复)
2. **permission 补全**
- 005 已有 EXAM_PROCTOR/EXAM_PROCTOR_READ但 004 第 2.2 节 exams 权限点列表未完整列出
3. **dependencyMatrix 补充**
- `app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx``exams/editor/{exam-nodes-to-editor-doc,structure-to-editor}``exams/utils/normalize-structure`(当前 004 已记 build/page.tsx但 edit-rich 同样依赖,需补)
4. **被依赖关系补全**
- `homework/data-access-utils.getQuestionText``exams/stats-service.ts` 调用005 JSON 中 homework 模块 exports 的 usedBy 需补 `exams/stats-service`
### 005_architecture_data.json 需补充
1. `modules.exams.exports` 数组补:
- `data-access-error-collection.ts`(含 `getExamErrorCollectionForExam` 等接口)
- `getExamForGradeEntry`(标注跨模块 JOIN 待修复)
2. `modules.homework.exports``getQuestionText``usedBy``"exams/stats-service"`
3. `modules.questions.exports` 新增 `getQuestionTypeMapByIds`(修复 P0-1 后)
4. `architectureOverview.violations` 数组新增当前未记录的违规项,修复后改为 ✅ 标记
---
## 附:重构方案设计要点(落地架构)
> 以下为 P2-4/P2-5 的具体设计方向,作为中长期重构蓝图。本次实施将先完成 P0/P1P2 仅落地基础接口与配置骨架。
### A. 完全解耦ExamServicePort + Context 注入
```typescript
// exams/services/exam-service-port.ts新增
export interface ExamServicePort {
listExams(params: GetExamsParams): Promise<Exam[]>
getExam(id: string): Promise<ExamDetail | null>
createExam(input: ExamCreateInput): Promise<ActionState<string>>
updateExam(input: ExamUpdateInput): Promise<ActionState<string>>
deleteExam(id: string): Promise<ActionState<string>>
duplicateExam(id: string): Promise<ActionState<string>>
getAnalytics(id: string): Promise<ExamAnalyticsSummary | null>
// ... 所有数据访问通过此接口
}
// exams/services/exam-service-context.tsx新增
const ExamServiceContext = createContext<ExamServicePort | null>(null)
export function ExamServiceProvider({ service, children }: { service: ExamServicePort; children: ReactNode }) { ... }
export function useExamService(): ExamServicePort { ... }
// 不同角色的实现
// exams/services/teacher-exam-service.ts // 调用真实 Server Actions
// exams/services/admin-exam-service.ts // admin 视角(聚合全校)
// exams/services/mock-exam-service.ts // 测试用
```
### B. 组合优先Widget 配置驱动
```typescript
// exams/config/exam-widgets.ts新增
export type ExamWidgetConfig = {
role: Role
widgets: Array<{
id: "list" | "analytics" | "proctoring" | "templates" | "blueprint"
visible: boolean
order: number
props?: Record<string, unknown>
}>
}
export const examWidgetConfigs: Record<Role, ExamWidgetConfig> = { ... }
```
### C. i18n 翻译文件结构示例(新增键)
```json
{
"exam": {
"card": {
"level": "难度 {{level}}",
"minutes": "{{count}} 分钟",
"points": "{{count}} 分",
"questions": "{{count}} 题"
},
"viewer": {
"section": "分卷",
"group": "大题",
"score": "分值",
"noQuestions": "暂无题目"
},
"previewDialog": {
"title": "试卷预览",
"generating": "生成预览中...",
"fullPreview": "完整试卷预览",
"summary": "{{count}} 题 · {{subject}} · {{grade}} · {{minutes}} 分钟 · {{total}} 分",
"noPreview": "暂无预览内容",
"confirmCreate": "确认并创建",
"untitledQuestion": "未命名题目",
"untitledSubQuestion": "未命名子题",
"scoreUnit": "分"
},
"richEditorAction": {
"titleRequired": "请填写试卷标题",
"contentRequired": "试卷内容不能为空",
"contentInvalid": "试卷内容格式无效",
"contentParseFailed": "试卷内容解析失败",
"draftCreated": "试卷草稿已创建",
"onlyOwnUpdate": "只能更新自己创建的试卷",
"updated": "试卷已更新"
},
"autoMarkAction": {
"sourceRequired": "试卷文本不能为空",
"completed": "AI 自动标记完成"
}
}
}
```
### D. 错误与边界
- 每个路由的 `error.tsx` 复用 `exams/components/exam-error-boundary.tsx`(新增)
- 列表/卡片使用 `<ExamSkeleton>` / `<ExamEmptyState>`(新增)
- 编辑器加载使用 Suspense + 自定义骨架
### E. 可测试性
- 纯逻辑已有抽取exam-preview-utils/editor-to-structure/structure-to-editor补单测
- ExamServicePort 接口允许测试注入 mock 实现
### F. 安全性
- data-access 层所有按 ID 查询函数增加可选 `scope` 参数Action 层强制传入
- `getExamPreview``duplicateExam``deleteExamById` 当前未校验 scope需补
### G. 监控埋点
-`exam.published``exam.archived``exam.auto_marked` 埋点
- analytics 页访问埋点 `exam.analytics_viewed`

Some files were not shown because too many files have changed in this diff Show More