Compare commits

..

83 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
1056 changed files with 93122 additions and 23012 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
### 安全规范
@@ -124,32 +166,61 @@ src/modules/[module]/
## 问题记录规则
**所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`(速查手册格式)。**
**所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`索引式速查手册)。**
### 必须记录的场景
| 场景 | 记录要求 |
|------|---------|
| 构建报错dev/build/lint/tsc | 记录错误现象 + 正确写法 |
| 运行时异常(白屏/API 报错/数据加载失败) | 记录错误现象 + 正确写法 |
| 框架/库版本兼容问题 | 记录错误现象 + 正确写法 |
| 依赖配置问题serverExternalPackages/webpackIgnore 等) | 记录错误现象 + 正确写法 |
| 架构约束违规 | 记录错误现象 + 正确写法 |
| 构建报错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 重构计划,当前已执行完毕 |
### 设计历史文档

File diff suppressed because it is too large Load Diff

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,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,806 @@
[
{
"id": "G1-001",
"file": "src/modules/textbooks/data-access-graph.ts",
"lines": "L7-L13, L46-L53, L107-L121",
"ruleId": "A-06",
"severity": "P0",
"dimension": "architecture",
"title": "textbooks 模块直接查询 questions/diagnostic 模块的表",
"description": "data-access-graph.ts 从 @/shared/db/schema 导入 questionsToKnowledgePoints属 questions 模块)和 knowledgePointMastery属 diagnostic 模块),并直接执行 SELECT FROM 查询L46-53 查 questionsToKnowledgePointsL107-121 查 knowledgePointMastery。这违反了三层架构'模块间通过对方 data-access 通信,不直接查询对方 DB 表'的规则。",
"recommendation": "1) questionsToKnowledgePoints 的关联题目数查询应改为调用 questions 模块 data-access 暴露的跨模块接口(如 getQuestionCountByKpIds2) knowledgePointMastery 查询应改为调用 diagnostic 模块 data-access 暴露的接口(如 getKpMasteryByTextbookId。",
"effort": "M (≤2h)"
},
{
"id": "G1-002",
"file": "src/modules/questions/data-access.ts",
"lines": "L294-L315",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "deleteQuestionRecursive 递归 N+1每个子题单独查询+删除",
"description": "deleteQuestionRecursive 在递归中对每个子题先 SELECT 子题列表L305-308再 for 循环递归调用自身L310-312最后 DELETE 当前题L314。对于有 N 层子题的复合题,会产生 2N 次数据库往返。",
"recommendation": "改为先递归收集所有后代 ID 到一个数组单次查询children即可然后用 inArray 批量 DELETE`await tx.delete(questions).where(inArray(questions.id, allDescendantIds))`。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-003",
"file": "src/modules/questions/data-access.ts",
"lines": "L350-L378",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "deleteQuestionsBatch 循环调用 deleteQuestionRecursive 产生 N+1",
"description": "deleteQuestionsBatch 在 L372-374 对 targetIds 数组 for 循环,每个 id 单独调用 deleteQuestionRecursive每次调用内部又递归查询子题。批量删除 M 个题目时产生 M × (递归深度) 次查询。",
"recommendation": "先将所有 targetIds 的后代 ID 一次性收集(用 inArray 批量查询 parentId in targetIds递归用 Map 解析),再单次 inArray 批量删除所有后代+自身。",
"effort": "M (≤2h)"
},
{
"id": "G1-004",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L128-L140",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "deleteComment 递归 N+1每个子回复单独查询+删除",
"description": "deleteComment 先 SELECT 子回复列表L130-133再 for 循环递归调用 deleteCommentL134-136最后 DELETE 当前评论L137-139。嵌套回复深时产生大量 DB 往返。",
"recommendation": "改为先用单次查询获取该 plan 下所有评论,在内存中构建 parent→children Map收集所有后代 ID 后用 inArray 批量删除。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-005",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L426-L458",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "reorderChapters 循环内逐条 UPDATEN+1",
"description": "reorderChapters 在事务内 for 循环遍历所有兄弟章节L445-457每个章节单独执行 tx.updateL448-454。重排 N 个章节产生 N 次 UPDATE 语句。",
"recommendation": "使用 CASE WHEN 批量更新:`UPDATE chapters SET order = CASE id WHEN ... THEN ... END, parentId = CASE id WHEN ... THEN ... END WHERE id IN (...)`,或用 sql`VALUES(...)` 构造批量更新。",
"effort": "M (≤2h)"
},
{
"id": "G1-006",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L237",
"ruleId": "F-02",
"severity": "P1",
"dimension": "performance",
"title": "LIKE '%query%' 全表扫描查询课案标题",
"description": "getLessonPlansRaw 在 L237 使用 `like(lessonPlans.title, \\`%${escapeLikePattern(params.query)}%\\`)`,前导通配符 % 导致无法使用索引,全表扫描。课案表数据量大时严重影响性能。",
"recommendation": "对 lessonPlans.title 建立全文索引MySQL FULLTEXT INDEX改用 `sql\\`MATCH(title) AGAINST(${query} IN BOOLEAN MODE)\\``;或至少对高频查询场景使用前缀匹配 `like(title, query + '%')`。",
"effort": "M (≤2h)"
},
{
"id": "G1-007",
"file": "src/modules/lesson-preparation/data-access-knowledge.ts",
"lines": "L99, L129",
"ruleId": "F-02",
"severity": "P1",
"dimension": "performance",
"title": "LIKE '%id%' 全表扫描 JSON content 字段",
"description": "getLessonPlansByKnowledgePointRawL99和 getLessonPlansByQuestionRawL129对 lessonPlans.contentJSON 列)使用 `like(content, \\`%${kpId}%\\`)` 做粗筛。JSON 列上的 LIKE 全表扫描代价极高,且无法走索引。",
"recommendation": "建立关联表 lesson_plan_knowledge_point_refs(plan_id, knowledge_point_id) 和 lesson_plan_question_refs(plan_id, question_id) 存储提取后的关联关系,改用 inArray 等值查询。短期可加 LIMIT 并在 actions 层缓存结果。",
"effort": "L (≤1d)"
},
{
"id": "G1-008",
"file": "src/modules/questions/data-access.ts",
"lines": "L60-L65",
"ruleId": "F-02",
"severity": "P1",
"dimension": "performance",
"title": "LOWER(CAST(content AS CHAR)) LIKE '%q%' 全表扫描",
"description": "getQuestionsRaw 在 L61-64 使用 `sql\\`LOWER(CAST(${questions.content} AS CHAR)) LIKE ${needle}\\`` 对 JSON content 列做 LIKE 模糊搜索,包含 LOWER + CAST + 前导 % 三重性能杀手,无法走索引。",
"recommendation": "对 questions 表增加 searchable_text 列(存储从 content 提取的纯文本),建立 FULLTEXT 索引;或引入 Meilisearch/TypeSense 等外部搜索引擎处理题目全文检索。",
"effort": "L (≤1d)"
},
{
"id": "G1-009",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L48-L54, L545-L551",
"ruleId": "F-02",
"severity": "P1",
"dimension": "performance",
"title": "LIKE '%q%' 全表扫描 4 个字段",
"description": "getTextbooksRawL48-54和 getTextbooksWithScopeRawL545-551对 title/subject/grade/publisher 四个字段做 `like(field, \\`%${q}%\\`)` OR 查询4 个前导通配符 LIKE 全表扫描。",
"recommendation": "对 title 建立全文索引;或将 subject/grade/publisher 改为等值过滤(下拉选择),仅 title 做前缀匹配。",
"effort": "M (≤2h)"
},
{
"id": "G1-010",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L247-L277",
"ruleId": "F-04",
"severity": "P1",
"dimension": "performance",
"title": "getLessonPlansRaw 5 表 LEFT JOIN",
"description": "getLessonPlansRaw 在 L270-275 对 lessonPlans LEFT JOIN textbooks/chapters/subjects/grades/users 共 5 个表。JOIN 表数量 > 3查询计划复杂度高且无 LIMIT。",
"recommendation": "拆分为两步1) 先查 lessonPlans 主表(带 scope + 过滤条件 + LIMIT + ORDER BY2) 用 collect 的 textbookId/chapterId/subjectId/gradeId/creatorId 批量查 textbooks/chapters/subjects/grades/users 名称,在内存中 Map 关联。",
"effort": "L (≤1d)"
},
{
"id": "G1-011",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L247-L277",
"ruleId": "F-05",
"severity": "P1",
"dimension": "performance",
"title": "getLessonPlansRaw 列表查询无 LIMIT",
"description": "getLessonPlansRaw 查询课案列表时无 LIMIT当课案数量增长时会一次性拉取全表数据到内存做分组聚合L283-316可能导致 OOM。",
"recommendation": "添加默认分页 `.limit(pageSize).offset(offset)`,或至少 `.limit(500)` 保护;版本聚合逻辑应改为分页后处理。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-012",
"file": "src/modules/lesson-preparation/data-access-review.ts",
"lines": "L168-L218, L255-L294",
"ruleId": "F-05",
"severity": "P1",
"dimension": "performance",
"title": "getPendingReviewPlansRaw / getPlansByStatusesRaw 无 LIMIT",
"description": "getPendingReviewPlansRawL184-196和 getPlansByStatusesRawL275-285均无 LIMIT且后者还在内存中做 filterL199-208而非 SQL 过滤。待审核/按状态查询的课案可能很多。",
"recommendation": "添加分页参数 page/pageSizeSQL 层用 inArray 过滤 gradeId/subjectId 而非内存 filter加 `.limit(pageSize).offset(offset)`。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-013",
"file": "src/modules/lesson-preparation/data-access-calendar.ts",
"lines": "L43-L60, L102-L120, L136-L153",
"ruleId": "F-05",
"severity": "P1",
"dimension": "performance",
"title": "getCalendarEventsRaw 三段查询均无 LIMIT",
"description": "getCalendarEventsRaw 对 lessonPlansL43、lessonPlanVersionsL102、lessonPlanReviewRecordsL136三段查询均无 LIMIT。日历范围跨度大时可能拉取大量记录。",
"recommendation": "每段查询添加 `.limit(500)` 上限保护,或在 actions 层强制限制日期范围跨度(如最多 90 天)。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-014",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L212-L239",
"ruleId": "F-10",
"severity": "P1",
"dimension": "performance",
"title": "getFormativeItemStatsRaw 全表拉取后内存聚合统计",
"description": "getFormativeItemStatsRaw 在 L215-218 SELECT 所有作答记录(无 LIMIT然后在 L220-232 内存循环统计 total/correct/incorrect/avgDuration。一个互动组件可能有上千条作答。",
"recommendation": "改用 SQL 聚合:`SELECT COUNT(*) as total, SUM(isCorrect=1) as correct, SUM(isCorrect=0) as incorrect, AVG(durationSec) as avgDuration FROM ... WHERE itemId=?`,单次查询完成。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-015",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L145-L157",
"ruleId": "F-10",
"severity": "P1",
"dimension": "performance",
"title": "countUnresolvedCommentsRaw SELECT 全部 ID 后取 length 计数",
"description": "countUnresolvedCommentsRaw 在 L146-155 SELECT 所有匹配的 id 字段,然后 L156 `return rows.length` 计数。应直接用 SQL COUNT 聚合,避免拉取全部行数据。",
"recommendation": "改为 `.select({ count: count() }).from(...).where(...)`,返回 `Number(rows[0]?.count ?? 0)`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-016",
"file": "src/modules/lesson-preparation/data-access-analytics.ts",
"lines": "L168-L184",
"ruleId": "F-06",
"severity": "P1",
"dimension": "performance",
"title": "getGlobalLessonPlanStatsRaw 5 次串行查询同表",
"description": "getGlobalLessonPlanStatsRaw 对 lessonPlans/lessonPlanStandards 表执行 5 次 SELECT COUNT 查询L168-184且是串行 await。仪表盘每次加载产生 5 次 DB 往返。",
"recommendation": "合并为单次 GROUP BY 查询:`SELECT status, COUNT(*) FROM lessonPlans GROUP BY status`,或用 Promise.all 并行执行lessonPlanStandards 计数可合并到同一查询。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-017",
"file": "src/modules/lesson-preparation/data-access-schedules.ts",
"lines": "L77-L123",
"ruleId": "F-01",
"severity": "P1",
"dimension": "performance",
"title": "getSchedulesByDateRangeRaw 拉全表后内存 filter",
"description": "getSchedulesByDateRangeRaw 仅按日期范围查询L99-104然后用 `rows.filter((r) => teacherPlanIds.includes(r.planId))`L108-109在内存过滤教师课案。注释 L101 自述'简化:仅按日期范围过滤'。当全校课案绑定量大时拉取大量无关数据。",
"recommendation": "将 planId 过滤下推到 SQL`inArray(lessonPlanSchedules.planId, teacherPlanIds)`,配合日期范围条件,避免拉取无关行。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-018",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L182-L205",
"ruleId": "F-01",
"severity": "P1",
"dimension": "performance",
"title": "getResponsesByStudentIdRaw 拉全量作答后内存 filter",
"description": "getResponsesByStudentIdRaw 当传入 planId 时L187-198先查该 plan 的 formative items IDL188-191再 SELECT 该学生的全部 responsesL194-197 无 itemId 过滤),最后内存 filter `itemIds.includes(r.itemId)`L198。应直接用 inArray 在 SQL 过滤。",
"recommendation": "在 L196 的 WHERE 中增加 `inArray(lessonPlanFormativeResponses.itemId, itemIds)` 条件,移除内存 filter。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-019",
"file": "src/modules/textbooks/actions.ts",
"lines": "L396-L398",
"ruleId": "F-08",
"severity": "P1",
"dimension": "performance",
"title": "getKnowledgeGraphDataAction 循环调用 getGradeNameByIdN+1",
"description": "getKnowledgeGraphDataAction 在 L396-398 用 `Promise.all(allowedGradeIds.map((gid) => getGradeNameById(gid)))` 逐个查询年级名称。虽然 Promise.all 并行了请求,但仍是 N 次 DB 查询。",
"recommendation": "school 模块应提供批量接口 `getGradeNamesByIds(gradeIds): Promise<Map<string,string>>`,单次 inArray 查询返回映射。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-020",
"file": "src/modules/questions/data-access.ts",
"lines": "L39-L49",
"ruleId": "P-04",
"severity": "P1",
"dimension": "pattern",
"title": "getQuestionsRaw 缺少显式返回类型标注",
"description": "getQuestionsRawL39使用 `=> {` 箭头函数,未显式标注返回类型 `Promise<T>`,依赖 TypeScript 推断。违反 P-04 规则'函数返回值必须显式标注,特别是 Promise<T>'。",
"recommendation": "定义返回类型并显式标注:`export const getQuestionsRaw = async (params: GetQuestionsParams = {}): Promise<QuestionsListResult> => { ... }`,将返回结构提取为命名类型。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-021",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L574-L590, L416-L426, L429-L444, L593-L617",
"ruleId": "P-03",
"severity": "P2",
"dimension": "pattern",
"title": "4 个读函数未走 cacheFn 包装",
"description": "getLessonPlanStatsL574、getTextbooksForPickerL416、getChaptersForPickerL429、getTemplateByIdL593均为纯读函数但未用 cacheFn 包装。其中 getTemplateById 在 createLessonPlan 热路径中被调用L366缺少缓存影响创建性能。",
"recommendation": "为每个读函数添加 Raw + cacheFn 配对:`export const getTemplateById = cacheFn(getTemplateByIdRaw, { tags: [...], ttl: 300, keyParts: [...] })`。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-022",
"file": "src/modules/lesson-preparation/data-access-substitutes.ts",
"lines": "L125-L141",
"ruleId": "P-03",
"severity": "P2",
"dimension": "pattern",
"title": "canTeacherAccessPlan 读函数未走 cacheFn",
"description": "canTeacherAccessPlanL125是读函数查询 plan + 查询 substitutes但未用 cacheFn 包装。该函数可能在权限校验热路径被频繁调用。",
"recommendation": "拆为 canTeacherAccessPlanRaw + cacheFn 包装,注意 TTL 应较短60s因权限相关。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-023",
"file": "src/modules/questions/data-access.ts",
"lines": "L380-L384, L391-L399, L406-L426, L433-L436, L577-L622",
"ruleId": "P-03",
"severity": "P2",
"dimension": "pattern",
"title": "5 个读函数未走 cacheFn 包装",
"description": "getKnowledgePointOptionsL380、getTextbookOptionsL391、getChapterOptionsL406、getKnowledgePointOptionsByChapterL433、exportQuestionsL577均为读函数但未用 cacheFn。前四个是级联筛选下拉数据频繁调用。",
"recommendation": "为 getKnowledgePointOptions/getTextbookOptions/getChapterOptions/getKnowledgePointOptionsByChapter 添加 cacheFnttl 可较长 600s。exportQuestions 因可能导出大结果集,可不缓存或短 TTL。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-024",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L492-L504, L511-L524, L689-L703",
"ruleId": "P-03",
"severity": "P2",
"dimension": "pattern",
"title": "3 个读函数未走 cacheFn 包装",
"description": "verifyChapterBelongsToTextbookL492、verifyKnowledgePointBelongsToTextbookL511、getPrerequisiteEdgesForTextbookL689均为读函数但未用 cacheFn。verify* 函数在 actions 层归属校验热路径中被频繁调用actions.ts 中多处调用)。",
"recommendation": "添加 cacheFn 包装TTL 较短60-120s。getPrerequisiteEdgesForTextbook 用于循环检测,可缓存 300s。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-025",
"file": "src/modules/lesson-preparation/data-access-schedules.ts",
"lines": "L29-L34",
"ruleId": "P-07",
"severity": "P2",
"dimension": "pattern",
"title": "toDateStr 本地实现日期序列化,未用 shared helper",
"description": "toDateStrL29-34手动拼接 YYYY-MM-DD 字符串,未使用项目统一的 serializeDate/toISODateString helper。其他模块如 data-access.ts 的 mapRowToLessonPlan使用 `.toISOString()` 序列化。",
"recommendation": "统一使用 shared/lib 中的日期序列化 helper或将 toDateStr 提取到 shared/lib/date-utils.ts 供所有模块复用。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-026",
"file": "src/modules/lesson-preparation/data-access-knowledge.ts",
"lines": "L16-L18",
"ruleId": "S-03",
"severity": "P2",
"dimension": "structure",
"title": "isStringArray 与 lib/type-guards 重复实现",
"description": "data-access-knowledge.ts 在 L16-18 本地定义 isStringArray而 data-access-ai-evaluation.ts L14 已从 './lib/type-guards' 导入同名函数。同一模块内重复实现 helper。",
"recommendation": "删除 data-access-knowledge.ts L16-18 的本地实现,改为 `import { isStringArray } from './lib/type-guards'`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-027",
"file": "src/modules/lesson-preparation/data-access-review.ts",
"lines": "L16-L23",
"ruleId": "S-03",
"severity": "P2",
"dimension": "structure",
"title": "toLessonPlanStatus/toReviewDecision 在多个文件重复定义",
"description": "data-access-review.tsL16-18和 data-access-calendar.tsL16-18各自定义了 toLessonPlanStatus 函数逻辑完全相同isLessonPlanStatus 守卫失败回退 'draft'。toReviewDecisionL21-23也仅在本文件定义但可共享。",
"recommendation": "将 toLessonPlanStatus 提取到 lib/type-guards.ts 或 lib/serialize.ts两个 data-access 文件统一导入。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-028",
"file": "src/modules/lesson-preparation/data-access-ai-evaluation.ts",
"lines": "L141-L192",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "evaluateDocument 业务逻辑(评分算法)放在 data-access 层",
"description": "evaluateDocumentL141-192是纯业务逻辑函数基于规则计算 5 维度评分 + 生成建议),不涉及任何 DB 操作,却导出在 data-access 文件中。违反 A-02'data-access 不含业务逻辑'规则。",
"recommendation": "将 evaluateDocument 移至 lib/ai-evaluation.ts纯函数模块data-access-ai-evaluation.ts 仅保留 DB CRUD。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-029",
"file": "src/modules/lesson-preparation/data-access-review.ts",
"lines": "L40-L45, L50-L76, L82-L137, L225-L250",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "状态机逻辑isValidTransition + 状态迁移)放在 data-access 层",
"description": "isValidTransitionL40-45是状态机校验纯函数submitForReviewL50-76、reviewPlanL82-137、withdrawSubmissionL225-250内部包含状态迁移判断逻辑L66-68、L100-107、L240-242属于业务编排而非纯数据访问。",
"recommendation": "将 isValidTransition 和状态迁移判断逻辑移至 actions-review.ts 或 lib/status-machine.tsdata-access 仅暴露 updateStatus(planId, newStatus) 和 insertReviewRecord() 等纯数据操作。",
"effort": "M (≤2h)"
},
{
"id": "G1-030",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L426-L458",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "reorderChapters 重排序业务逻辑放在 data-access 层",
"description": "reorderChaptersL426-458包含排序算法splice 插入 L442、parentId 变更判断L447等业务逻辑且在事务内循环更新。这些编排逻辑应属于 actions 层。",
"recommendation": "将排序算法和变更判断移至 actions.tsdata-access 仅暴露 updateChapterOrder(tx, id, order, parentId) 单条更新接口,由 actions 在事务内调用。",
"effort": "M (≤2h)"
},
{
"id": "G1-031",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L10-L15",
"ruleId": "A-06",
"severity": "P2",
"dimension": "architecture",
"title": "导入 textbooks/chapters 表(属 textbooks 模块)用于 JOIN",
"description": "data-access.ts L10-15 从 @/shared/db/schema 导入 textbooks、chapters 表(属 textbooks 模块)用于 L271-272 的 LEFT JOIN。虽然 L27 也通过 textbooks data-access 导入查询函数,但 JOIN 仍直接引用对方表。",
"recommendation": "短期:保留 JOIN 引用但添加注释说明;长期:重构为两步查询(先查 lessonPlans再用 ID 批量查 textbooks/chapters 名称),彻底消除跨模块 schema 引用。",
"effort": "L (≤1d)"
},
{
"id": "G1-032",
"file": "src/modules/lesson-preparation/data-access-schedules.ts",
"lines": "L9",
"ruleId": "A-06",
"severity": "P2",
"dimension": "architecture",
"title": "导入 classes 表(属 classes 模块)用于 JOIN",
"description": "data-access-schedules.ts L9 从 @/shared/db/schema 导入 classes 表(属 classes 模块),在 L55、L98、L164 的 LEFT JOIN 中获取 className。应通过 classes 模块 data-access 获取。",
"recommendation": "改为两步1) 查 lessonPlanSchedules不含 JOIN2) 收集 classId 后调用 classes 模块的 getClassNamesByIds(classIds) 批量获取名称,内存 Map 关联。",
"effort": "M (≤2h)"
},
{
"id": "G1-033",
"file": "src/modules/questions/data-access.ts",
"lines": "L4",
"ruleId": "A-06",
"severity": "P2",
"dimension": "architecture",
"title": "导入 knowledgePoints 表(属 textbooks 模块)用于 JOIN",
"description": "data-access.ts L4 从 @/shared/db/schema 导入 knowledgePoints 表(属 textbooks 模块),在 L463 的 INNER JOIN 中获取知识点名称。虽然 L8-14 已通过 textbooks data-access 导入查询函数,此处 JOIN 仍直接引用对方表。",
"recommendation": "getKnowledgePointsForQueries 改为两步1) 查 questionsToKnowledgePoints本模块表获取 questionId→knowledgePointId 映射2) 调用 textbooks data-access 批量获取知识点名称,内存关联。",
"effort": "M (≤2h)"
},
{
"id": "G1-034",
"file": "src/modules/lesson-preparation/data-access-versions.ts",
"lines": "L35-L55, L59-L95, L97-L126, L128-L165, L167-L205",
"ruleId": "S-06",
"severity": "P2",
"dimension": "structure",
"title": "5 个公共导出函数缺少 JSDoc 注释",
"description": "getLessonPlansRawL35、createLessonPlanVersionL59、getVersionContentRawL97、revertToVersionL128、pruneAutoVersionsL167均无 JSDoc。仅 L132/L140 有内联注释。公共导出函数应补齐 JSDoc 说明用途、参数、返回值。",
"recommendation": "为每个导出函数添加 JSDoc如 `/** 创建课案版本,在事务内 max(versionNo)+1 防止并发重复 */`。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-035",
"file": "src/modules/lesson-preparation/data-access-templates.ts",
"lines": "L45-L72, L76-L112, L114-L125",
"ruleId": "S-06",
"severity": "P2",
"dimension": "structure",
"title": "3 个公共导出函数缺少 JSDoc 注释",
"description": "getLessonPlansRawL45、saveAsTemplateL76、deletePersonalTemplateL114均无 JSDoc。saveAsTemplate 的 sourcePlanId→skeleton 提取逻辑L94-100需要文档说明。",
"recommendation": "添加 JSDoc 说明函数用途、参数含义、返回值。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-036",
"file": "src/modules/questions/data-access.ts",
"lines": "L39-L193, L214-L247, L258-L292",
"ruleId": "S-06",
"severity": "P2",
"dimension": "structure",
"title": "核心函数 getQuestionsRaw/insertQuestionWithRelations/updateQuestionById 缺少 JSDoc",
"description": "getQuestionsRawL39、insertQuestionWithRelationsL214、updateQuestionByIdL258等核心函数无 JSDoc。getQuestionsRaw 的级联筛选逻辑L75-122较复杂需要文档说明筛选优先级。",
"recommendation": "为这些函数添加 JSDoc特别是 getQuestionsRaw 的 knowledgePointId > chapterId > textbookId 级联筛选优先级。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-037",
"file": "src/modules/questions/data-access.ts",
"lines": "L1-L662",
"ruleId": "S-02",
"severity": "P2",
"dimension": "structure",
"title": "单文件导出函数数约 28 个,超过 20 警告阈值",
"description": "data-access.ts 导出约 28 个符号(含类型、函数、接口),包括 getQuestions/getQuestionsDashboardStats/createQuestionWithRelations/updateQuestionById/deleteQuestionByIdRecursive/deleteQuestionsBatch/getKnowledgePointOptions/getTextbookOptions/getChapterOptions/getKnowledgePointOptionsByChapter/getKnowledgePointsForQuestions/getQuestionsContentForErrorCollection/getQuestionTypeMapByIds/exportQuestions/importQuestions 等。职责混合了 CRUD + 跨模块接口 + 导入导出。",
"recommendation": "按职责拆分为 data-access.ts核心 CRUD、data-access-cross-module.ts跨模块只读接口、data-access-import-export.ts导入导出。",
"effort": "L (≤1d)"
},
{
"id": "G1-038",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L1-L703",
"ruleId": "S-02",
"severity": "P2",
"dimension": "structure",
"title": "单文件导出函数数约 35 个,超过 20 警告阈值",
"description": "data-access.ts 导出约 35 个符号,涵盖教材 CRUD、章节 CRUD、知识点 CRUD、排序、统计、归属校验、scope 查询、跨模块接口、前置依赖 CRUD。职责过重。",
"recommendation": "拆分为 data-access.ts教材+章节、data-access-knowledge-points.ts知识点+前置依赖、data-access-cross-module.ts跨模块只读接口。",
"effort": "L (≤1d)"
},
{
"id": "G1-039",
"file": "src/modules/lesson-preparation/data-access-ai-evaluation.ts",
"lines": "L58, L77",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getEvaluationsByPlanIdRaw / getLatestEvaluationRaw",
"description": "getEvaluationsByPlanIdRawL58和 getLatestEvaluationRawL77使用 `.select()` 无参数SELECT 所有列。表字段可能后续增加,且传输不需要的列浪费带宽。",
"recommendation": "改为显式列枚举 `.select({ id: ..., planId: ..., ... })`,仅查询 mapRowToEvaluation 实际使用的字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-040",
"file": "src/modules/lesson-preparation/data-access-analytics.ts",
"lines": "L63, L210",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getTeacherInvestmentRaw / upsertDailyAnalytics",
"description": "getTeacherInvestmentRawL63和 upsertDailyAnalytics 内的查询L210使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-041",
"file": "src/modules/lesson-preparation/data-access-review.ts",
"lines": "L146",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getReviewRecordsByPlanIdRaw",
"description": "getReviewRecordsByPlanIdRawL146使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-042",
"file": "src/modules/lesson-preparation/data-access-substitutes.ts",
"lines": "L34, L52",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getSubstitutesByPlanIdRaw / getActiveSubstitutesByTeacherIdRaw",
"description": "两个读函数 L34、L52 均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-043",
"file": "src/modules/lesson-preparation/data-access-versions.ts",
"lines": "L50",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getLessonPlanVersionsRaw",
"description": "getLessonPlanVersionsRawL50使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-044",
"file": "src/modules/lesson-preparation/data-access-templates.ts",
"lines": "L61",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getLessonPlansRaw",
"description": "getLessonPlansRawL61使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-045",
"file": "src/modules/lesson-preparation/data-access-knowledge.ts",
"lines": "L93, L123",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getLessonPlansByKnowledgePointRaw / getLessonPlansByQuestionRaw",
"description": "两个函数 L93、L123 均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段,仅查询 mapRowToListItemWithoutJoin 实际使用的列。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-046",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L51, L68, L169, L195, L201, L216",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "6 处 SELECT * 未指定列",
"description": "getFormativeItemsByPlanIdRawL51、getFormativeItemByIdRawL68、getResponsesByItemIdRawL169、getResponsesByStudentIdRawL195、L201、getFormativeItemStatsRawL216均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。getFormativeItemStatsRaw 尤其应仅查聚合字段。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-047",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L33, L51",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getCommentsByPlanIdRaw / getCommentsByBlockIdRaw",
"description": "两个读函数 L33、L51 均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-048",
"file": "src/modules/lesson-preparation/data-access-attachments.ts",
"lines": "L31, L49, L123",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "3 处 SELECT * 未指定列",
"description": "getAttachmentsByPlanIdRawL31、getAttachmentsByBlockIdRawL49、getAttachmentByIdRawL123均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-049",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L427, L431",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列reorderChapters 内查询)",
"description": "reorderChapters 中 L427 `db.select().from(chapters)` 和 L431 `db.select().from(chapters)` 使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段id, textbookId, parentId, order, title。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-050",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L43-L91, L627-L660",
"ruleId": "F-05",
"severity": "P2",
"dimension": "performance",
"title": "getTextbooksRaw / getKnowledgePointOptionsRaw 无 LIMIT",
"description": "getTextbooksRawL64-79和 getKnowledgePointOptionsRawL628-648无 LIMIT。getKnowledgePointOptionsRaw 拉取全量知识点+章节+教材 JOIN数据量大时风险高。",
"recommendation": "getTextbooksRaw 添加分页或 `.limit(200)`getKnowledgePointOptionsRaw 应改为按 textbookId/subject 参数过滤,或前端懒加载。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-051",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L165-L175, L182-L205",
"ruleId": "F-05",
"severity": "P2",
"dimension": "performance",
"title": "getResponsesByItemIdRaw / getResponsesByStudentIdRaw 无 LIMIT",
"description": "两个函数查询学生作答记录均无 LIMIT。一个互动组件可能有上千条作答一个学生可能有大量作答历史。",
"recommendation": "添加分页参数或 `.limit(500)` 上限保护。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-052",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L29-L39, L46-L62",
"ruleId": "F-05",
"severity": "P2",
"dimension": "performance",
"title": "getCommentsByPlanIdRaw / getCommentsByBlockIdRaw 无 LIMIT",
"description": "两个函数查询评论均无 LIMIT。热门课案评论数可能很多。",
"recommendation": "添加分页参数或 `.limit(200)` 上限保护。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-053",
"file": "src/modules/lesson-preparation/data-access-knowledge.ts",
"lines": "L92-L101, L122-L131",
"ruleId": "F-05",
"severity": "P2",
"dimension": "performance",
"title": "getLessonPlansByKnowledgePointRaw / getLessonPlansByQuestionRaw 无 LIMIT",
"description": "两个函数对 lessonPlans 全表 LIKE 扫描后无 LIMIT且无分页。匹配数量不可控。",
"recommendation": "添加 `.limit(100)` 上限保护,或改为分页查询。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-054",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L465-L474",
"ruleId": "F-10",
"severity": "P2",
"dimension": "performance",
"title": "getTextbooksDashboardStatsRaw 全表 COUNT 无过滤",
"description": "getTextbooksDashboardStatsRawL465-474对 textbooks 和 chapters 表各执行 `count()` 无 WHERE 过滤,统计全量数据。仪表盘统计应至少按可见范围过滤。",
"recommendation": "如需按权限范围统计,传入 scope 参数添加 WHERE 条件;若确为管理员全局统计,可保留但加缓存(已有 cacheFn。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-055",
"file": "src/modules/questions/data-access.ts",
"lines": "L204-L207",
"ruleId": "F-10",
"severity": "P2",
"dimension": "performance",
"title": "getQuestionsDashboardStatsRaw 全表 COUNT 无过滤",
"description": "getQuestionsDashboardStatsRawL204-207对 questions 表执行 `count()` 无 WHERE 过滤。仪表盘应按用户可见范围统计。",
"recommendation": "传入 scope/authorId 参数添加 WHERE 条件,或确认是否为管理员全局统计。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-056",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L574-L590",
"ruleId": "F-10",
"severity": "P2",
"dimension": "performance",
"title": "getLessonPlanStats 全表 GROUP BY 无过滤",
"description": "getLessonPlanStatsL574-590对 lessonPlans 全表 GROUP BY status 统计,无 WHERE 过滤。管理员看板统计应限定范围(如本学期/本学年)。",
"recommendation": "添加时间范围 WHERE 条件(如 createdAt >= 学期开始日期),避免统计历史归档数据。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-057",
"file": "src/modules/lesson-preparation/data-access-substitutes.ts",
"lines": "L125-L141",
"ruleId": "A-02",
"severity": "P3",
"dimension": "architecture",
"title": "canTeacherAccessPlan 含权限判断业务逻辑",
"description": "canTeacherAccessPlanL125-141包含'原教师→true / 代课教师→true'的权限判断逻辑,属于业务编排。虽然查询了 DB但'是否可访问'的判断应属于 actions 或权限层。",
"recommendation": "将 canTeacherAccessPlan 的判断逻辑移至 actions 层data-access 仅暴露 getPlanCreatorId 和 getActiveSubstitutesByTeacherId 两个纯读接口。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-058",
"file": "src/modules/lesson-preparation/data-access-analytics.ts",
"lines": "L201-L249",
"ruleId": "A-02",
"severity": "P3",
"dimension": "architecture",
"title": "upsertDailyAnalytics 含 read-then-write 业务逻辑",
"description": "upsertDailyAnalyticsL201-249先 SELECT 判断是否存在L209-218存在则 UPDATE 累加L222-234不存在则 INSERTL236-247。该 upsert 编排逻辑可下放到 actions 或用 SQL `INSERT ... ON DUPLICATE KEY UPDATE` 替代。",
"recommendation": "改用 MySQL `INSERT ... ON DUPLICATE KEY UPDATE` 单语句完成 upsert或在 actions 层编排 read-then-write。",
"effort": "M (≤2h)"
},
{
"id": "G1-059",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L336, L535, L612",
"ruleId": "F-03",
"severity": "P3",
"dimension": "performance",
"title": "3 处 SELECT * 未指定列",
"description": "getLessonPlanByIdRawL336、duplicateLessonPlanL535、getTemplateByIdL612使用 `.select()` 无参数。其中 getLessonPlanByIdRaw 查询后用 mapRowToLessonPlan 映射,所需字段已知。",
"recommendation": "显式枚举 mapRowToLessonPlan 所需的 14 个字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-060",
"file": "src/modules/lesson-preparation/data-access-substitutes.ts",
"lines": "L136",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "plan[0]!.creatorId 非空断言",
"description": "L136 `if (plan[0]!.creatorId === teacherId) return true;` 在已检查 `plan.length === 0`L135后使用 `!` 非空断言。虽逻辑正确,但可改为更安全的 `const row = plan[0]; if (row && row.creatorId === teacherId) ...`。",
"recommendation": "用 `const row = plan[0]; if (!row) return false; if (row.creatorId === teacherId) return true;` 替代非空断言。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-061",
"file": "src/modules/lesson-preparation/data-access-calendar.ts",
"lines": "L181",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "split('T')[0]! 非空断言",
"description": "L181 `e.occurredAt.toISOString().split('T')[0]!` 对数组取值使用 `!`。虽然 toISOString() 必定含 'T',但 `!` 属非空断言。",
"recommendation": "改为 `e.occurredAt.toISOString().split('T')[0] ?? ''` 或用专门的 toISODateString helper。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-062",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L72",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "rows[0]! 非空断言",
"description": "L72 `return rows.length === 0 ? null : mapRowToItem(rows[0]!);` 使用 `!`。虽逻辑正确,但可避免。",
"recommendation": "改为 `const row = rows[0]; return row ? mapRowToItem(row) : null;`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-063",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L118",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "rows[0]!.resolved 非空断言",
"description": "L118 `const newResolved = !rows[0]!.resolved;` 使用 `!`。已检查 `rows.length === 0`L117但风格上可改进。",
"recommendation": "改为 `const row = rows[0]; if (!row) return; const newResolved = !row.resolved;`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-064",
"file": "src/modules/lesson-preparation/data-access-schedules.ts",
"lines": "L167",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "created[0]! 非空断言",
"description": "L167 `const r = created[0]!;` 在 createSchedule 中查询刚插入的记录后使用 `!`。INSERT 后立即查询,理论上必定有值,但 `!` 不够安全。",
"recommendation": "改为 `const r = created[0]; if (!r) throw new Error('SCHEDULE_CREATE_FAILED');`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-065",
"file": "src/modules/lesson-preparation/data-access-analytics.ts",
"lines": "L98",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "r.templateId! 非空断言",
"description": "L98 `templateId: r.templateId!,` 在 WHERE 已过滤 `templateId IS NOT NULL`L93后使用 `!`。",
"recommendation": "改为 `templateId: r.templateId ?? ''`,或用类型守卫收窄。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-066",
"file": "src/modules/questions/data-access.ts",
"lines": "L1-L662",
"ruleId": "S-06",
"severity": "P3",
"dimension": "structure",
"title": "deleteQuestionRecursive/insertQuestionWithRelations 缺少 JSDoc",
"description": "deleteQuestionRecursiveL294、insertQuestionWithRelationsL214等内部函数无 JSDoc。环检测逻辑L299-303需要文档说明。",
"recommendation": "补充 JSDoc 说明环检测目的和 visited Set 的作用。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-067",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L170-L206, L208-L210, L212-L240, L242-L273, L275-L333",
"ruleId": "S-06",
"severity": "P3",
"dimension": "structure",
"title": "createTextbook/updateTextbook/deleteTextbook/createChapter 等多个函数缺少 JSDoc",
"description": "createTextbookL170、updateTextbookL192、deleteTextbookL208、createChapterL212、updateChapterContentL242、deleteChapterL275、createKnowledgePointL398、updateKnowledgePointL411、deleteKnowledgePointL422、reorderChaptersL426均无 JSDoc。deleteChapter 的级联删除逻辑L310-332较复杂需要文档。",
"recommendation": "为这些函数添加 JSDoc特别是 deleteChapter 需说明级联删除知识点+前置依赖的行为。",
"effort": "S (≤30 分钟)"
}
]

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