{ "_meta": { "project": "Next_Edu", "description": "K12 智慧教务管理系统", "generatedAt": "2026-06-17", "formatVersion": "1.1", "rule": "每次文件修改后须同步更新本文件", "lastUpdate": "Phase 4.6 grades 模块组件化重构闭环完成(2026-07-06):(1) 拆分 batch-grade-entry.tsx 巨型文件 — 将试卷/班级选择器(含未选态引导)拆至 batch-grade-entry-selectors.tsx、底部工具栏(清除/保存/粘贴提示)拆至 batch-grade-entry-toolbar.tsx,主组件从 457 行降至 374 行,容器仅负责状态编排与 Server Action 调用;(2) 拆分 grade-record-list.tsx 巨型文件 — 行渲染(桌面表格 + 移动端卡片堆叠)拆至 grade-record-row.tsx、编辑/单条删除/批量删除对话框集合拆至 grade-record-detail.tsx(导出 EditableFields 类型 + toEditableFields 工具函数),主组件从 523 行降至 231 行;(3) 迁移 4 个重复组件到共享底座并删除原文件 — grade-stats-card.tsx → StatsGrid + StatItem 组合(class-grade-report.tsx 内联 8 个统计项)、grade-filters.tsx → app 层 student-grade-filters.tsx(复用 FilterBar + FilterSearchInput + Select + nuqs useQueryState)、grade-query-filters.tsx → app 层 teacher-grade-query-filters.tsx(复用 FilterBar + Select + useRouter/useSearchParams)、analytics-filters.tsx → app 层 analytics-grade-filters.tsx(复用 FilterBar + ChipNav,保留原 buildHref URL 构造逻辑)。同步更新:StatItem/ChipNav/FilterBar/FilterSearchInput 的 usedBy 列表、GradeStats/isGradeType/isSemester 的 usedBy 列表、BatchGradeEntry 与 GradeRecordList 与 ClassGradeReport 的 purpose/props/deps、新增 4 个子组件条目(BatchGradeEntrySelectors/BatchGradeEntryToolbar/GradeRecordRow/GradeRecordDetail)、删除 4 个旧组件条目(GradeStatsCard/GradeQueryFilters/AnalyticsFilters 及 grades/components/grade-filters 隐式条目)。验证:npx tsc --noEmit 零错误;npm run lint 仅 3 个 pre-existing react-hooks/refs 错误(位于未修改的 use-exam-preview-tasks.ts);grep 确认旧组件名(GradeStatsCard/GradeFilters/GradeQueryFilters/AnalyticsFilters)无业务代码残留。前次更新:缓存策略落地专项重构完成(2026-07-05),覆盖全部 28 个模块,新增 shared/lib/cache/ 9 个文件 + INVALIDATION_MAP 203 个 actionId,data-access 83 文件 250+ 查询函数迁移至 cacheFn 双导出模式,actions 43 文件 247 处 revalidatePath 替换为 invalidateFor,详细见 004 第 1.1.6 节。", "lastUpdated": "2026-07-06" }, "architectureOverview": { "layers": [ { "name": "app", "description": "路由层,Next.js App Router,按角色分组(admin/teacher/student/parent/management)", "path": "src/app/" }, { "name": "modules", "description": "业务模块层,按业务领域划分(25 个模块)", "path": "src/modules/" }, { "name": "shared", "description": "基础设施层,提供共享能力(db/lib/hooks/components/types)", "path": "src/shared/" } ], "dependencyRule": "app → modules → shared (单向依赖,上层可依赖下层,下层不可依赖上层)", "violations": [ "✅ shared/lib ↔ auth 循环依赖已修复(audit-logger/change-logger/auth-guard 改为通过 shared/lib/session.ts 单一入口获取 session,session.ts 内部 dynamic import @/auth 打破静态循环)", "✅ dashboard 跨模块直查 11 张表已修复(改为并行调用各模块 dashboard stats 函数: getUsersDashboardStats/getClassesDashboardStats/getTextbooksDashboardStats/getQuestionsDashboardStats/getExamsDashboardStats/getHomeworkDashboardStats)", "✅ messaging 绕过 notifications dispatcher 已修复(通知 CRUD 和偏好迁移至 notifications 模块,messaging 通过 dispatcher 发送通知)", "✅ classes/data-access.ts 混入 homework/scheduling/grades 跨模块逻辑已修复(拆分为 5 个文件,classes 通过 homework/data-access-classes 获取作业数据)", "✅ classSchedule 表三处写入口已统一(写函数从 classes 迁移至 scheduling/data-access-class-schedule.ts,classes 仅保留读函数)", "✅ P1-1 跨模块直查已修复(homework/grades/parent/diagnostic/elective/proctoring/notifications/scheduling/classes 等模块的直查改为通过对方 data-access 接口)" ] }, "techStack": { "framework": "Next.js 16 (App Router)", "language": "TypeScript (strict)", "ui": "React 19 + Tailwind CSS v4 + shadcn/ui", "state": [ "Zustand", "nuqs", "React Hook Form" ], "database": "MySQL + Drizzle ORM 0.45", "auth": "NextAuth v5 (JWT strategy)", "validation": "Zod 4", "ai": "OpenAI SDK (multi-provider)", "testing": [ "Vitest", "Playwright" ] }, "roles": [ "admin", "teacher", "student", "parent", "grade_head", "teaching_head" ], "permissions": { "EXAM_CREATE": "exam:create", "EXAM_READ": "exam:read", "EXAM_UPDATE": "exam:update", "EXAM_DELETE": "exam:delete", "EXAM_DUPLICATE": "exam:duplicate", "EXAM_PUBLISH": "exam:publish", "EXAM_AI_GENERATE": "exam:ai_generate", "EXAM_SUBMIT": "exam:submit", "HOMEWORK_CREATE": "homework:create", "HOMEWORK_GRADE": "homework:grade", "HOMEWORK_SUBMIT": "homework:submit", "QUESTION_CREATE": "question:create", "QUESTION_READ": "question:read", "QUESTION_UPDATE": "question:update", "QUESTION_DELETE": "question:delete", "TEXTBOOK_CREATE": "textbook:create", "TEXTBOOK_READ": "textbook:read", "TEXTBOOK_UPDATE": "textbook:update", "TEXTBOOK_DELETE": "textbook:delete", "CLASS_CREATE": "class:create", "CLASS_READ": "class:read", "CLASS_UPDATE": "class:update", "CLASS_DELETE": "class:delete", "CLASS_ENROLL": "class:enroll", "CLASS_SCHEDULE": "class:schedule", "SCHOOL_MANAGE": "school:manage", "GRADE_MANAGE": "grade:manage", "USER_MANAGE": "user:manage", "AI_CHAT": "ai:chat", "AI_CONFIGURE": "ai:configure", "SETTINGS_ADMIN": "settings:admin", "AUDIT_LOG_READ": "audit_log:read", "ANNOUNCEMENT_MANAGE": "announcement:manage", "ANNOUNCEMENT_READ": "announcement:read", "GRADE_RECORD_MANAGE": "grade_record:manage", "GRADE_RECORD_READ": "grade_record:read", "FILE_UPLOAD": "file:upload", "FILE_READ": "file:read", "FILE_DELETE": "file:delete", "COURSE_PLAN_MANAGE": "course_plan:manage", "COURSE_PLAN_READ": "course_plan:read", "ATTENDANCE_MANAGE": "attendance:manage", "ATTENDANCE_READ": "attendance:read", "LEAVE_REQUEST_CREATE": "leave_request:create", "LEAVE_REQUEST_READ": "leave_request:read", "LEAVE_REQUEST_REVIEW": "leave_request:review", "MESSAGE_SEND": "message:send", "MESSAGE_READ": "message:read", "MESSAGE_DELETE": "message:delete", "SCHEDULE_AUTO": "schedule:auto", "SCHEDULE_ADJUST": "schedule:adjust", "DIAGNOSTIC_MANAGE": "diagnostic:manage", "DIAGNOSTIC_READ": "diagnostic:read", "ELECTIVE_MANAGE": "elective:manage", "ELECTIVE_READ": "elective:read", "ELECTIVE_SELECT": "elective:select", "EXAM_PROCTOR": "exam:proctor", "EXAM_PROCTOR_READ": "exam:proctor:read", "LESSON_PLAN_CREATE": "lesson_plan:create", "LESSON_PLAN_READ": "lesson_plan:read", "LESSON_PLAN_UPDATE": "lesson_plan:update", "LESSON_PLAN_DELETE": "lesson_plan:delete", "LESSON_PLAN_PUBLISH": "lesson_plan:publish", "DASHBOARD_ADMIN_READ": "dashboard:admin_read", "DASHBOARD_TEACHER_READ": "dashboard:teacher_read", "DASHBOARD_STUDENT_READ": "dashboard:student_read", "DASHBOARD_PARENT_READ": "dashboard:parent_read", "ERROR_BOOK_READ": "error_book:read", "ERROR_BOOK_MANAGE": "error_book:manage", "ERROR_BOOK_ANALYTICS_READ": "error_book:analytics_read", "ADAPTIVE_PRACTICE_READ": "adaptive_practice:read", "ADAPTIVE_PRACTICE_MANAGE": "adaptive_practice:manage", "LESSON_PLAN_SUBMIT": "lesson_plan:submit", "LESSON_PLAN_REVIEW": "lesson_plan:review", "LESSON_PLAN_ANALYTICS_READ": "lesson_plan:analytics_read", "LESSON_PLAN_SUBSTITUTE_MANAGE": "lesson_plan:substitute_manage", "STANDARD_READ": "standard:read", "STANDARD_MANAGE": "standard:manage", "STANDARD_LINK": "standard:link" }, "rolePermissions": { "admin": [ "EXAM_CREATE", "EXAM_READ", "EXAM_UPDATE", "EXAM_DELETE", "EXAM_DUPLICATE", "EXAM_PUBLISH", "EXAM_AI_GENERATE", "HOMEWORK_CREATE", "HOMEWORK_GRADE", "QUESTION_CREATE", "QUESTION_READ", "QUESTION_UPDATE", "QUESTION_DELETE", "TEXTBOOK_CREATE", "TEXTBOOK_READ", "TEXTBOOK_UPDATE", "TEXTBOOK_DELETE", "CLASS_CREATE", "CLASS_READ", "CLASS_UPDATE", "CLASS_DELETE", "CLASS_ENROLL", "CLASS_SCHEDULE", "SCHOOL_MANAGE", "GRADE_MANAGE", "USER_MANAGE", "AI_CHAT", "AI_CONFIGURE", "SETTINGS_ADMIN", "AUDIT_LOG_READ", "ANNOUNCEMENT_MANAGE", "ANNOUNCEMENT_READ", "GRADE_RECORD_MANAGE", "GRADE_RECORD_READ", "FILE_UPLOAD", "FILE_READ", "FILE_DELETE", "COURSE_PLAN_MANAGE", "COURSE_PLAN_READ", "ATTENDANCE_MANAGE", "ATTENDANCE_READ", "MESSAGE_SEND", "MESSAGE_READ", "MESSAGE_DELETE", "SCHEDULE_AUTO", "SCHEDULE_ADJUST", "DIAGNOSTIC_MANAGE", "DIAGNOSTIC_READ", "ELECTIVE_MANAGE", "ELECTIVE_READ", "EXAM_PROCTOR", "EXAM_PROCTOR_READ", "DASHBOARD_ADMIN_READ", "ERROR_BOOK_ANALYTICS_READ", "ADAPTIVE_PRACTICE_READ", "LEAVE_REQUEST_CREATE", "LEAVE_REQUEST_READ", "LEAVE_REQUEST_REVIEW" ], "teacher": [ "EXAM_CREATE", "EXAM_READ", "EXAM_UPDATE", "EXAM_DELETE", "EXAM_DUPLICATE", "EXAM_PUBLISH", "EXAM_AI_GENERATE", "HOMEWORK_CREATE", "HOMEWORK_GRADE", "QUESTION_CREATE", "QUESTION_READ", "QUESTION_UPDATE", "QUESTION_DELETE", "TEXTBOOK_CREATE", "TEXTBOOK_READ", "TEXTBOOK_UPDATE", "CLASS_READ", "CLASS_ENROLL", "CLASS_SCHEDULE", "AI_CHAT", "ANNOUNCEMENT_READ", "GRADE_RECORD_MANAGE", "GRADE_RECORD_READ", "FILE_UPLOAD", "FILE_READ", "FILE_DELETE", "COURSE_PLAN_READ", "ATTENDANCE_MANAGE", "ATTENDANCE_READ", "MESSAGE_SEND", "MESSAGE_READ", "MESSAGE_DELETE", "DIAGNOSTIC_MANAGE", "DIAGNOSTIC_READ", "ELECTIVE_MANAGE", "ELECTIVE_READ", "EXAM_PROCTOR", "EXAM_PROCTOR_READ", "DASHBOARD_TEACHER_READ", "ERROR_BOOK_ANALYTICS_READ", "ADAPTIVE_PRACTICE_READ", "LEAVE_REQUEST_READ", "LEAVE_REQUEST_REVIEW" ], "student": [ "EXAM_READ", "EXAM_SUBMIT", "HOMEWORK_SUBMIT", "QUESTION_READ", "TEXTBOOK_READ", "CLASS_READ", "AI_CHAT", "ANNOUNCEMENT_READ", "GRADE_RECORD_READ", "FILE_READ", "COURSE_PLAN_READ", "ATTENDANCE_READ", "MESSAGE_SEND", "MESSAGE_READ", "MESSAGE_DELETE", "DIAGNOSTIC_READ", "ELECTIVE_SELECT", "ELECTIVE_READ", "DASHBOARD_STUDENT_READ", "ERROR_BOOK_READ", "ERROR_BOOK_MANAGE", "LESSON_PLAN_READ", "ADAPTIVE_PRACTICE_READ", "ADAPTIVE_PRACTICE_MANAGE", "LEAVE_REQUEST_CREATE", "LEAVE_REQUEST_READ" ], "parent": [ "EXAM_READ", "TEXTBOOK_READ", "CLASS_READ", "ANNOUNCEMENT_READ", "GRADE_RECORD_READ", "FILE_READ", "ATTENDANCE_READ", "MESSAGE_SEND", "MESSAGE_READ", "MESSAGE_DELETE", "DASHBOARD_PARENT_READ", "ERROR_BOOK_READ", "LESSON_PLAN_READ", "ADAPTIVE_PRACTICE_READ", "AI_CHAT", "LEAVE_REQUEST_CREATE", "LEAVE_REQUEST_READ" ], "grade_head": [ "EXAM_CREATE", "EXAM_READ", "EXAM_UPDATE", "EXAM_DELETE", "EXAM_DUPLICATE", "EXAM_PUBLISH", "EXAM_AI_GENERATE", "HOMEWORK_CREATE", "HOMEWORK_GRADE", "QUESTION_CREATE", "QUESTION_READ", "QUESTION_UPDATE", "QUESTION_DELETE", "TEXTBOOK_CREATE", "TEXTBOOK_READ", "TEXTBOOK_UPDATE", "CLASS_CREATE", "CLASS_READ", "CLASS_UPDATE", "CLASS_ENROLL", "CLASS_SCHEDULE", "GRADE_MANAGE", "AI_CHAT", "ANNOUNCEMENT_READ", "GRADE_RECORD_READ", "COURSE_PLAN_READ", "ATTENDANCE_READ", "MESSAGE_SEND", "MESSAGE_READ", "MESSAGE_DELETE", "DIAGNOSTIC_MANAGE", "DIAGNOSTIC_READ", "ELECTIVE_READ", "EXAM_PROCTOR_READ", "ERROR_BOOK_ANALYTICS_READ", "LESSON_PLAN_READ", "ADAPTIVE_PRACTICE_READ", "LEAVE_REQUEST_READ", "LEAVE_REQUEST_REVIEW" ], "teaching_head": [ "EXAM_CREATE", "EXAM_READ", "EXAM_UPDATE", "EXAM_DELETE", "EXAM_DUPLICATE", "EXAM_PUBLISH", "EXAM_AI_GENERATE", "HOMEWORK_CREATE", "HOMEWORK_GRADE", "QUESTION_CREATE", "QUESTION_READ", "QUESTION_UPDATE", "QUESTION_DELETE", "TEXTBOOK_CREATE", "TEXTBOOK_READ", "TEXTBOOK_UPDATE", "CLASS_READ", "GRADE_MANAGE", "AI_CHAT", "ANNOUNCEMENT_READ", "GRADE_RECORD_READ", "COURSE_PLAN_READ", "ATTENDANCE_READ", "MESSAGE_SEND", "MESSAGE_READ", "MESSAGE_DELETE", "DIAGNOSTIC_READ", "ELECTIVE_READ", "ERROR_BOOK_ANALYTICS_READ", "LESSON_PLAN_READ", "ADAPTIVE_PRACTICE_READ", "LEAVE_REQUEST_READ", "LEAVE_REQUEST_REVIEW" ] }, "dataScopeTypes": { "all": "管理员:无过滤", "owned": "仅自己创建的资源,含 userId 字段", "class_taught": "教师:所教班级,含 classIds[] 和可选 subjectIds[]", "grade_managed": "年级主任:所管年级,含 gradeIds[]", "class_members": "学生:所在班级的成员数据,含 classIds[] 和可选 gradeIds[](auth-guard 预解析)", "children": "家长:子女数据,含 childrenIds[] 和可选 gradeIds[](auth-guard 预解析)" }, "modules": { "shared": { "path": "src/shared", "description": "全项目共享基础设施:数据库、工具函数、权限系统、UI组件、Hooks", "exports": { "functions": [ { "name": "cn", "file": "lib/utils.ts", "signature": "cn(...inputs: ClassValue[]): string", "purpose": "合并CSS类名并解决Tailwind冲突", "deps": [ "clsx", "tailwind-merge" ], "usedBy": [ "*所有模块组件" ] }, { "name": "formatDate", "file": "lib/utils.ts", "signature": "formatDate(date: string | Date, locale?: string): string", "params": { "date": "日期值", "locale": "Intl locale,默认zh-CN" }, "purpose": "国际化日期格式化", "deps": [], "usedBy": [ "exams", "homework", "dashboard", "textbooks" ] }, { "name": "formatDateTime", "file": "lib/utils.ts", "signature": "formatDateTime(date: string | Date, locale?: string): string", "params": { "date": "日期值", "locale": "Intl locale,默认zh-CN" }, "purpose": "国际化日期+时间格式化(含小时、分钟),P1-3 重构从 lesson-plan-card、version-history-drawer、proctoring-dashboard、exam-ai-generator 等处重复的 new Date(x).toLocaleString(...) 抽取", "deps": [], "usedBy": [ "lesson-preparation/components/lesson-plan-card.tsx", "lesson-preparation/components/version-history-drawer.tsx", "proctoring/components/proctoring-dashboard.tsx", "exams/components/exam-ai-generator.tsx" ] }, { "name": "formatLongDate", "file": "lib/utils.ts", "signature": "formatLongDate(date: string | Date, locale?: string): string", "params": { "date": "日期值", "locale": "Intl locale,默认zh-CN" }, "purpose": "国际化长日期格式化(含星期、完整月份名),P1-3 重构从 teacher-dashboard-header 中重复的 new Date().toLocaleDateString('en-US', { weekday, year, month, day }) 抽取;teacher_bug_v4 P2-1 默认 locale 改为 zh-CN,weekday 改为 short,输出形如「2026年6月20日周一」", "deps": [], "usedBy": [ "dashboard/components/teacher-dashboard/teacher-dashboard-header.tsx" ] }, { "name": "getParam", "file": "lib/search-params.ts", "signature": "getParam(params: SearchParams, key: string): string | undefined", "params": { "params": "Next.js searchParams 对象", "key": "参数键名" }, "purpose": "规范化 Next.js 15+ searchParams 访问(string | string[] | undefined → string | undefined),re-export 自 utils.ts 的 getSearchParam", "deps": [ "shared/lib/utils.getSearchParam" ], "usedBy": [ "teacher/attendance/page.tsx", "teacher/attendance/sheet/page.tsx", "teacher/attendance/stats/page.tsx", "teacher/classes/schedule/page.tsx", "teacher/classes/students/page.tsx", "teacher/course-plans/page.tsx", "teacher/diagnostic/page.tsx", "teacher/elective/page.tsx", "teacher/exams/all/page.tsx", "teacher/grades/page.tsx", "teacher/grades/analytics/page.tsx", "teacher/grades/entry/page.tsx", "teacher/grades/stats/page.tsx", "teacher/homework/assignments/page.tsx", "teacher/questions/page.tsx", "teacher/textbooks/page.tsx" ] }, { "name": "parseAiChatPayload", "file": "lib/ai/payload-parser.ts", "signature": "parseAiChatPayload(body: unknown): AiChatRequest", "purpose": "解析并校验AI聊天请求负载", "deps": [ "zod" ], "usedBy": [ "app/api/ai/chat/route.ts" ] }, { "name": "encryptAiApiKey", "file": "lib/ai/api-key-crypto.ts", "signature": "encryptAiApiKey(value: string): string", "purpose": "AES加密AI Provider API Key", "deps": [ "crypto" ], "usedBy": [ "settings/actions.ts" ] }, { "name": "decryptAiApiKey", "file": "lib/ai/api-key-crypto.ts", "signature": "decryptAiApiKey(value: string): string", "purpose": "AES解密AI Provider API Key", "deps": [ "crypto" ], "usedBy": [ "settings/actions.ts", "ai/api-key-crypto.ts内部" ] }, { "name": "testAiProviderConfig", "file": "lib/ai/client.ts", "signature": "testAiProviderConfig(input: { apiKey: string; baseUrl?: string; model: string }): Promise", "purpose": "测试AI Provider连通性(直接配置)", "deps": [ "createAiChatCompletion" ], "usedBy": [ "settings/actions.ts" ] }, { "name": "testAiProviderById", "file": "lib/ai/client.ts", "signature": "testAiProviderById(providerId: string, overrides?: { baseUrl?: string; model?: string }): Promise", "purpose": "测试AI Provider连通性(按ID)", "deps": [ "shared/db", "createAiChatCompletion" ], "usedBy": [ "settings/actions.ts" ] }, { "name": "createAiChatCompletion", "file": "lib/ai/client.ts", "signature": "createAiChatCompletion(input: AiChatRequest): Promise<{ content: string; usage: unknown }>", "purpose": "调用AI模型生成聊天回复", "deps": [ "openai", "shared/db", "ai/provider-config.ts" ], "usedBy": [ "exams/ai-pipeline.ts", "app/api/ai/chat/route.ts" ] }, { "name": "getAiErrorMessage", "file": "lib/ai/errors.ts", "signature": "getAiErrorMessage(v: unknown): string", "purpose": "从AI错误中提取可读消息", "deps": [], "usedBy": [ "exams/ai-pipeline.ts" ] }, { "name": "getAuthContext", "file": "lib/auth-guard.ts", "signature": "getAuthContext(): Promise", "returns": "AuthContext { userId, roles, permissions, dataScope }", "purpose": "获取当前用户完整认证上下文", "deps": [ "shared/lib/session.getSession", "shared/db" ], "usedBy": [ "所有业务模块的Server Actions" ] }, { "name": "requirePermission", "file": "lib/auth-guard.ts", "signature": "requirePermission(permission: Permission): Promise", "throws": "PermissionDeniedError", "purpose": "断言当前用户拥有指定权限", "deps": [ "getAuthContext" ], "usedBy": [ "所有业务模块的Server Actions" ] }, { "name": "checkPermission", "file": "lib/auth-guard.ts", "signature": "checkPermission(permission: Permission): Promise<{ allowed: boolean; ctx: AuthContext }>", "purpose": "非抛出版权限检查", "deps": [ "getAuthContext" ], "usedBy": [] }, { "name": "requireAuth", "file": "lib/auth-guard.ts", "signature": "requireAuth(): Promise", "purpose": "仅断言用户已登录", "deps": [ "getAuthContext" ], "usedBy": [] }, { "name": "resolvePermissions", "file": "lib/permissions.ts", "signature": "resolvePermissions(roleNames: string[]): Permission[]", "purpose": "合并多角色的权限列表(去重)。audit-P1-7 后 auth.ts JWT callback 调用后将结果通过 encodePermissionsBitmap 编码为位图存入 token.permissionsBitmap,不再直接存数组", "deps": [ "ROLE_PERMISSIONS" ], "usedBy": [ "auth.ts (JWT callback)" ] }, { "name": "logAudit", "file": "lib/audit-logger.ts", "signature": "logAudit(params: LogAuditParams): Promise", "purpose": "记录操作日志(静默失败)", "deps": [ "shared/lib/session.getSession", "shared/lib/http-utils.resolveClientIp", "shared/lib/http-utils.getUserAgent", "shared.db", "shared.db.schema.auditLogs", "@paralleldrive/cuid2" ], "usedBy": [ "school/actions.ts", "其他Server Actions" ] }, { "name": "logLoginEvent", "file": "lib/login-logger.ts", "signature": "logLoginEvent(params: LogLoginEventParams): Promise", "purpose": "记录登录日志(signin/signout/signup,静默失败)", "deps": [ "shared/lib/http-utils.resolveClientIp", "shared/lib/http-utils.getUserAgent", "shared.db", "shared.db.schema.loginLogs", "@paralleldrive/cuid2" ], "usedBy": [ "auth.ts (events.signIn, events.signOut)" ] }, { "name": "trackAuthEvent", "file": "lib/track-event.ts", "signature": "trackAuthEvent(event: Extract, params: { userId?, targetId?, properties? }): Promise", "purpose": "✅ audit-P1-9 新增:auth 模块专用埋点函数,封装 trackEvent 自动设置 targetType=\"user\"。8 个 auth 事件:auth.signin_success/signin_failure/signout/signup/2fa_enabled/2fa_disabled/account_locked/rate_limited。用于监控登录成功率、异常登录地理/设备告警、2FA 启用率、账户锁定触发率、限流触发率", "deps": [ "lib/track-event.trackEvent" ], "usedBy": [ "auth.ts (events.signIn 触发 auth.signin_success, events.signOut 触发 auth.signout)", "modules/auth/services/login-service.ts (限流/账户锁定/密码错误/2FA 失败 4 个分支)", "modules/auth/actions.ts (registerAction 成功后触发 auth.signup)", "modules/settings/actions-security.ts (verifyTwoFactorAction 触发 auth.2fa_enabled, disableTwoFactorAction 触发 auth.2fa_disabled)" ] }, { "name": "normalizeRole", "file": "lib/role-utils.ts", "signature": "normalizeRole(value: unknown): NormalizedRole", "purpose": "将角色值规范化为 admin/grade_head/teaching_head/teacher/student/parent 之一(纯函数)。P2-8 修改:不再将 grade_head/teaching_head 映射为 teacher,保留三个角色独立性", "deps": [], "usedBy": [ "auth.ts (jwt/session callbacks)", "lib/role-utils.resolvePrimaryRole", "shared/lib/route-resolver.resolveDefaultPath" ] }, { "name": "resolvePrimaryRole", "file": "lib/role-utils.ts", "signature": "resolvePrimaryRole(roleNames: string[]): NormalizedRole", "purpose": "从多角色列表解析主角色(优先级 admin>grade_head>teaching_head>teacher>student>parent,纯函数)。P2-8 修改:新增 grade_head/teaching_head 优先级", "deps": [ "lib/role-utils.normalizeRole" ], "usedBy": [ "auth.ts (authorize, jwt callback)", "modules/users/data-access.getUserProfile", "modules/onboarding/data-access.getOnboardingStatus" ] }, { "name": "resolveDefaultPath", "file": "lib/route-resolver.ts", "signature": "resolveDefaultPath(roles: string[]): string", "purpose": "按角色列表解析默认跳转路径(纯函数,无 server-only,可在 edge/proxy 运行时使用)。优先级 admin>grade_head/teaching_head>teacher>student>parent,fallback /dashboard。✅ audit-P0-6 新增:合并 proxy.ts 内联实现与 onboarding/data-access.resolveDefaultPathByRoles 重复代码", "deps": [ "lib/role-utils.normalizeRole" ], "usedBy": [ "proxy.ts (onboarding 重定向 + 权限拒绝重定向)", "modules/onboarding/actions.completeOnboardingAction (返回 defaultPath)" ] }, { "name": "SPECIFIC_ROUTE_PERMISSIONS", "file": "lib/route-permissions.ts", "signature": "SPECIFIC_ROUTE_PERMISSIONS: Record", "purpose": "✅ audit-P1-10 + audit-P1-12 新增:精确路由权限配置常量(优先级最高,覆盖前缀匹配)。用于将 /admin/* 下特定页面开放给非管理员角色。audit-P1-10 扩展自 3 个至 15 个条目,新增 /admin/announcements(ANNOUNCEMENT_MANAGE)、/admin/audit-logs + 3 子路径(AUDIT_LOG_READ)、/admin/elective(ELECTIVE_MANAGE)、/admin/questions(QUESTION_READ)、/admin/users(USER_MANAGE)、/admin/error-book(ERROR_BOOK_ANALYTICS_READ)、/admin/lesson-plans(LESSON_PLAN_READ)、/admin/course-plans(COURSE_PLAN_READ)。value 类型为 Permission(非 string),消除 as Permission 断言", "deps": [ "shared/types/permissions.Permissions", "shared/types/permissions.Permission" ], "usedBy": [ "proxy.ts (精确路由权限检查,优先级 1)" ] }, { "name": "ROUTE_PREFIX_PERMISSIONS", "file": "lib/route-permissions.ts", "signature": "ROUTE_PREFIX_PERMISSIONS: Record", "purpose": "✅ audit-P1-12 新增:路由前缀权限配置常量(精确路由未命中时按前缀匹配)。原名 ROUTE_PERMISSIONS,重命名为 ROUTE_PREFIX_PERMISSIONS 以更准确表达语义。5 个角色路由前缀:/admin(SCHOOL_MANAGE)、/teacher(EXAM_CREATE)、/student(HOMEWORK_SUBMIT)、/parent(DASHBOARD_PARENT_READ)、/management(GRADE_MANAGE)", "deps": [ "shared/types/permissions.Permissions", "shared/types/permissions.Permission" ], "usedBy": [ "proxy.ts (路由前缀权限检查,优先级 3)" ] }, { "name": "DASHBOARD_ROUTE_PERMISSIONS", "file": "lib/route-permissions.ts", "signature": "DASHBOARD_ROUTE_PERMISSIONS: Record", "purpose": "✅ audit-P1-12 抽取:仪表盘路由细粒度权限配置常量(优先级 2,覆盖前缀匹配,防止跨角色访问仪表盘)。4 个仪表盘路由:/admin/dashboard、/teacher/dashboard、/student/dashboard、/parent/dashboard 各自独立的读权限", "deps": [ "shared/types/permissions.Permissions", "shared/types/permissions.Permission" ], "usedBy": [ "proxy.ts (仪表盘路由权限检查,优先级 2)" ] }, { "name": "API_ROUTE_PERMISSIONS", "file": "lib/route-permissions.ts", "signature": "API_ROUTE_PERMISSIONS: Record", "purpose": "✅ audit-P1-12 抽取:API 路由前缀权限配置常量。原名 API_PERMISSIONS,重命名为 API_ROUTE_PERMISSIONS 以与 ROUTE_PREFIX_PERMISSIONS 命名一致。当前 1 个条目:/api/ai/chat(AI_CHAT)", "deps": [ "shared/types/permissions.Permissions", "shared/types/permissions.Permission" ], "usedBy": [ "proxy.ts (API 路由权限检查)" ] }, { "name": "encodePermissionsBitmap", "file": "lib/permission-bitmap.ts", "signature": "encodePermissionsBitmap(permissions: Permission[]): string", "purpose": "✅ audit-P1-7 新增:将 Permission[] 编码为 base36 字符串(约 13 字符 vs JSON 数组 1.1KB,体积减少 99%)。每个权限按 PERMISSION_BITMAP_ORDER 数组位置分配 bit (0-66),67 bit 打包为 BigInt → base36 字符串。PERMISSION_BITMAP_ORDER 顺序固定不可变,新增权限只能追加末尾。纯函数无 server-only,可在 edge runtime 使用", "deps": [ "shared/types/permissions.Permissions", "shared/types/permissions.Permission" ], "usedBy": [ "auth.ts (jwt callback 写入 token.permissionsBitmap)" ] }, { "name": "decodePermissionsBitmap", "file": "lib/permission-bitmap.ts", "signature": "decodePermissionsBitmap(bitmap: string): Permission[]", "purpose": "✅ audit-P1-7 新增:将 base36 位图字符串解码为 Permission[]。BigInt 精度安全:手工按位累加 base36 → BigInt(parseBase36ToBigInt 内部函数),避免 parseInt(bitmap, 36) 的 Number 53 bit 精度丢失。无效字符返回空数组(容错)。用于 auth.ts session callback 还原数组供业务代码使用", "deps": [ "shared/types/permissions.Permission" ], "usedBy": [ "auth.ts (session callback 还原 session.user.permissions)" ] }, { "name": "hasPermissionInBitmap", "file": "lib/permission-bitmap.ts", "signature": "hasPermissionInBitmap(bitmap: string, permission: Permission): boolean", "purpose": "✅ audit-P1-7 新增:检查位图中是否包含指定权限,无需完整解码。用于 proxy.ts 等对性能敏感的场景(边缘运行时每请求都经过),避免每次路由检查都解码全部权限", "deps": [ "shared/types/permissions.Permission" ], "usedBy": [ "proxy.ts (路由权限检查 hasPermission 闭包)" ] }, { "name": "normalizeBcryptHash", "file": "lib/bcrypt-utils.ts", "signature": "normalizeBcryptHash(value: string): string", "purpose": "将存储的 bcrypt 哈希规范化为 $2b$ 前缀形式(纯函数,兼容 legacy 无前缀存储)", "deps": [], "usedBy": [ "auth.ts (authorize)" ] }, { "name": "checkBreachedPassword", "file": "lib/breached-password.ts", "signature": "(password: string) => Promise", "purpose": "audit-P2-4 新增:通过 Have I Been Pwned API(k-anonymity 范围查询)检测密码是否在已知数据泄露中出现。仅发送 SHA-1 前 5 字符到 api.pwnedpasswords.com,本地比对完整哈希后缀,不泄露用户密码。带 3 秒超时(AbortController)。fail-open 策略:API 不可用时返回 checkSkipped=true 不阻断注册/改密流程", "deps": [ "crypto.subtle.digest (SHA-1)", "fetch (HIBP API)" ], "usedBy": [ "modules/auth/actions.registerAction", "modules/settings/actions-password.changePasswordAction" ] }, { "name": "resolveClientIp", "file": "lib/http-utils.ts", "signature": "resolveClientIp(): Promise", "purpose": "从请求头解析客户端 IP(x-forwarded-for 第一段/x-real-ip,best-effort,失败返回 unknown)", "deps": [ "next/headers" ], "usedBy": [ "auth.ts (authorize 速率限制键)", "lib/audit-logger.logAudit", "lib/change-logger.logDataChange", "lib/login-logger.logLoginEvent" ] }, { "name": "getUserAgent", "file": "lib/http-utils.ts", "signature": "getUserAgent(): Promise", "purpose": "从请求头解析 User-Agent(best-effort,失败返回 unknown)", "deps": [ "next/headers" ], "usedBy": [ "lib/audit-logger.logAudit", "lib/login-logger.logLoginEvent" ] }, { "name": "getSession", "file": "lib/session.ts", "signature": "getSession(): Promise", "purpose": "session 获取单一入口(server-only,内部 dynamic import @/auth 打破 shared/lib ↔ auth 静态循环依赖)", "deps": [ "@/auth (dynamic import)" ], "usedBy": [ "lib/auth-guard.getAuthContext", "lib/audit-logger.logAudit", "lib/change-logger.logDataChange" ] }, { "name": "getOrCreatePasswordSecurity", "file": "lib/password-security-service.ts", "signature": "getOrCreatePasswordSecurity(db, passwordSecurity, userId: string): Promise", "purpose": "获取或创建用户的 password_security 行(server-only)", "deps": [ "drizzle-orm.eq", "@paralleldrive/cuid2", "shared.db", "shared.db.schema.passwordSecurity" ], "usedBy": [ "auth.ts (authorize)", "lib/password-security-service.recordFailedLogin", "lib/password-security-service.resetFailedLogin" ] }, { "name": "recordFailedLogin", "file": "lib/password-security-service.ts", "signature": "recordFailedLogin(db, passwordSecurity, userId: string): Promise<{ locked: boolean; lockedUntil: Date | null }>", "purpose": "递增失败登录计数,达到阈值则锁定账户(server-only)", "deps": [ "lib/password-security-service.getOrCreatePasswordSecurity", "lib/password-policy.PASSWORD_RULES", "shared.db", "shared.db.schema.passwordSecurity" ], "usedBy": [ "auth.ts (authorize 密码校验失败分支)" ] }, { "name": "resetFailedLogin", "file": "lib/password-security-service.ts", "signature": "resetFailedLogin(db, passwordSecurity, userId: string): Promise", "purpose": "登录成功后重置失败计数与锁定状态(server-only)", "deps": [ "lib/password-security-service.getOrCreatePasswordSecurity", "shared.db", "shared.db.schema.passwordSecurity" ], "usedBy": [ "auth.ts (authorize 登录成功分支)" ] }, { "name": "isAllowedMimeType", "file": "lib/file-storage.ts", "signature": "isAllowedMimeType(mimeType: string): boolean", "purpose": "判断MIME类型是否在允许上传的白名单中", "deps": [ "ALLOWED_MIME_TYPES 常量" ], "usedBy": [ "app/api/upload/route.ts", "files/components/file-upload.tsx" ] }, { "name": "generateStoragePath", "file": "lib/file-storage.ts", "signature": "generateStoragePath(originalName: string): string", "purpose": "根据原始文件名生成存储路径 uploads/YYYY-MM/cuid.ext(相对于public/)", "deps": [ "@paralleldrive/cuid2" ], "usedBy": [ "app/api/upload/route.ts" ] }, { "name": "getFileExtension", "file": "lib/file-storage.ts", "signature": "getFileExtension(filename: string): string", "purpose": "从文件名中提取小写扩展名(不含点)", "deps": [], "usedBy": [ "shared/lib/file-storage 内部" ] }, { "name": "formatFileSize", "file": "lib/file-storage.ts", "signature": "formatFileSize(bytes: number): string", "purpose": "将字节数格式化为人类可读字符串(如 1.5 MB、800 KB)", "deps": [], "usedBy": [ "files/components/file-upload.tsx", "files/components/file-list.tsx", "files/components/file-preview.tsx" ] }, { "name": "exportToExcel", "file": "lib/excel.ts", "signature": "exportToExcel(params: { sheets: ExcelSheet[] }): Promise", "params": { "sheets": "ExcelSheet[],每个含 name/columns/rows" }, "purpose": "将多 sheet 数据导出为 Excel Buffer(表头加粗+冻结首行+自动筛选)", "deps": [ "exceljs" ], "usedBy": [ "users/import-export.exportUsersToExcel", "grades/export.exportGradeRecordsToExcel", "grades/export.exportClassGradeReportToExcel" ] }, { "name": "parseExcel", "file": "lib/excel.ts", "signature": "parseExcel(buffer: Buffer): Promise", "returns": "ParsedSheet[],每个含 sheetName/rows", "purpose": "从 Buffer 解析 Excel 文件,首行作为表头,返回每 sheet 的行记录数组", "deps": [ "exceljs" ], "usedBy": [ "users/actions.importUsersAction", "app/api/import/route.ts" ] }, { "name": "generateTemplate", "file": "lib/excel.ts", "signature": "generateTemplate(params: { sheets: TemplateSheet[] }): Promise", "params": { "sheets": "TemplateSheet[],每个含 name/columns(含 note)/sampleRows?" }, "purpose": "生成导入模板 Buffer(表头加粗+第二行填写说明+示例行)", "deps": [ "exceljs" ], "usedBy": [ "users/import-export.generateUserImportTemplate" ] }, { "name": "useA11yId", "file": "lib/a11y.ts", "signature": "useA11yId(prefix: string): string", "purpose": "基于React.useId生成SSR安全的唯一ID,用于aria-describedby、aria-labelledby等", "deps": [ "react" ], "usedBy": [ "待扩展(表单组件、a11y组件)" ] }, { "name": "mergeA11yProps", "file": "lib/a11y.ts", "signature": "mergeA11yProps>(...props: (T | undefined | null | false)[]): T", "purpose": "合并多组aria/data属性,普通属性后者覆盖前者,aria-*/data-*字符串属性以空格拼接", "deps": [], "usedBy": [ "待扩展" ] }, { "name": "describeInput", "file": "lib/a11y.ts", "signature": "describeInput(describedBy?: string, error?: string, hint?: string): { ariaDescribedBy?: string; ariaInvalid?: boolean }", "purpose": "计算输入框的aria-describedby(合并多个ID)与aria-invalid(error存在则为true)", "deps": [], "usedBy": [ "待扩展(表单组件)" ] }, { "name": "loadingAria", "file": "lib/a11y.ts", "signature": "loadingAria(isLoading: boolean): { ariaBusy: boolean; ariaLive: 'polite' | 'assertive' }", "purpose": "提供加载状态的aria-busy与aria-live=polite属性", "deps": [], "usedBy": [ "待扩展" ] }, { "name": "getInitials", "file": "lib/utils.ts", "signature": "getInitials(name: string | null | undefined): string", "purpose": "从用户姓名提取首字母缩写(最多2字符,用于头像 fallback)", "deps": [], "usedBy": [ "shared/components/ui/avatar.tsx", "modules/dashboard/components/*-dashboard/*-header.tsx", "modules/parent/components/child-card.tsx" ] }, { "name": "formatDateForFile", "file": "lib/utils.ts", "signature": "formatDateForFile(d?: Date): string", "purpose": "格式化日期为 YYYY-MM-DD 用于文件名(P1-c/P2-c 重构:从 grades/export.ts、audit/actions.ts、api/export/route.ts、users/actions.ts 四处重复实现抽取)", "deps": [], "usedBy": [ "grades/actions.exportGradesAction", "audit/actions.exportAuditLogsAction", "api/export/route", "users/actions.exportUsersAction" ] }, { "name": "downloadBase64File", "file": "lib/download.ts", "signature": "downloadBase64File(base64: string, filename: string, mimeType?: string): void", "purpose": "客户端下载 Base64 编码文件(默认 MIME 为 Excel xlsx),P1-c 重构从 grades/export-button、users/user-import-dialog 两处重复实现抽取", "deps": [ "shared/lib/download.downloadBlob" ], "usedBy": [ "grades/components/export-button.tsx", "users/components/user-import-dialog.tsx" ] }, { "name": "downloadBlob", "file": "lib/download.ts", "signature": "downloadBlob(blob: Blob, filename: string): void", "purpose": "客户端下载 Blob 对象(创建临时 URL + a 标签点击 + revoke),P1-c 重构从 audit/audit-log-export-button 抽取", "deps": [], "usedBy": [ "shared/lib/download.downloadBase64File", "audit/components/audit-log-export-button.tsx" ] }, { "name": "handleActionError", "file": "lib/action-utils.ts", "signature": "handleActionError(e: unknown): ActionState", "purpose": "2026-06-23 审计修复新增:统一 Server Action catch 块错误处理。PermissionDeniedError/BusinessError 返回其 message(可安全暴露),其他 Error 返回通用消息并 console.error 记录到服务端日志,避免内部错误消息(如 SQL 错误、堆栈信息)暴露给客户端", "deps": [ "shared/lib/auth-guard.PermissionDeniedError", "shared/types/action-state.ActionState" ], "usedBy": [ "grades/actions", "grades/actions-analytics", "homework/actions", "lesson-preparation/actions", "lesson-preparation/actions-publish", "lesson-preparation/actions-ai", "lesson-preparation/actions-kp", "其他模块 Server Actions" ] }, { "name": "safeActionCall", "file": "lib/action-utils.ts", "signature": "safeActionCall(action: () => Promise>, options?: { onError?: (error: unknown) => void; onFinally?: () => void }): Promise | null>", "purpose": "2026-06-23 审计修复新增:客户端调用 Server Action 的 try/catch/finally 包装器。Action 抛出异常时执行 onError 回调,无论成功失败执行 onFinally 回调(用于重置 loading 状态),防止 UI 永久卡 loading", "deps": [ "shared/types/action-state.ActionState" ], "usedBy": [ "grades/components/grade-record-list", "grades/components/grade-record-form", "grades/components/batch-grade-entry", "grades/components/export-button", "其他模块客户端组件(20+ 处)" ] }, { "name": "safeJsonParse", "file": "lib/action-utils.ts", "signature": "safeJsonParse(json: string, errorMessage: string): T", "purpose": "2026-06-23 审计修复新增:安全 JSON.parse,失败时抛出 ValidationError(替代裸 JSON.parse,避免 SyntaxError 被外层 catch 捕获后暴露解析细节给客户端)", "deps": [ "shared/lib/action-utils.ValidationError" ], "usedBy": [ "grades/actions.batchCreateGradeRecordsAction", "homework/actions.batchAutoGradeSubmissionsAction", "其他模块 Server Actions(8+ 处)" ] }, { "name": "safeParseDate", "file": "lib/action-utils.ts", "signature": "safeParseDate(value: string, fieldName: string): Date", "purpose": "2026-06-23 审计修复新增:校验日期字符串有效性,无效则抛出 ValidationError。用于 Server Action 中包装 new Date() 调用,避免 Invalid Date 传播到下游逻辑", "deps": [ "shared/lib/action-utils.ValidationError" ], "usedBy": [ "lesson-preparation/actions-publish", "其他模块 Server Actions(15+ 处)" ] }, { "name": "safeParseNumber", "file": "lib/action-utils.ts", "signature": "safeParseNumber(value: string, fieldName: string): number", "purpose": "2026-06-23 审计修复新增:校验数字字符串,无效则抛出 ValidationError。用于 Server Action 中包装 Number() 调用", "deps": [ "shared/lib/action-utils.ValidationError" ], "usedBy": [ "其他模块 Server Actions" ] }, { "name": "escapeLikePattern", "file": "lib/action-utils.ts", "signature": "escapeLikePattern(input: string): string", "purpose": "2026-06-23 审计修复新增:转义 SQL LIKE 通配符(% _ \\),防止用户输入干扰模糊查询。用于构建 sql`LIKE ${'%'+escapeLikePattern(q)+'%'}` 模式", "deps": [], "usedBy": [ "其他模块 data-access 模糊查询" ] }, { "name": "BusinessError", "file": "lib/action-utils.ts", "signature": "class BusinessError extends Error { constructor(message: string, code?: string) }", "purpose": "2026-06-23 审计修复新增:已知业务错误基类,message 可安全返回客户端(由 handleActionError 识别)。子类:NotFoundError(资源不存在)、ValidationError(输入校验错误)", "deps": [], "usedBy": [ "grades/actions.assertClassInScope", "grades/data-access.updateGradeRecord", "grades/data-access.deleteGradeRecord", "shared/lib/action-utils.handleActionError" ] }, { "name": "NotFoundError", "file": "lib/action-utils.ts", "signature": "class NotFoundError extends BusinessError { constructor(resource: string) }", "purpose": "2026-06-23 审计修复新增:资源不存在错误。自动生成 `${resource} 不存在` 消息,code: not_found。用于 data-access 层存在性检查失败时抛出", "deps": [ "shared/lib/action-utils.BusinessError" ], "usedBy": [ "grades/data-access.updateGradeRecord", "grades/data-access.deleteGradeRecord" ] }, { "name": "ValidationError", "file": "lib/action-utils.ts", "signature": "class ValidationError extends BusinessError { constructor(message: string) }", "purpose": "2026-06-23 审计修复新增:输入校验错误,code: validation_error。用于 safeJsonParse/safeParseDate/safeParseNumber 解析失败时抛出", "deps": [ "shared/lib/action-utils.BusinessError" ], "usedBy": [ "shared/lib/action-utils.safeJsonParse", "shared/lib/action-utils.safeParseDate", "shared/lib/action-utils.safeParseNumber" ] }, { "name": "createQueryClient", "file": "lib/query-client.ts", "signature": "createQueryClient(): QueryClient", "purpose": "V5 状态管理统一专项新增:TanStack Query 工厂,默认配置 staleTime 30s / retry 1 / refetchOnWindowFocus false / mutations.retry 0 + 全局 onError 兜底(notify.error + console.error)。必须在客户端 useState 内调用,避免 SSR 跨用户共享缓存", "deps": [ "@tanstack/react-query" ], "usedBy": [ "app/providers.tsx" ] }, { "name": "configureNotify + notify + rawToast", "file": "lib/notify.ts", "signature": "configureNotify(resolver: I18nResolver): void; const notify: { success, error, info, warning, promise }; const rawToast: typeof sonner.toast", "purpose": "V5 状态管理统一专项新增:统一 toast 接口。notify 包装 sonner 并内置 i18n key 解析(消除 100+ 处 t(\"xxx\") 重复)。通过 configureNotify 在 Providers 的 NotifyConfigurator 中注入 next-intl 的 t 函数。rawToast 保留 sonner escape hatch", "deps": [ "sonner", "next-intl (运行时通过 configureNotify 注入)" ], "usedBy": [ "app/providers.tsx (configureNotify)", "shared/lib/query-client.ts (notify.error 全局兜底)", "业务组件迁移后由各模块使用" ] }, { "name": "createServiceProvider", "file": "lib/create-service-provider.tsx", "signature": "createServiceProvider(displayName: string): { Provider, useService, Context }", "purpose": "V5 状态管理统一专项新增:Service DI Provider 工厂,替换项目中 14 处重复的 createContext(null) + Provider + useService 抛错模板。用于注入模块数据服务对象(不可变引用),非可变状态", "deps": [ "react" ], "usedBy": [ "待迁移:announcements / attendance / audit / dashboard / messaging / settings 等模块的 Service DI Provider" ] }, { "name": "apiSuccess", "file": "lib/api-response.ts", "signature": "apiSuccess(data: T, init?: { status?: number; headers?: HeadersInit }): NextResponse", "purpose": "构造 API 成功响应 { success: true, data }", "deps": [ "NextResponse" ], "usedBy": [ "app/api/**/route.ts" ] }, { "name": "apiError", "file": "lib/api-response.ts", "signature": "apiError(message: string, status: number, errorCode?: string, init?: { headers?: HeadersInit }): NextResponse", "purpose": "构造 API 失败响应 { success: false, message, errorCode }", "deps": [ "NextResponse" ], "usedBy": [ "app/api/**/route.ts" ] }, { "name": "apiFromAction", "file": "lib/api-response.ts", "signature": "apiFromAction(result: ActionState, options?: { successStatus?: number; headers?: HeadersInit }): NextResponse", "purpose": "从 ActionState 构造响应,通过 errorCode 驱动状态码(not_found→404 / validation_error→400 / auth_required→401 / permission_denied→403 / 其他→500),替代字符串匹配推导", "deps": [ "ActionState", "NextResponse" ], "usedBy": [ "api/upload", "api/files/[id]", "api/files/batch-delete" ] }, { "name": "handleApiError", "file": "lib/api-response.ts", "signature": "handleApiError(error: unknown, init?: { headers?: HeadersInit }): NextResponse", "purpose": "API 路由异常处理器:PermissionDeniedError→403 / NotFoundError→404 / ValidationError→400 / BusinessError→400 / 其他→500,已知业务错误不记录日志,未预期错误 console.error 记录", "deps": [ "PermissionDeniedError", "BusinessError", "NotFoundError", "ValidationError" ], "usedBy": [ "withApiErrorHandler" ] }, { "name": "withApiErrorHandler", "file": "lib/api-response.ts", "signature": "withApiErrorHandler(handler: (...args: TArgs) => Promise): (...args: TArgs) => Promise", "purpose": "HOF 包装 route handler,统一异常处理,消除每个 route 重复 try/catch 模板", "deps": [ "handleApiError" ], "usedBy": [ "api/search", "api/proctoring/event", "api/export", "api/import", "api/ai/chat", "api/cron/audit-cleanup", "api/rate-limit-test" ] }, { "name": "parseJsonBody", "file": "lib/api-response.ts", "signature": "parseJsonBody(req: Request): Promise", "purpose": "安全解析 JSON 请求体,失败抛 ValidationError(由 withApiErrorHandler 转 400)", "deps": [ "ValidationError" ], "usedBy": [ "api/proctoring/event" ] }, { "name": "errorToStatus", "file": "lib/api-response.ts", "signature": "errorToStatus(error: unknown): number", "purpose": "错误→HTTP 状态码集中映射,避免字符串匹配推导状态码", "deps": [ "PermissionDeniedError", "NotFoundError", "ValidationError", "BusinessError" ], "usedBy": [ "handleApiError" ] }, { "name": "formatSseEvent", "file": "lib/sse.ts", "signature": "formatSseEvent(data: unknown): string", "purpose": "序列化 payload 为 SSE data: 行(data: {...}\\n\\n)", "deps": [], "usedBy": [ "api/ai/chat/stream", "api/notifications/stream" ] }, { "name": "formatSseError", "file": "lib/sse.ts", "signature": "formatSseError(message: string): string", "purpose": "格式化标准错误事件 { type: 'error', message }", "deps": [ "formatSseEvent" ], "usedBy": [ "api/ai/chat/stream", "api/notifications/stream" ] }, { "name": "formatSseDone", "file": "lib/sse.ts", "signature": "formatSseDone(): string", "purpose": "SSE 终止信号 data: [DONE]\\n\\n", "deps": [], "usedBy": [ "api/ai/chat/stream", "api/notifications/stream" ] }, { "name": "createSseResponse", "file": "lib/sse.ts", "signature": "createSseResponse(stream: ReadableStream): Response", "purpose": "构造标准 SSE 成功响应(流建立后使用),含 Content-Type/Cache-Control/Connection 头", "deps": [], "usedBy": [ "api/ai/chat/stream", "api/notifications/stream" ] }, { "name": "createSseError", "file": "lib/sse.ts", "signature": "createSseError(message: string, status: number): Response", "purpose": "在流建立前返回错误(鉴权失败、参数校验失败),text/event-stream Content-Type", "deps": [ "formatSseError" ], "usedBy": [ "api/ai/chat/stream", "api/notifications/stream" ] }, { "name": "MemoryCacheStore", "file": "lib/cache/memory-store.ts", "signature": "class MemoryCacheStore implements CacheStore { getOrSet(key, producer, options): Promise; invalidateTags(tags): Promise }", "purpose": "CacheStore 内存实现:Map + LRU 淘汰(maxEntries 默认 500)+ tag 反查索引 Map>,TTL 惰性删除。适用于单实例 dev/test", "deps": [ "server-only", "./types" ], "usedBy": [ "shared/lib/cache/store-factory" ] }, { "name": "RedisCacheStore", "file": "lib/cache/redis-store.ts", "signature": "class RedisCacheStore implements CacheStore { getOrSet(key, producer, options): Promise; invalidateTags(tags): Promise }", "purpose": "CacheStore Redis 实现:key 格式 next-edu:cache:{key},value JSON.stringify,tag 反查索引用 Redis Set,TTL 通过 expire 原子设置。Redis 不可用时故障降级透传 producer", "deps": [ "server-only", "@/shared/lib/redis-client", "./types" ], "usedBy": [ "shared/lib/cache/store-factory" ] }, { "name": "getCacheStore", "file": "lib/cache/store-factory.ts", "signature": "getCacheStore(): Promise", "purpose": "获取当前进程 CacheStore 单例(默认 MemoryCacheStore,CACHE_DRIVER=redis 时懒加载 RedisCacheStore)", "deps": [ "env.mjs", "./memory-store", "./redis-store" ], "usedBy": [ "shared/lib/cache/cache-fn", "shared/lib/cache/invalidate" ] }, { "name": "cacheFn", "file": "lib/cache/cache-fn.ts", "signature": "cacheFn(fn: (...args: TArgs) => Promise, options: CacheFnOptions): (...args: TArgs) => Promise", "purpose": "服务端数据缓存双层包装器:外层 react.cache 提供请求级 memoization(同 RSC 请求内去重),内层 cacheStore.getOrSet 跨请求/跨实例缓存。命中且未过期返回缓存值,否则调用 producer 并按 tag 写入 CacheStore", "deps": [ "./store-factory", "react" ], "usedBy": [ "modules/* data-access" ] }, { "name": "fillTemplate", "file": "lib/cache/invalidation-map.ts", "signature": "fillTemplate(template: string, params: Record): string", "purpose": "模板占位符填充({key} → params[key]),缺失参数时保留原占位符", "deps": [], "usedBy": [ "shared/lib/cache/invalidate" ] }, { "name": "INVALIDATION_MAP", "file": "lib/cache/invalidation-map.ts", "signature": "INVALIDATION_MAP: Record", "purpose": "集中式失效映射表:每个 mutation actionId 声明其副作用 tags/queryKeys/paths(服务端三段)", "deps": [ "./types" ], "usedBy": [ "shared/lib/cache/invalidate" ] }, { "name": "CLIENT_INVALIDATION_MAP", "file": "lib/cache/client-invalidation-map.ts", "signature": "CLIENT_INVALIDATION_MAP: Record", "purpose": "客户端可见的失效映射子集(仅 queryKeys 字段),从 invalidation-map 提取以避免客户端 bundle 拉入 revalidateTag 等 server-only 依赖。由 useActionMutation 在 onSuccess 中查询以自动 invalidateQueries", "deps": [], "usedBy": [ "shared/hooks/use-action-mutation" ] }, { "name": "invalidateFor", "file": "lib/cache/invalidate.ts", "signature": "invalidateFor(actionId: string, params?: Record): Promise", "purpose": "Server Action 写操作成功后集中编排缓存失效三步:1) CacheStore.invalidateTags 2) Next.js revalidateTag × N 3) revalidatePath × N", "deps": [ "./invalidation-map", "./store-factory", "next/cache" ], "usedBy": [ "modules/* Server Actions" ] }, { "name": "getRedisClient", "file": "lib/redis-client.ts", "signature": "getRedisClient(): Promise", "purpose": "共享 Redis 客户端单例(rate-limit + cache 共用)。复用 env.UPSTASH_REDIS_REST_URL/TOKEN,动态 import + webpackIgnore。包未安装或 env 未配置时返回 null(fail-open 降级)", "deps": [ "server-only", "@/env.mjs", "@upstash/redis (optional, webpackIgnore)" ], "usedBy": [ "shared/lib/cache/redis-store", "shared/lib/rate-limit/redis-limiter" ] }, { "name": "queryKeys", "file": "lib/query-keys.ts", "signature": "queryKeys: { classes: { all, lists, list(filters), details, detail(id), students(classId), schedule(classId), stats(classId), invitations(classId) } }", "purpose": "客户端 queryKey 工厂对象。命名约定 [module, resource, ...args],失效时使用前缀匹配 invalidateQueries({ queryKey: ['classes', 'detail'] }) 失效所有 ['classes', 'detail', *]。后续模块按此模式扩展", "deps": [], "usedBy": [ "modules/classes/components/*", "shared/hooks/use-action-query", "shared/hooks/use-action-mutation" ] } ], "hooks": [ { "name": "useActionWithToast", "file": "hooks/use-action-with-toast.ts", "signature": "useActionWithToast(): { isPending: boolean; execute: (action: () => Promise>) => void }", "purpose": "包装Server Action + toast反馈" }, { "name": "useActionMutation", "file": "hooks/use-action-mutation.ts", "signature": "useActionMutation(options?: UseActionMutationOptions): { isWorking: boolean; mutate: (action?: () => Promise>) => Promise | undefined> }。Options 为联合类型:旧版 { successMessage?, errorMessage?, onSuccess?, onError? } 或新版 { mutationFn?, actionId?, params?, invalidateQueryKeys?, successMessage?, errorMessage?, onSuccess?, onError? }。新版接入 QueryClient,成功后按 CLIENT_INVALIDATION_MAP[actionId] 自动 invalidateQueries。mutate 参数可选:传 action 走旧模式,不传则使用 options.mutationFn", "purpose": "通用 Server Action mutation Hook,P1-4 重构从 50+ 个文件中重复的 setIsWorking(true) + try/catch/finally + toast 模式抽取。V5 缓存策略重构:接入 QueryClient + actionId 自动 invalidate,向后兼容旧调用方(mutate(action) 仍可用)", "usedBy": [ "school/components/school-form-dialog.tsx", "school/components/school-delete-dialog.tsx" ] }, { "name": "useActionQuery", "file": "hooks/use-action-query.ts", "signature": "useActionQuery(action: () => Promise>, options?: UseActionQueryOptions): { data, loading, error, refetch }。Options 为联合类型:旧版 { deps?, enabled?, errorMessage? } 或新版 UseActionQueryWithCacheOptions(含 queryKey + UseQueryOptions)。传入 queryKey 走 useQuery 跨页共享缓存;不传走旧 useEffect + useState。Hook 内部始终声明 useQuery + useState/useEffect 以遵守 React Hooks 规则", "purpose": "通用 Server Action 查询 Hook,P1-4 重构从 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式抽取。V5 缓存策略重构:新增 queryKey 入参走 QueryClient,向后兼容旧调用方。内置竞态防护(requestId ref)", "usedBy": [ "questions/components/create-question-dialog.tsx" ] }, { "name": "useDebounce", "file": "hooks/use-debounce.ts", "signature": "useDebounce(value: T, delay?: number): T", "purpose": "防抖Hook" }, { "name": "useMediaQuery", "file": "hooks/use-media-query.ts", "signature": "useMediaQuery(query: string): boolean", "purpose": "响应式媒体查询Hook" }, { "name": "useLocalStorage", "file": "hooks/use-local-storage.ts", "signature": "useLocalStorage(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void]", "purpose": "localStorage持久化Hook" }, { "name": "useCurrentTime", "file": "hooks/use-current-time.ts", "signature": "useCurrentTime(intervalMs?: number): Date", "purpose": "V4(P2-5)新增:返回当前时间并按指定间隔自动更新的 Hook,用于需要随时间刷新的 UI(如课表'进行中'徽章),避免 useMemo 依赖 [items] 导致过时" }, { "name": "usePermission", "file": "hooks/use-permission.ts", "signature": "usePermission(): { permissions: Permission[]; roles: string[]; hasPermission: (p: Permission) => boolean; hasAnyPermission: (...p: Permission[]) => boolean; hasAllPermissions: (...p: Permission[]) => boolean; hasRole: (r: string) => boolean }", "purpose": "客户端权限检查Hook", "usedBy": [ "layout/app-sidebar.tsx", "exams/components", "homework/components" ] }, { "name": "logDataChange", "file": "lib/change-logger.ts", "signature": "logDataChange(params: LogDataChangeParams): Promise", "purpose": "记录数据变更日志(写入 dataChangeLogs 表,自动获取 changedBy/changedByName/ipAddress;静默失败)", "deps": [ "shared/lib/session.getSession", "shared/lib/http-utils.resolveClientIp", "shared/db (dataChangeLogs)", "@paralleldrive/cuid2" ], "usedBy": [ "待扩展(数据变更场景)" ] }, { "name": "StorageProvider", "file": "lib/storage-provider.ts", "signature": "interface StorageProvider { save(file: Buffer, storagePath: string): Promise; read(storagePath: string): Promise; delete(storagePath: string): Promise; exists(storagePath: string): Promise; getUrl(storagePath: string): string }", "purpose": "文件存储抽象接口,便于未来切换到 OSS/S3", "usedBy": [ "app/api/files/batch-delete/route.ts" ] }, { "name": "LocalStorageProvider", "file": "lib/storage-provider.ts", "signature": "class LocalStorageProvider implements StorageProvider", "purpose": "本地磁盘存储实现,文件持久化到 public/uploads/...,URL 为 /uploads/...", "deps": [ "fs/promises", "path" ], "usedBy": [ "通过 storageProvider 实例使用" ] }, { "name": "storageProvider", "file": "lib/storage-provider.ts", "signature": "const storageProvider: StorageProvider", "purpose": "默认存储 Provider 单例(LocalStorageProvider 实例),替换此实例可迁移到 OSS/S3", "usedBy": [ "app/api/files/batch-delete/route.ts" ] }, { "name": "useDialogState", "file": "hooks/use-dialog-state.ts", "signature": "useDialogState(): readonly [boolean, () => void, () => void, () => void]", "purpose": "V5 状态管理统一专项新增:Dialog/Sheet 开关状态 Hook,替换项目中 30+ 处 const [open, setOpen] = useState(false) 模式。提供语义化方法名 openModal / closeModal / toggle,避免散落 setOpen(true)/setOpen(false) 调用", "usedBy": [ "待迁移:users/user-import-dialog.tsx、textbooks/textbook-form-dialog.tsx、error-book/error-book-detail-dialog.tsx 等 30+ 处 Dialog 组件" ] } ], "stores": [ { "name": "useUiStore + useModal", "file": "stores/ui-store.ts", "signature": "useUiStore: zustand store { modals, openModal, closeModal, isOpen, closeAll }; useModal(name): { open, props, openModal, closeModal }", "purpose": "V5 状态管理统一专项新增:全局 UI store(zustand),集中管理 modal registry。替换 30+ 处 Dialog 各自 useState(false) 的反模式。支持命令式 API(openModal/closeModal)和 Hook API(useModal)双入口。与 sonner 的分工:toast 走 shared/lib/notify,modal 走本 store", "deps": [ "zustand" ], "usedBy": [ "待迁移:30+ 处 Dialog 组件切换为 useModal(name)" ] } ], "components": [ { "name": "AuthSessionProvider", "file": "components/auth-session-provider.tsx", "purpose": "NextAuth SessionProvider包装", "usedBy": [ "app/layout.tsx" ] }, { "name": "OnboardingGate", "file": "components/onboarding-gate.tsx", "purpose": "新用户引导流程(已废弃,逻辑迁移至 modules/onboarding/)", "deprecated": true, "usedBy": [] }, { "name": "ThemeProvider", "file": "components/theme-provider.tsx", "purpose": "next-themes主题切换", "usedBy": [ "app/layout.tsx" ] }, { "name": "EmptyState", "file": "components/ui/empty-state.tsx", "purpose": "列表空状态展示", "usedBy": [ "exams", "homework", "questions", "textbooks" ] }, { "name": "GlobalSearch", "file": "components/global-search.tsx", "props": "{ className?, placeholder? }", "purpose": "全局搜索组件(防抖300ms调用 GET /api/search,Cmd/Ctrl+K快捷键聚焦,Escape关闭,↑/↓键盘导航,Enter跳转;下拉展示 question/textbook/exam/announcement 四类结果)", "internalDeps": [ "useDebounce", "Input", "Link", "useRouter" ], "usedBy": [ "layout/components/site-header.tsx" ] }, { "name": "Switch", "file": "components/ui/switch.tsx", "basedOn": "@radix-ui/react-switch", "props": "Radix Switch Root props (checked, onCheckedChange, disabled, id, aria-label)", "purpose": "开关切换UI组件(shadcn风格,checked/unchecked两态)", "usedBy": [ "settings/components/notification-preferences-form.tsx" ] }, { "name": "StatCard", "file": "components/ui/stat-card.tsx", "props": "{ title, value, icon?, description?, color?, highlight?, href?, isLoading?, valueClassName? }", "purpose": "统计卡片(标题+数值+图标+描述+跳转+骨架屏),P1-a 重构从 8 处重复实现抽取(teacher/student/admin dashboard stats、class-overview-stats、grade insights 等)", "internalDeps": [ "Card", "CardHeader", "CardTitle", "CardContent", "Link", "cn", "Skeleton" ], "usedBy": [ "dashboard/components/teacher-dashboard/teacher-stats.tsx", "dashboard/components/student-dashboard/student-stats-grid.tsx", "dashboard/components/admin-dashboard/admin-dashboard.tsx", "classes/components/class-detail/class-overview-stats.tsx", "app/(dashboard)/admin/school/grades/insights/page.tsx", "app/(dashboard)/management/grade/insights/page.tsx" ] }, { "name": "StatItem", "file": "components/ui/stat-item.tsx", "props": "{ label, value, icon?, hint?, valueClassName? }", "purpose": "紧凑统计项(label+icon+value+hint,用于统计面板网格),P1-a 重构从 attendance-stats-card、grade-stats-card 两处重复实现抽取。Phase 4.6:grades 模块 grade-stats-card.tsx 已删除,class-grade-report.tsx 改用 StatsGrid + StatItem 组合", "internalDeps": [ "cn" ], "usedBy": [ "attendance/components/attendance-stats-card.tsx", "grades/components/class-grade-report.tsx", "shared/components/ui/stats-grid.tsx" ] }, { "name": "ChipNav", "file": "components/ui/chip-nav.tsx", "props": "{ options: ChipNavOption[], currentId, buildHref: (id) => string, size?: 'sm'|'xs', allOption?, className? }", "purpose": "芯片导航组(通过 URL search params 切换筛选维度,Link 跳转),P1-b 重构从 stats-class-selector、attendance-stats-class-selector、analytics-filters 三处重复实现抽取。Phase 4.6:grades 模块 analytics-filters.tsx 已删除,analytics-grade-filters.tsx(app 层)改用 FilterBar + ChipNav 组合", "internalDeps": [ "Link", "cn" ], "usedBy": [ "grades/components/stats-class-selector.tsx", "attendance/components/attendance-stats-class-selector.tsx", "app/(dashboard)/teacher/grades/analytics/analytics-grade-filters.tsx" ] }, { "name": "PageHeader", "file": "components/ui/page-header.tsx", "props": "{ title, description?, icon?: ComponentType, actions?: ReactNode, className? }", "purpose": "页面头部(标题+描述+icon+actions,响应式布局:移动端纵向,桌面端横向),P2-b 重构从 admin-dashboard、profile/page、settings/security/page 三处内联头部抽取", "internalDeps": [ "cn" ], "usedBy": [ "dashboard/components/admin-dashboard/admin-dashboard.tsx", "app/(dashboard)/profile/page.tsx", "app/(dashboard)/settings/security/page.tsx", "modules/settings/components/settings-view.tsx" ] }, { "name": "FilterBar", "file": "components/ui/filter-bar.tsx", "props": "{ children, hasFilters?, onReset?, layout?: 'default'|'wrap'|'between', gapClassName?, className?, resetClassName? }", "purpose": "筛选栏容器(统一布局壳 + Reset 按钮),P3-b 重构从 exam/textbook/question/audit-log/login-log filters 五处重复布局抽取。URL 状态管理方式(nuqs/router/callback)由各模块自行处理。Phase 4.6:grades 模块 3 个 filter 组件迁移至 app 层并复用 FilterBar(student-grade-filters / teacher-grade-query-filters / analytics-grade-filters)", "internalDeps": [ "Button", "cn" ], "usedBy": [ "exams/components/exam-filters.tsx", "textbooks/components/textbook-filters.tsx", "questions/components/question-filters.tsx", "audit/components/audit-log-filters.tsx", "audit/components/login-log-filters.tsx", "app/(dashboard)/student/grades/student-grade-filters.tsx", "app/(dashboard)/teacher/grades/teacher-grade-query-filters.tsx", "app/(dashboard)/teacher/grades/analytics/analytics-grade-filters.tsx" ] }, { "name": "FilterSearchInput", "file": "components/ui/filter-bar.tsx", "props": "{ value, onChange, placeholder?, className?, inputClassName? }", "purpose": "筛选栏搜索框(带 Search 图标的 Input),P3-b 重构从 exam/textbook/question filters 三处重复搜索框抽取。Phase 4.6:grades 模块 student-grade-filters 复用", "internalDeps": [ "Input", "Search (lucide-react)", "cn" ], "usedBy": [ "exams/components/exam-filters.tsx", "textbooks/components/textbook-filters.tsx", "questions/components/question-filters.tsx", "app/(dashboard)/student/grades/student-grade-filters.tsx" ] }, { "name": "FilterResetButton", "file": "components/ui/filter-bar.tsx", "props": "{ onClick, className? }", "purpose": "筛选栏重置按钮(Reset + X 图标),P3-b 重构从 6 个 filter 文件中重复的 Reset 按钮抽取", "internalDeps": [ "Button", "X (lucide-react)", "cn" ], "usedBy": [ "FilterBar(内部使用)" ] }, { "name": "ChartCardShell", "file": "components/charts/chart-card-shell.tsx", "props": "{ title, description?, icon?, iconClassName?, titleClassName?, isEmpty?, emptyTitle?, emptyDescription?, emptyIcon?, emptyClassName?, children, className?, contentClassName? }", "purpose": "图表卡片外壳(Card + CardHeader + EmptyState + CardContent 统一结构),P3-c 重构从 8 个图表文件重复的 Card 包装抽取", "internalDeps": [ "Card", "CardHeader", "CardTitle", "CardDescription", "CardContent", "EmptyState", "cn" ], "usedBy": [ "grades/components/grade-trend-chart.tsx", "grades/components/grade-distribution-chart.tsx", "grades/components/class-comparison-chart.tsx", "grades/components/subject-comparison-chart.tsx", "dashboard/components/teacher-dashboard/teacher-grade-trends.tsx", "dashboard/components/student-dashboard/student-grades-card.tsx", "parent/components/child-grade-summary.tsx", "diagnostic/components/mastery-radar-chart.tsx" ] }, { "name": "TrendLineChart", "file": "components/charts/trend-line-chart.tsx", "props": "{ data, series: TrendLineSeries[], xKey?, yDomain?, yTickFormatter?, xTickFormatter?, heightClassName?, margin?, yWidth?, tooltipClassName?, tooltipLabelKey?, className? }", "purpose": "趋势折线图(LineChart 统一配置:CartesianGrid + XAxis + YAxis + ChartTooltip + Line),P3-c 重构从 4 个 LineChart 文件(grade-trend-chart、teacher-grade-trends、student-grades-card、child-grade-summary)几乎逐行相同的配置抽取", "internalDeps": [ "ChartContainer", "ChartTooltip", "ChartTooltipContent", "CartesianGrid", "Line", "LineChart", "XAxis", "YAxis", "cn" ], "usedBy": [ "grades/components/grade-trend-chart.tsx", "dashboard/components/teacher-dashboard/teacher-grade-trends.tsx", "dashboard/components/student-dashboard/student-grades-card.tsx", "parent/components/child-grade-summary.tsx" ] }, { "name": "SimpleBarChart", "file": "components/charts/simple-bar-chart.tsx", "props": "{ data, bars: BarSeries[], xKey, yDomain?, yAllowDecimals?, yTickFormatter?, xTickFormatter?, xTruncateLength?, yWidth?, heightClassName?, margin?, showLegend?, tooltipClassName?, tooltipFormatter?, cellColors?, className? }", "purpose": "柱状图(BarChart 统一配置:CartesianGrid + XAxis + YAxis + ChartTooltip + Bar),P3-c 重构从 grade-distribution-chart(单 Bar + Cell 分桶着色)和 class-comparison-chart(多 Bar + Legend)抽取", "internalDeps": [ "ChartContainer", "ChartTooltip", "ChartTooltipContent", "Bar", "BarChart", "CartesianGrid", "Legend", "XAxis", "YAxis", "Cell", "cn" ], "usedBy": [ "grades/components/grade-distribution-chart.tsx", "grades/components/class-comparison-chart.tsx" ] }, { "name": "ComparisonRadarChart", "file": "components/charts/comparison-radar-chart.tsx", "props": "{ data, series: RadarSeries[], angleKey, angleTickFormatter?, angleTickFontSize?, domain?, tickCount?, showLegend?, heightClassName?, tooltipClassName?, className?, gridStrokeDasharray?, gridStrokeOpacity? }", "purpose": "对比雷达图(RadarChart 统一配置:PolarGrid + PolarAngleAxis + PolarRadiusAxis + ChartTooltip + Radar),P3-c 重构从 subject-comparison-chart(双 Radar:averageScore + passRate)和 mastery-radar-chart(双 Radar:student + classAverage,含条件 Legend)抽取", "internalDeps": [ "ChartContainer", "ChartTooltip", "ChartTooltipContent", "PolarAngleAxis", "PolarGrid", "PolarRadiusAxis", "Radar", "RadarChart", "Legend", "cn" ], "usedBy": [ "grades/components/subject-comparison-chart.tsx", "diagnostic/components/mastery-radar-chart.tsx" ] }, { "name": "ScheduleList", "file": "components/schedule/schedule-list.tsx", "props": "{ items: ScheduleListItemData[], variant?: 'separator'|'card', spacingClassName?, renderTrailing?, className? }", "purpose": "课表列表(课程+时间+地点+班级徽章),P3-a 重构从 student-today-schedule-card、child-schedule-card、student-schedule-view 三处逐行复制的列表项渲染抽取。支持 separator(分隔线)和 card(卡片)两种变体", "internalDeps": [ "ScheduleListItem", "cn" ], "usedBy": [ "dashboard/components/student-dashboard/student-today-schedule-card.tsx", "parent/components/child-schedule-card.tsx", "student/components/student-schedule-view.tsx" ] }, { "name": "ScheduleListItem", "file": "components/schedule/schedule-list.tsx", "props": "{ item: ScheduleListItemData, variant?: 'separator'|'card', trailing?, className? }", "purpose": "课表列表项(单条课程渲染:course + Clock + MapPin + Badge),P3-a 重构从 3 个课表文件中重复的列表项抽取", "internalDeps": [ "Badge", "Clock (lucide-react)", "MapPin (lucide-react)", "cn" ], "usedBy": [ "ScheduleList(内部使用)" ] }, { "name": "QuestionBankFilters", "file": "components/question/question-bank-filters.tsx", "props": "{ search, onSearchChange, type, onTypeChange, difficulty, onDifficultyChange, layout?: 'default'|'compact', className? }", "purpose": "题库筛选栏(搜索+题型+难度),P3-d 重构从 exam-assembly(compact 布局)和 question-bank-picker(default 布局,同时将原生 HTML input/select 迁移到 shadcn Input/Select)两处重复筛选栏抽取。状态管理方式由调用方自行处理", "internalDeps": [ "Select", "SelectContent", "SelectItem", "SelectTrigger", "SelectValue", "FilterSearchInput", "cn" ], "usedBy": [ "exams/components/exam-assembly.tsx", "lesson-preparation/components/question-bank-picker.tsx" ] }, { "name": "ConfirmDeleteDialog", "file": "components/ui/confirm-delete-dialog.tsx", "props": "{ open, onOpenChange, title, description, onConfirm, isWorking?, confirmText?, cancelText?, destructive? }", "purpose": "通用删除确认对话框(AlertDialog 包装),P0-1 重构从 announcement-detail、message-detail、course-plan-detail、grade-classes-view、students-table 五处重复的 AlertDialog 删除确认块抽取", "internalDeps": [ "AlertDialog", "AlertDialogContent", "AlertDialogHeader", "AlertDialogTitle", "AlertDialogDescription", "AlertDialogFooter", "AlertDialogCancel", "AlertDialogAction", "cn" ], "usedBy": [ "announcements/components/announcement-detail.tsx", "messaging/components/message-detail.tsx", "course-plans/components/course-plan-detail.tsx", "classes/components/grade-classes-view.tsx", "classes/components/students-table.tsx" ] }, { "name": "Pagination", "file": "components/ui/pagination.tsx", "props": "{ page, pageSize, total, totalPages, onPageChange, itemLabel? }", "purpose": "通用分页 UI(Showing X-Y of Z + Page X of Y + 上一页/下一页按钮),P0-2 重构从 audit-log-table、login-log-table、data-change-log-table 三处重复的分页块抽取", "internalDeps": [ "Button", "ChevronLeft (lucide-react)", "ChevronRight (lucide-react)", "cn" ], "usedBy": [ "audit/components/audit-log-table.tsx", "audit/components/login-log-table.tsx", "audit/components/data-change-log-table.tsx" ] }, { "name": "EmptyTableRow", "file": "components/ui/empty-table-row.tsx", "props": "{ colSpan, message? }", "purpose": "表格空状态行(TableRow + TableCell 居中显示空状态文案),P0-3 重构从 audit-log-table、login-log-table、data-change-log-table 三处重复的空表格行抽取", "internalDeps": [ "TableRow", "TableCell" ], "usedBy": [ "audit/components/audit-log-table.tsx", "audit/components/login-log-table.tsx", "audit/components/data-change-log-table.tsx" ] }, { "name": "ResizablePanel", "file": "components/ui/resizable-panel.tsx", "props": "{ minLeft?: number, minRight?: number, initialLeft?: number, left: ReactNode, right: ReactNode, className?: string }", "purpose": "2026-06-24 新增:自实现可拖拽分栏容器(左右两栏 + 中间分隔条 pointer 拖拽调整宽度,无新依赖)。用于试卷富文本编辑器左编辑右预览、阅卷式批改左题目右扫描图等场景。pointerdown/move/up 事件驱动,document.body cursor=userSelect=none 防选中,clamp 到 minLeft/minRight 范围。", "internalDeps": [ "cn", "useState", "useRef", "useCallback", "useEffect" ], "usedBy": [ "exams/components/exam-rich-form.tsx", "homework/components/homework-scan-grading-view.tsx" ] }, { "name": "StatusBadge", "file": "components/ui/status-badge.tsx", "props": "{ status, variantMap, labelMap?, classNameMap?, className?, capitalize? }", "purpose": "通用状态徽章(Badge + 状态→variant/label/className 映射表),P1-1 重构从 audit StatusBadge/ActionBadge(2 处 100% 相同)、grades typeColors(2 处 100% 相同)、StudentHomeworkProgressStatus(3 处不一致映射)、QuestionType(switch+label)等 9+ 处重复抽取。同时修复 in_progress 在不同文件中颜色不一致的 bug", "internalDeps": [ "Badge", "cn" ], "usedBy": [ "audit/components/audit-log-table.tsx", "audit/components/login-log-table.tsx", "audit/components/data-change-log-table.tsx", "grades/components/grade-record-list.tsx", "grades/components/student-grade-summary.tsx", "app/(dashboard)/student/learning/assignments/page.tsx", "parent/components/child-homework-summary.tsx", "dashboard/components/student-dashboard/student-upcoming-assignments-card.tsx", "questions/components/question-columns.tsx" ] }, { "name": "TextField", "file": "components/form-fields/text-field.tsx", "props": "{ control, name, label, placeholder?, description?, type?, disabled?, itemClassName?, inputClassName?, inputProps?, toInputValue?, fromInputValue? }", "purpose": "通用文本字段(FormField + FormItem + FormLabel + FormControl + Input + FormMessage 包装),P1-2 重构从 profile-settings-form(6 处)、exam-basic-info-form(4 处)、ai-provider-settings-card(4 处)等 16 处重复的 Input FormField 抽取", "internalDeps": [ "FormField", "FormItem", "FormLabel", "FormControl", "FormDescription", "FormMessage", "Input" ], "usedBy": [ "settings/components/profile-settings-form.tsx", "settings/components/ai-provider-settings-card.tsx", "exams/components/exam-basic-info-form.tsx" ] }, { "name": "SelectField", "file": "components/form-fields/select-field.tsx", "props": "{ control, name, label, placeholder?, description?, options: SelectOption[], disabled?, itemClassName?, labelSlot?, toSelectValue?, fromSelectValue? }", "purpose": "通用选择字段(FormField + FormItem + FormLabel + Select + SelectContent + SelectItem + FormMessage 包装),P1-2 重构从 exam-basic-info-form(3 处)、ai-provider-settings-card(1 处)、create-question-dialog(2 处)、profile-settings-form(1 处)等 8 处重复的 Select FormField 抽取。支持 toSelectValue/fromSelectValue 转换器处理 number↔string", "internalDeps": [ "FormField", "FormItem", "FormLabel", "FormControl", "FormDescription", "FormMessage", "Select", "SelectContent", "SelectItem", "SelectTrigger", "SelectValue" ], "usedBy": [ "settings/components/profile-settings-form.tsx", "settings/components/ai-provider-settings-card.tsx", "exams/components/exam-basic-info-form.tsx", "questions/components/create-question-dialog.tsx" ] }, { "name": "TextareaField", "file": "components/form-fields/textarea-field.tsx", "props": "{ control, name, label, placeholder?, description?, disabled?, itemClassName?, textareaClassName?, textareaProps? }", "purpose": "通用多行文本字段(FormField + FormItem + FormLabel + FormControl + Textarea + FormMessage 包装),P1-2 重构从 exam-ai-generator、create-question-dialog 两处重复的 Textarea FormField 抽取", "internalDeps": [ "FormField", "FormItem", "FormLabel", "FormControl", "FormDescription", "FormMessage", "Textarea" ], "usedBy": [ "questions/components/create-question-dialog.tsx" ] }, { "name": "extractQuestionPreview", "file": "lib/question-content.ts", "signature": "extractQuestionPreview(content: unknown, fallback?: string, maxLength?: number): string", "purpose": "✅ 2026-06-24 新增:从 unknown 类型题目内容递归提取纯文本预览,支持字符串/数组/对象格式,可选截断。纯函数无副作用,便于单测。被 error-book、homework 等模块共享使用,避免各模块重复实现题目内容解析逻辑", "deps": [], "usedBy": [ "error-book/app/(dashboard)/student/error-book/add-error-book-dialog-with-questions.tsx" ] }, { "name": "isRecord", "file": "lib/type-guards.ts", "signature": "isRecord(v: unknown): v is Record", "purpose": "P1-6 代码去重新增:通用类型守卫,判断未知值是否为非空对象 Record。集中定义于 shared/lib/type-guards.ts,消除各模块(homework/stats-service、lesson-preparation/node-summary、lesson-preparation/ai-suggest 等)重复定义的 isRecord 局部实现,从 unknown 安全收窄类型,替代 as Record 断言", "deps": [], "usedBy": [ "homework/stats-service.getHomeworkAssignmentAnalytics", "lesson-preparation/lib/node-summary", "lesson-preparation/ai-suggest", "其他模块(12+ 个文件)" ] }, { "name": "ErrorState", "file": "components/error-state.tsx", "type": "component", "client": true, "props": "{ error: Error & { digest?: string }, reset: () => void, namespace: string }", "purpose": "✅ 2026-06-26 审计修复新增:通用错误边界展示组件(\"use client\"),统一 30+ error.tsx 渲染逻辑。基于 EmptyState + AlertTriangle 图标 + next-intl useTranslations 实现。翻译键查找规则:在传入 namespace 下查找 error.title / error.description / error.retry", "internalDeps": [ "EmptyState", "AlertTriangle (lucide-react)", "useTranslations (next-intl)" ], "usedBy": [ "app/(dashboard)/teacher/classes/my/error.tsx", "app/(dashboard)/teacher/classes/my/[id]/error.tsx", "app/(dashboard)/teacher/classes/students/error.tsx", "app/(dashboard)/teacher/classes/schedule/error.tsx", "app/(dashboard)/student/learning/error.tsx", "app/(dashboard)/student/learning/courses/error.tsx", "app/(dashboard)/student/learning/courses/[classId]/error.tsx", "app/(dashboard)/student/schedule/error.tsx" ] } ], "constants": [ { "name": "ROLE_PERMISSIONS", "file": "lib/permissions.ts", "type": "const", "description": "角色-权限映射表", "usedBy": [ "resolvePermissions", "auth.ts JWT callback" ] }, { "name": "ROLE_NAMES", "file": "types/permissions.ts", "type": "const", "description": "✅ 2026-06-24 新增:角色名常量对象(包含 ADMIN/TEACHER/STUDENT/PARENT/GRADE_HEAD/TEACHING_HEAD 键),替代 DB 查询中硬编码的角色名字符串", "usedBy": [ "error-book/data-access-analytics.getAllStudentIds" ] }, { "name": "Permissions", "file": "types/permissions.ts", "type": "const", "description": "38个权限点常量对象(含 FILE_UPLOAD/FILE_READ/FILE_DELETE)", "usedBy": [ "auth-guard", "use-permission", "所有actions" ] }, { "name": "db", "file": "db/index.ts", "type": "const", "description": "Drizzle ORM 实例", "usedBy": [ "所有业务模块" ] }, { "name": "questionTypeEnum", "file": "db/schema.ts", "type": "const", "description": "题目类型枚举", "usedBy": [ "questions", "exams" ] }, { "name": "classEnrollmentStatusEnum", "file": "db/schema.ts", "type": "const", "description": "选课状态枚举", "usedBy": [ "classes" ] } ], "files": [ { "path": "db/relations.ts", "description": "20+ 个 Drizzle relations 定义", "usedBy": [ "所有业务模块的关联查询" ] }, { "path": "next-auth.d.ts", "description": "NextAuth Session/JWT 类型扩展声明", "usedBy": [ "auth.ts", "useSession" ] }, { "path": "components/error-state.tsx", "lines": 35, "description": "✅ 2026-06-26 审计修复新增:ErrorState 通用错误边界展示组件(\"use client\")。统一 30+ error.tsx 渲染逻辑:AlertTriangle 图标 + i18n title/description + retry 按钮。基于 EmptyState + next-intl useTranslations。props: { error, reset, namespace }", "exports": [ "ErrorState" ], "usedBy": [ "classes 模块 error.tsx(4 个)", "student 模块 error.tsx(4 个)" ] }, { "path": "lib/api-response.ts", "lines": 212, "description": "✅ 2026-07-04 API 规范化重构新增:API 路由层统一响应工具。导出 apiSuccess/apiError/apiFromAction/handleApiError/withApiErrorHandler/parseJsonBody/errorToStatus。统一响应信封 { success, message?, errorCode?, data? } 与 ActionState 对齐。集中错误→HTTP 状态码映射(PermissionDeniedError→403 / NotFoundError→404 / ValidationError→400 / BusinessError→400 / 其他→500),消除字符串匹配推导。", "exports": [ "apiSuccess", "apiError", "apiFromAction", "handleApiError", "withApiErrorHandler", "parseJsonBody", "errorToStatus", "ApiResponse" ], "usedBy": [ "app/api/**/route.ts" ] }, { "path": "lib/sse.ts", "lines": 81, "description": "✅ 2026-07-04 API 规范化重构新增:Server-Sent Events 共享工具。导出 formatSseEvent/formatSseError/formatSseDone/createSseResponse/createSseError。统一 stream 路由的事件格式与响应头(Content-Type: text/event-stream + Cache-Control: no-cache, no-transform + Connection: keep-alive)。消除 ai/chat/stream 与 notifications/stream 重复实现。", "exports": [ "formatSseEvent", "formatSseError", "formatSseDone", "createSseResponse", "createSseError" ], "usedBy": [ "api/ai/chat/stream", "api/notifications/stream" ] } ], "types": [ { "name": "ActionState", "file": "types/action-state.ts", "definition": "ActionState = { success: boolean; message?: string; errors?: Record; data?: T }", "usedBy": [ "所有模块Server Action" ] }, { "name": "Permission", "file": "types/permissions.ts", "definition": "Permission = (typeof Permissions)[keyof typeof Permissions]", "usedBy": [ "auth-guard", "use-permission", "所有actions" ] }, { "name": "Role", "file": "types/permissions.ts", "definition": "Role = 'admin' | 'teacher' | 'student' | 'parent' | 'grade_head' | 'teaching_head'", "usedBy": [ "auth-guard", "permissions", "proxy", "next-auth.d.ts", "use-permission" ] }, { "name": "DataScope", "file": "types/permissions.ts", "definition": "DataScope = { type: 'all' } | { type: 'owned'; userId: string } | { type: 'class_members'; classIds: string[]; gradeIds?: string[] } | { type: 'grade_managed'; gradeIds: string[] } | { type: 'class_taught'; classIds: string[]; subjectIds?: string[] } | { type: 'children'; childrenIds: string[]; gradeIds?: string[] }", "usedBy": [ "auth-guard", "exams/data-access", "homework/data-access", "dashboard/data-access", "grades/data-access", "lesson-preparation/data-access", "parent/children/[studentId]/page.tsx" ] }, { "name": "AuthContext", "file": "types/permissions.ts", "definition": "AuthContext = { userId: string; roles: Role[]; permissions: Permission[]; dataScope: DataScope }", "usedBy": [ "auth-guard", "所有调用requirePermission的Server Action" ] }, { "name": "PermissionDeniedError", "file": "lib/auth-guard.ts", "definition": "class PermissionDeniedError extends Error { constructor(permission: string) }", "usedBy": [ "所有Server Action的try/catch" ] } ] }, "dbTables": { "users": { "fields": [ "id", "name", "email", "emailVerified", "image", "password", "phone", "address", "gender", "age", "birthDate", "guardianName", "guardianPhone", "guardianRelation", "consentAcceptedAt", "gradeId", "departmentId", "onboardedAt", "createdAt", "updatedAt" ], "usedBy": [ "auth", "users", "dashboard", "classes" ] }, "accounts": { "fields": [ "userId", "type", "provider", "providerAccountId", "refresh_token", "access_token", "expires_at", "token_type", "scope", "id_token", "session_state" ], "usedBy": [ "auth" ] }, "sessions": { "fields": [ "sessionToken", "userId", "expires" ], "usedBy": [ "auth" ] }, "verificationTokens": { "fields": [ "identifier", "token", "expires" ], "usedBy": [ "auth" ] }, "roles": { "fields": [ "id", "name", "description", "createdAt", "updatedAt" ], "usedBy": [ "auth", "auth-guard" ] }, "usersToRoles": { "fields": [ "userId", "roleId" ], "usedBy": [ "auth", "auth-guard" ] }, "rolePermissions": { "fields": [ "roleId", "permission" ], "usedBy": [ "auth (seed)" ] }, "knowledgePoints": { "fields": [ "id", "name", "description", "anchorText", "parentId", "chapterId", "level", "order", "createdAt", "updatedAt" ], "usedBy": [ "textbooks", "questions" ] }, "knowledgePointPrerequisites": { "fields": [ "knowledgePointId", "prerequisiteKpId", "createdAt" ], "usedBy": [ "textbooks" ], "description": "知识点前置依赖(知识图谱):复合主键 knowledgePointId+prerequisiteKpId,双外键 cascade 删除,无独立 id 字段" }, "questions": { "fields": [ "id", "content", "type", "difficulty", "authorId", "parentId", "createdAt", "updatedAt" ], "usedBy": [ "questions", "exams", "homework" ] }, "questionsToKnowledgePoints": { "fields": [ "questionId", "knowledgePointId" ], "usedBy": [ "questions" ] }, "subjects": { "fields": [ "id", "name", "code", "order", "createdAt", "updatedAt" ], "usedBy": [ "exams", "textbooks" ] }, "textbooks": { "fields": [ "id", "title", "subject", "grade", "publisher", "createdAt", "updatedAt" ], "usedBy": [ "textbooks" ] }, "chapters": { "fields": [ "id", "textbookId", "title", "order", "parentId", "content", "createdAt", "updatedAt" ], "usedBy": [ "textbooks" ] }, "departments": { "fields": [ "id", "name", "description", "createdAt", "updatedAt" ], "usedBy": [ "school" ] }, "classrooms": { "fields": [ "id", "name", "building", "floor", "capacity", "createdAt", "updatedAt" ], "usedBy": [ "school" ] }, "academicYears": { "fields": [ "id", "name", "startDate", "endDate", "isActive", "createdAt", "updatedAt" ], "usedBy": [ "school" ] }, "schools": { "fields": [ "id", "name", "code", "createdAt", "updatedAt" ], "usedBy": [ "school", "classes" ] }, "grades": { "fields": [ "id", "schoolId", "name", "order", "gradeHeadId", "teachingHeadId", "createdAt", "updatedAt" ], "usedBy": [ "school", "classes", "exams", "auth-guard" ] }, "classes": { "fields": [ "id", "schoolId", "gradeId", "teacherId", "name", "homeroom", "room", "invitationCode", "schoolName", "grade", "createdAt", "updatedAt" ], "usedBy": [ "classes", "homework", "auth-guard" ] }, "classSubjectTeachers": { "fields": [ "classId", "teacherId", "subjectId", "createdAt", "updatedAt" ], "usedBy": [ "classes", "auth-guard" ] }, "classEnrollments": { "fields": [ "classId", "studentId", "status", "createdAt" ], "usedBy": [ "classes", "homework" ] }, "classSchedule": { "fields": [ "id", "classId", "weekday", "startTime", "endTime", "course", "location", "createdAt", "updatedAt" ], "usedBy": [ "classes" ] }, "exams": { "fields": [ "id", "creatorId", "title", "description", "subjectId", "gradeId", "status", "structure", "startTime", "endTime", "createdAt", "updatedAt" ], "usedBy": [ "exams", "homework" ] }, "examQuestions": { "fields": [ "examId", "questionId", "score", "order" ], "usedBy": [ "exams" ] }, "examSubmissions": { "fields": [ "id", "examId", "studentId", "score", "submittedAt", "status", "createdAt", "updatedAt" ], "usedBy": [ "exams" ] }, "submissionAnswers": { "fields": [ "id", "submissionId", "questionId", "answerContent", "score", "feedback", "createdAt", "updatedAt" ], "usedBy": [ "exams" ] }, "homeworkAssignments": { "fields": [ "id", "creatorId", "sourceExamId", "title", "description", "status", "availableAt", "dueAt", "allowLate", "lateDueAt", "maxAttempts", "structure", "createdAt", "updatedAt" ], "usedBy": [ "homework" ] }, "homeworkAssignmentQuestions": { "fields": [ "assignmentId", "questionId", "score", "order" ], "usedBy": [ "homework" ] }, "homeworkAssignmentTargets": { "fields": [ "assignmentId", "studentId", "createdAt" ], "usedBy": [ "homework" ] }, "homeworkSubmissions": { "fields": [ "id", "assignmentId", "studentId", "status", "attemptNo", "score", "submittedAt", "startedAt", "isLate", "createdAt", "updatedAt" ], "usedBy": [ "homework" ] }, "homeworkAnswers": { "fields": [ "id", "submissionId", "questionId", "answerContent", "score", "feedback", "createdAt", "updatedAt" ], "usedBy": [ "homework" ] }, "aiProviders": { "fields": [ "id", "provider", "baseUrl", "model", "apiKeyEncrypted", "apiKeyLast4", "isDefault", "createdBy", "updatedBy", "createdAt", "updatedAt" ], "usedBy": [ "settings", "ai" ] }, "auditLogs": { "fields": [ "id", "userId", "userName", "action", "module", "targetId", "targetType", "detail", "ipAddress", "userAgent", "status", "createdAt" ], "usedBy": [ "audit", "shared/lib/audit-logger" ] }, "loginLogs": { "fields": [ "id", "userId", "userEmail", "action", "status", "ipAddress", "userAgent", "errorMessage", "createdAt" ], "usedBy": [ "audit", "shared/lib/login-logger", "auth" ] }, "dataChangeLogs": { "fields": [ "id", "tableName", "recordId", "action", "oldValue", "newValue", "changedBy", "changedByName", "ipAddress", "createdAt" ], "usedBy": [ "audit", "shared/lib/change-logger" ] }, "announcements": { "fields": [ "id", "title", "content", "type", "status", "targetGradeId", "targetClassId", "authorId", "publishedAt", "createdAt", "updatedAt" ], "usedBy": [ "announcements" ] }, "fileAttachments": { "fields": [ "id", "filename", "originalName", "mimeType", "size", "storagePath", "url", "uploaderId", "targetType", "targetId", "createdAt" ], "usedBy": [ "files", "homework" ] }, "gradeRecords": { "fields": [ "id", "studentId", "classId", "subjectId", "examId", "academicYearId", "title", "score", "fullScore", "type", "semester", "recordedBy", "remark", "createdAt", "updatedAt" ], "usedBy": [ "grades" ] }, "coursePlans": { "fields": [ "id", "classId", "subjectId", "teacherId", "academicYearId", "semester", "totalHours", "completedHours", "weeklyHours", "startDate", "endDate", "syllabus", "objectives", "status", "createdBy", "createdAt", "updatedAt" ], "usedBy": [ "course-plans" ] }, "coursePlanItems": { "fields": [ "id", "planId", "week", "topic", "content", "hours", "textbookChapter", "notes", "isCompleted", "completedAt", "createdAt", "updatedAt" ], "usedBy": [ "course-plans" ] }, "parentStudentRelations": { "fields": [ "id", "parentId", "studentId", "relation", "createdAt" ], "usedBy": [ "parent", "auth-guard" ] }, "messages": { "fields": [ "id", "senderId", "receiverId", "subject", "content", "isRead", "readAt", "parentMessageId", "senderDeletedAt", "receiverDeletedAt", "createdAt" ], "usedBy": [ "messaging" ] }, "messageNotifications": { "fields": [ "id", "userId", "type", "title", "content", "link", "isRead", "priority", "isArchived", "createdAt" ], "usedBy": [ "notifications", "messaging (via re-export)" ] }, "notificationPreferences": { "fields": [ "id", "userId", "emailEnabled", "smsEnabled", "pushEnabled", "homeworkNotifications", "gradeNotifications", "announcementNotifications", "messageNotifications", "attendanceNotifications", "createdAt", "updatedAt" ], "usedBy": [ "notifications", "messaging (via re-export)", "settings" ] }, "attendanceRecords": { "fields": [ "id", "studentId", "classId", "scheduleId", "date", "period (L-6 节次考勤:enum('morning_reading','morning','afternoon','evening','full_day') nullable,向后兼容 NULL 视为 full_day)", "status", "remark", "recordedBy", "createdAt", "updatedAt" ], "indexes": [ "classDatePeriodIdx (L-6: class_id + date + period 复合索引)" ], "usedBy": [ "attendance", "leave-requests (via attendance/data-access.syncExcusedFromLeaveRequest)" ] }, "attendanceRules": { "fields": [ "id", "classId", "lateThresholdMinutes", "earlyLeaveThresholdMinutes", "enableAutoMark", "createdAt", "updatedAt" ], "usedBy": [ "attendance" ] }, "schedulingRules": { "fields": [ "id", "classId", "maxDailyHours", "maxContinuousHours", "lunchBreakStart", "lunchBreakEnd", "morningStart", "afternoonEnd", "avoidBackToBack", "balancedSubjects", "createdAt", "updatedAt" ], "usedBy": [ "scheduling" ] }, "scheduleChanges": { "fields": [ "id", "originalScheduleId", "classId", "originalTeacherId", "substituteTeacherId", "originalDate", "newDate", "newStartTime", "newEndTime", "reason", "status", "requestedBy", "approvedBy", "createdAt", "updatedAt" ], "usedBy": [ "scheduling" ] }, "passwordSecurity": { "fields": [ "id", "userId", "failedLoginAttempts", "lockedUntil", "passwordChangedAt", "mustChangePassword", "lastPasswordChange", "createdAt", "updatedAt" ], "usedBy": [ "auth", "settings" ] }, "knowledgePointMastery": { "fields": [ "id", "studentId", "knowledgePointId", "masteryLevel", "totalQuestions", "correctQuestions", "lastAssessedAt", "createdAt", "updatedAt" ], "usedBy": [ "diagnostic" ], "description": "知识点掌握度记录(复合主键 studentId+knowledgePointId,onDuplicateKeyUpdate upsert)" }, "learningDiagnosticReports": { "fields": [ "id", "studentId", "generatedBy", "classId", "reportType", "period", "summary", "strengths", "weaknesses", "recommendations", "overallScore", "status", "createdAt", "updatedAt" ], "indexes": [ "diagnostic_student_idx (studentId)", "diagnostic_generated_by_idx (generatedBy)", "diagnostic_status_idx (status)", "diagnostic_report_type_idx (reportType)", "diagnostic_class_idx (classId)" ], "usedBy": [ "diagnostic" ], "description": "学情诊断报告(reportType: individual/class/grade;status: draft/published/archived;v4-P1-4 新增 classId 字段 varchar(128) references classes.id onDelete:set null,用于班级报告关联班级,发布时批量通知全班学生)" }, "electiveCourses": { "fields": [ "id", "name", "subjectId", "teacherId", "gradeId", "description", "capacity", "enrolledCount", "classroom", "schedule", "startDate", "endDate", "selectionStartAt", "selectionEndAt", "dropDeadline", "status", "selectionMode", "credit", "createdAt", "updatedAt" ], "usedBy": [ "elective" ], "description": "选修课程(status: draft/open/closed/cancelled;selectionMode: fcfs/lottery;P2-4 新增 dropDeadline: datetime 退课截止时间,可空)" }, "courseSelections": { "fields": [ "id", "courseId", "studentId", "status", "priority", "selectedAt", "enrolledAt", "droppedAt", "lotteryRank", "dropReason", "createdAt", "updatedAt" ], "usedBy": [ "elective" ], "description": "选课记录(复合主键 courseId+studentId;status: selected/enrolled/waitlist/dropped/rejected;P2-4 新增 dropReason: varchar(255) 退课理由,可空)" } }, "designTokens": { "name": "design-tokens", "type": "css-tokens", "path": "src/app/styles/tokens/", "files": [ "primitive.css", "semantic-light.css", "semantic-dark.css", "lesson-preparation.css", "tailwind-theme.css", "index.css" ], "layers": [ { "name": "primitive", "description": "原始色板/字号/间距/阴影/字体家族,业务代码不直接引用", "tokens": [ "--color-zinc-50 ~ 950", "--color-stone-50 ~ 950", "--color-indigo-500/600", "--font-size-1 ~ 9", "--space-0 ~ 18", "--shadow-1 ~ 6", "--font-family-sans/serif/mono", "--leading-tight/snug/normal/relaxed/loose", "--weight-regular/medium/semibold/bold", "--duration-fast/normal/slow", "--ease-in/out/in-out", "--z-dropdown/sticky/modal/popover/toast" ] }, { "name": "semantic", "description": "语义令牌,业务代码唯一引用入口,明暗双份", "tokens": [ "--background/foreground/card/popover/primary/secondary/muted/accent/destructive/border/input/ring/radius", "--background-elevated/sunken", "--text-primary/secondary/tertiary", "--border-strong/subtle", "--chart-1 ~ 5", "--sidebar-*", "--diff-add/add-bg/remove/remove-bg", "--graph-node-1 ~ 6" ] }, { "name": "lesson-preparation", "description": "--lp-* 命名空间令牌,明暗双份", "tokens": [ "--lp-paper/paper-edge/paper-shadow/paper-shadow-active", "--lp-anchor-range/range-active/point", "--lp-inline-node-border/text/meta", "--lp-interaction", "--lp-dot-objective/key-point/import/new-teaching/consolidation/summary/homework/blackboard/text-study/exercise/rich-text/reflection/interaction/textbook/default" ] } ], "tailwindExposure": "tailwind-theme.css @theme inline", "constraints": { "eslint": [ "no-restricted-syntax: 禁止 #hex 字面量", "design-tokens/no-hardcoded-fonts: 禁止 Inter/Fraunces/JetBrains Mono 字面量" ], "whitelist": [ "src/app/styles/tokens/primitive.css", "src/modules/notifications/channels/email-channel.ts", "src/app/manifest.ts" ] }, "addedAt": "2026-07-04", "description": "设计令牌专项重构(2026-07-04):Primitive + Semantic 双层架构,HEX→HSL,明暗双份,@theme inline 暴露为 Tailwind 类。详见 docs/architecture/004_architecture_impact_map.md 1.1.2 章节" } }, "auth": { "path": "src/auth.ts", "description": "用户认证:NextAuth配置(handlers/auth/signIn/signOut)、JWT/Session callbacks、events回调(登录日志)。集成密码安全策略(账户锁定、失败登录追踪)和登录速率限制。P1-3 拆分后,辅助函数已迁移至 shared/lib/{role-utils,bcrypt-utils,http-utils,password-security-service}。audit-P0 重构(2026-06-25):authorize 中的 2FA 校验改用同模块 modules/auth/services/two-factor-service(不再跨模块依赖 modules/settings/actions-security);jwt/session/signOut 中 3 处 `as` 断言通过 next-auth.d.ts 扩展 User 接口 + `\"token\" in message` 类型收窄消除", "imports": [ "shared/lib/permissions", "shared/lib/login-logger", "shared/lib/password-policy", "shared/lib/rate-limit", "shared/lib/role-utils", "shared/lib/bcrypt-utils", "shared/lib/http-utils", "shared/lib/password-security-service", "shared/db", "shared/db/schema", "modules/auth/services/two-factor-service (dynamic import)" ], "exports": { "functions": [ { "name": "auth", "signature": "auth(): Promise", "purpose": "获取当前用户Session", "usedBy": [ "shared/lib/session.ts (dynamic import,再被 auth-guard/audit-logger/change-logger 间接使用)", "所有Server Component页面", "app/api 路由" ] }, { "name": "handlers", "signature": "{ GET, POST }", "purpose": "NextAuth Route Handler", "usedBy": [ "app/api/auth/[...nextauth]/route.ts" ] }, { "name": "signIn", "signature": "signIn(provider, options?)", "purpose": "登录", "usedBy": [ "login-form.tsx" ] }, { "name": "signOut", "signature": "signOut(options?)", "purpose": "登出", "usedBy": [ "site-header.tsx" ] } ], "events": [ { "name": "signIn", "signature": "async signIn({ user }) => void", "purpose": "用户登录成功后记录登录日志", "deps": [ "shared/lib/login-logger.logLoginEvent" ], "params": "{ userId: user.id, userEmail: user.email, action: 'signin', status: 'success' }" }, { "name": "signOut", "signature": "async signOut(message) => void", "purpose": "用户登出后记录登录日志(处理NextAuth v5不同message形状)", "deps": [ "shared/lib/login-logger.logLoginEvent" ], "params": "{ userId?, userEmail, action: 'signout', status: 'success' }" } ] }, "middleware": { "file": "src/proxy.ts", "signature": "proxy(request: NextRequest): Promise", "purpose": "基于权限点的路由守卫(Next.js 16 将 middleware 重命名为 proxy)。audit-P1-7:使用 hasPermissionInBitmap() 直接检查 token.permissionsBitmap 位图,不再解码完整权限数组(边缘运行时每请求都经过,性能优化)。audit-P1-10 + audit-P1-12:4 个权限映射常量抽取至 shared/lib/route-permissions.ts 配置文件(配置驱动设计),新增路由权限只需修改配置文件无需改 proxy.ts 逻辑;SPECIFIC_ROUTE_PERMISSIONS 扩展至 15 个条目覆盖 /admin/* 子路径细粒度权限", "permissionConfigSource": "shared/lib/route-permissions.ts (audit-P1-12 抽取)", "specificRoutePermissions": { "/admin/ai-settings": "ai:chat", "/admin/roles": "role:read", "/admin/permissions": "permission:read", "/admin/announcements": "announcement:manage", "/admin/audit-logs": "audit_log:read", "/admin/audit-logs/overview": "audit_log:read", "/admin/audit-logs/login-logs": "audit_log:read", "/admin/audit-logs/data-changes": "audit_log:read", "/admin/elective": "elective:manage", "/admin/questions": "question:read", "/admin/users": "user:manage", "/admin/error-book": "error_book:analytics_read", "/admin/lesson-plans": "lesson_plan:read", "/admin/course-plans": "course_plan:read" }, "routePrefixPermissions": { "/admin": "school:manage", "/teacher": "exam:create", "/student": "homework:submit", "/parent": "dashboard:parent:read", "/management": "grade:manage" }, "dashboardRoutePermissions": { "/admin/dashboard": "dashboard:admin:read", "/teacher/dashboard": "dashboard:teacher:read", "/student/dashboard": "dashboard:student:read", "/parent/dashboard": "dashboard:parent:read" }, "apiRoutePermissions": { "/api/ai/chat": "ai:chat" }, "securityNote": "P0 修复:原先 /teacher 和 /parent 都使用 EXAM_READ,但 student/parent 也有 EXAM_READ,导致跨角色访问漏洞。改为使用各角色独有的权限点。audit-P0-6 修复:内联的 resolveDefaultPath 函数提取至 shared/lib/route-resolver.ts(纯函数,无 server-only,可在 edge/proxy 运行时使用),与 onboarding/actions.ts 共用,消除重复实现。audit-P1-10 修复:SPECIFIC_ROUTE_PERMISSIONS 从 3 个扩展至 15 个条目,覆盖 /admin/announcements 等子路径细粒度权限,使非管理员角色可访问对应权限的 /admin/* 页面。audit-P1-12 修复:4 个权限映射常量从 proxy.ts 内联抽取至 shared/lib/route-permissions.ts 配置文件(配置驱动设计),重命名 ROUTE_PERMISSIONS→ROUTE_PREFIX_PERMISSIONS、API_PERMISSIONS→API_ROUTE_PERMISSIONS,value 类型从 string 收紧为 Permission 消除 as 断言" } }, "modules/auth": { "path": "src/modules/auth", "description": "认证模块:注册 Server Action + 注册 data-access + 登录 2FA 服务编排 + 认证页面 UI。audit-P0 重构(2026-06-25)从纯 UI 模块升级为完整三层结构", "exports": { "actions": [ { "name": "registerAction", "file": "actions.ts", "permission": "(public, 注册前无需权限)", "signature": "(formData: FormData) => Promise>", "purpose": "注册 Server Action:Zod 校验 → 独立 rate limit key register:{ip}(audit-P1-6 后 await rateLimit()) → 邮箱可用性检查 → checkBreachedPassword(audit-P2-4 新增:HIBP k-anonymity API 检测已泄露密码,fail-open 策略)→ createUser(bcrypt cost=12,不自动创建角色)→ 审计日志 + trackAuthEvent('auth.signup')(audit-P1-9) → 结构化错误码 REGISTER_ERROR_CODES(audit-P2-4 新增 BREACHED_PASSWORD)+ messageForError()(audit-P1-1 已完成:替换硬编码中文映射为 next-intl getTranslations('auth.register'),按 RegisterErrorCode 查表 errors.${code})", "deps": [ "shared/lib/rate-limit.rateLimit", "shared/lib/rate-limit.rateLimitKey", "shared/lib/rate-limit.RATE_LIMIT_RULES", "shared/lib/login-logger.logLoginEvent", "shared/lib/track-event.trackAuthEvent", "shared/lib/breached-password.checkBreachedPassword", "data-access.createUser", "data-access.isEmailAvailable", "data-access.buildRegisterInput", "schema.RegisterSchema" ], "usedBy": [ "app/(auth)/register/page.tsx" ] }, { "name": "checkEmailAvailabilityAction", "file": "actions.ts", "permission": "(public)", "signature": "(email: string) => Promise>", "purpose": "邮箱可用性检查 Server Action(带 rate limit 防爆破)", "deps": [ "shared/lib/rate-limit", "data-access.isEmailAvailable" ], "usedBy": [ "components/register-form.tsx" ] } ], "dataAccess": [ { "name": "createUser", "file": "data-access.ts", "signature": "(input: RegisterInput) => Promise<{ id: string; email: string }>", "purpose": "创建用户(bcrypt cost=12 哈希密码,不自动创建角色,角色由管理员预分配)" }, { "name": "isEmailAvailable", "file": "data-access.ts", "signature": "(email: string) => Promise", "purpose": "邮箱可用性检查" }, { "name": "buildRegisterInput", "file": "data-access.ts", "signature": "(formData: FormData) => RegisterInput", "purpose": "从 FormData 构造 RegisterInput(含邮箱小写化、密码 trim)" } ], "services": [ { "name": "verifyTwoFactorForLogin", "file": "services/two-factor-service.ts", "signature": "(params: { userId: string; token?: string }) => Promise", "purpose": "登录时 2FA 校验编排:检查用户是否启用 2FA → 校验 TOTP 码 → 备份码(消耗)。✅ audit-P0-2 新增:从 modules/settings/actions-security 迁移至同模块 services/,auth.ts 不再跨模块依赖 settings/actions-security", "deps": [ "modules/settings/data-access-two-factor.getTwoFactorEnabled", "modules/settings/data-access-two-factor.getTotpSecret", "modules/settings/data-access-two-factor.getBackupCodesHashed", "modules/settings/data-access-two-factor.setBackupCodesHashed", "modules/settings/lib/totp.verifyTotpCode", "modules/settings/lib/totp.verifyBackupCode", "modules/settings/lib/totp.consumeBackupCode" ], "usedBy": [ "src/auth.ts (authorize, dynamic import)" ] }, { "name": "preflightTwoFactorByEmail", "file": "services/two-factor-service.ts", "signature": "(email: string) => Promise<{ required: boolean }>", "purpose": "登录前预检:检查用户是否启用 2FA(用于前端提示输入 2FA 码)", "deps": [ "shared/db", "shared/db/schema.users", "modules/settings/data-access-two-factor.getTwoFactorEnabled" ], "usedBy": [ "components/login-form.tsx" ] }, { "name": "authenticateUser", "file": "services/login-service.ts", "signature": "(credentials: AuthenticateUserInput) => Promise", "purpose": "audit-P1-3 新增:登录认证服务,从 auth.ts authorize 回调提取 7 类职责(输入解析/速率限制/用户查询/账户锁定/密码校验/2FA/角色加载),失败路径统一 logLoginEvent,返回 null;成功路径重置失败计数与速率限制(audit-P1-6 后 rateLimit()/resetRateLimit() 改为 await,因 rate-limit 抽象为 Promise 接口)。audit-P1-7 后返回的 roles 由 auth.ts JWT callback 调用 resolvePermissions + encodePermissionsBitmap 编码为位图", "deps": [ "shared/db", "shared/db/schema (users/roles/usersToRoles/passwordSecurity)", "shared/lib/login-logger.logLoginEvent", "shared/lib/password-policy (PASSWORD_RULES/isAccountLocked)", "shared/lib/rate-limit (RATE_LIMIT_RULES/rateLimit/rateLimitKey/resetRateLimit)", "shared/lib/bcrypt-utils.normalizeBcryptHash", "shared/lib/http-utils.resolveClientIp", "shared/lib/password-security-service (getOrCreatePasswordSecurity/recordFailedLogin/resetFailedLogin)", "shared/lib/role-utils.resolvePrimaryRole", "modules/auth/services/two-factor-service.verifyTwoFactorForLogin" ], "usedBy": [ "src/auth.ts (authorize, dynamic import)" ] } ], "schema": [ { "name": "RegisterSchema", "file": "schema.ts", "purpose": "Zod 注册校验:email/password/name/birthDate/phone/agreedTerms/agreedGuardian + 未成年人 guardian 字段条件校验(superRefine + calcAge + isMinorByBirthDate 纯函数)" } ], "types": [ { "name": "RegisterResult / RegisterInput / EmailAvailabilityResult / RegisterErrorCode", "file": "types.ts", "purpose": "类型定义(纯类型,无 server-only,可被客户端组件导入)" }, { "name": "AuthenticateUserInput / AuthenticatedUser", "file": "services/login-service.ts", "purpose": "audit-P1-3 新增:authenticateUser 函数的输入类型(email/password/totpCode 均为 unknown)与返回类型(id/name/email/role/roles,与 next-auth User 接口兼容)" } ], "components": [ { "name": "AuthLayout", "file": "components/auth-layout.tsx", "purpose": "认证页面布局(audit-P2-6 增强:接受 brand?: BrandConfig prop,由 app/(auth)/layout.tsx Server Component 调用 getBrandConfig() 注入;渲染 schoolName / logoUrl(img 或 GraduationCap 图标 fallback)/ testimonialQuote / testimonialAuthor;未配置时使用 DEFAULT_BRAND_CONFIG 默认值)", "deps": [ "settings/brand-config.DEFAULT_BRAND_CONFIG", "settings/brand-config.BrandConfig" ], "usedBy": [ "app/(auth)/layout.tsx" ] }, { "name": "LoginForm", "file": "components/login-form.tsx", "purpose": "登录表单(含 2FA 输入与预检)" }, { "name": "RegisterForm", "file": "components/register-form.tsx", "purpose": "注册表单(registerAction 通过 prop 注入;含 agreedTerms/agreedGuardian hidden input 确保 Zod 收到 boolean)" } ] } }, "exams": { "path": "src/modules/exams", "description": "考试全生命周期:创建(手动/AI)、编辑、预览、发布、删除、复制", "exports": { "actions": [ { "name": "createExamAction", "permission": "EXAM_CREATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "手动模式创建考试草稿", "deps": [ "requirePermission", "shared/db", "data-access.persistExamDraft" ], "usedBy": [ "exam-form.tsx" ] }, { "name": "createAiExamAction", "permission": "EXAM_AI_GENERATE", "signature": "同上", "purpose": "AI模式创建考试", "deps": [ "requirePermission", "ai-pipeline.generateAiCreateDraftFromSource", "data-access.persistAiGeneratedExamDraft" ], "usedBy": [ "exam-form.tsx" ] }, { "name": "previewAiExamAction", "permission": "EXAM_AI_GENERATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "AI预览试卷(不持久化)", "deps": [ "requirePermission", "ai-pipeline.generateAiPreviewData" ], "usedBy": [ "exam-ai-generator.tsx" ] }, { "name": "regenerateAiQuestionAction", "permission": "EXAM_AI_GENERATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "AI重写单个题目", "deps": [ "requirePermission", "ai-pipeline.regenerateAiQuestionByInstruction" ], "usedBy": [ "exam-preview-question-editor.tsx" ] }, { "name": "updateExamAction", "permission": "EXAM_UPDATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新考试(含资源归属校验)", "deps": [ "requirePermission", "data-access.getExamCreatorId", "data-access.updateExamWithQuestions" ], "usedBy": [ "exam-form.tsx" ] }, { "name": "deleteExamAction", "permission": "EXAM_DELETE", "signature": "同上", "purpose": "删除考试(含资源归属校验)", "deps": [ "requirePermission", "data-access.getExamCreatorId", "data-access.deleteExamById" ], "usedBy": [ "exam-actions.tsx" ] }, { "name": "duplicateExamAction", "permission": "EXAM_DUPLICATE", "signature": "同上", "purpose": "复制考试", "deps": [ "requirePermission", "data-access.getExamCreatorId", "data-access.duplicateExam" ], "usedBy": [ "exam-actions.tsx" ] }, { "name": "getExamPreviewAction", "permission": "EXAM_READ", "signature": "(examId: string) => Promise }>>", "purpose": "获取考试预览数据", "deps": [ "requirePermission", "data-access.getExamPreview" ], "usedBy": [ "exam-viewer.tsx" ] }, { "name": "getSubjectsAction", "permission": "EXAM_READ", "signature": "() => Promise>", "purpose": "获取科目列表", "deps": [ "requirePermission", "data-access.getExamSubjects" ], "usedBy": [ "exam-form.tsx" ] }, { "name": "getGradesAction", "permission": "EXAM_READ", "signature": "同上", "purpose": "获取年级列表", "deps": [ "requirePermission", "data-access.getExamGrades" ], "usedBy": [ "exam-form.tsx" ] }, { "name": "getExamsByGradeIdAction", "permission": "EXAM_READ", "signature": "(gradeId: string) => Promise>", "purpose": "v4-P2-7 新增:年级仪表盘维度3,获取年级下所有考试 + 提交统计", "deps": [ "requirePermission", "data-access.getExamsByGradeId" ], "usedBy": [ "management/grade/dashboard" ] }, { "name": "autoMarkExamAction", "file": "ai-pipeline/auto-mark.ts", "permission": "EXAM_AI_GENERATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "2026-06-24 新增:AI 自动标记试卷文本。将粘贴的试卷文本交给 AI 解析为带题目块/分组/填空/加点字标记的 Tiptap JSONContent 文档,教师可在富文本编辑器中基于此结果继续微调。返回 { doc, title } 供编辑器载入。2026-06-24 拆分:从 actions.ts 迁移至 ai-pipeline/auto-mark.ts,actions.ts re-export 保持向后兼容。", "deps": [ "requirePermission", "shared/lib/ai.createAiChatCompletion", "shared/lib/action-utils.handleActionError", "exams/ai-pipeline/parse.parseAiResponse", "exams/ai-pipeline/auto-mark.buildTiptapDocFromAiResponse", "exams/ai-pipeline/auto-mark.extractTitleFromAiResponse", "exams/ai-pipeline/auto-mark.AutoMarkSchema" ], "usedBy": [ "exams/components/exam-rich-form.tsx" ] }, { "name": "createExamFromRichEditorAction", "file": "actions-rich-editor.ts", "permission": "EXAM_CREATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "2026-06-24 新增:从富文本编辑器保存试卷草稿。将 Tiptap JSONContent 通过 editor/editor-to-structure.editorDocToStructure 转换为 questions + structure 后复用 persistAiGeneratedExamDraft 持久化。revalidatePath('/teacher/exams/all')。2026-06-24 拆分:从 actions.ts 迁移至 actions-rich-editor.ts,actions.ts re-export 保持向后兼容。", "deps": [ "requirePermission", "shared/lib/action-utils.handleActionError", "shared/lib/action-utils.safeJsonParse", "exams/editor/editor-to-structure.editorDocToStructure", "exams/data-access.persistAiGeneratedExamDraft", "exams/actions-helpers.prepareExamCreateContext", "exams/actions-helpers.parseExamModeConfig", "exams/actions-rich-editor.RichExamCreateSchema", "exams/actions-rich-editor.convertStructureNode", "revalidatePath" ], "usedBy": [ "exams/components/exam-rich-form.tsx" ] }, { "name": "updateExamFromRichEditorAction", "file": "actions-rich-editor.ts", "permission": "EXAM_UPDATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "2026-06-24 新增:从富文本编辑器更新已有试卷。将 Tiptap JSONContent 转换为 structure + orderedQuestions 后调用 data-access.updateExamWithRichEditorData 持久化。✅ P0-4 修复(2026-06-24):原实现直接 db.transaction 写 exams/examQuestions 表,现已下沉到 data-access.updateExamWithRichEditorData。revalidatePath('/teacher/exams/all')。2026-06-24 拆分:从 actions.ts 迁移至 actions-rich-editor.ts,actions.ts re-export 保持向后兼容。", "deps": [ "requirePermission", "shared/lib/action-utils.handleActionError", "shared/lib/action-utils.safeJsonParse", "exams/editor/editor-to-structure.editorDocToStructure", "exams/data-access.updateExamWithRichEditorData", "exams/actions-rich-editor.RichExamUpdateSchema", "exams/actions-rich-editor.convertStructureNode", "revalidatePath" ], "usedBy": [ "exams/components/exam-rich-form.tsx" ] } ], "dataAccess": [ { "name": "getExams", "signature": "getExams(params: GetExamsParams & { scope: DataScope }): Promise", "purpose": "查询考试列表(含数据权限过滤)", "usedBy": [ "teacher/exams/all/page.tsx", "homework创建页面" ] }, { "name": "getExamById", "signature": "getExamById(id: string, scope?: DataScope): Promise", "purpose": "按ID获取考试详情", "usedBy": [ "exam详情/编辑页面" ] }, { "name": "persistExamDraft", "signature": "persistExamDraft(input: { examId, title, creatorId, subjectId, gradeId, scheduledAt?, description }): Promise", "purpose": "持久化手动考试草稿", "usedBy": [ "createExamAction" ] }, { "name": "addExamQuestions", "signature": "addExamQuestions(examId: string, items: Array<{ questionId: string; score: number; order: number }>): Promise", "file": "data-access-cross-module.ts", "purpose": "批量插入考试-题目关联(跨模块写接口,供 lesson-preparation/publish-service 调用,避免直查 examQuestions 表)", "usedBy": [ "lesson-preparation/publish-service.publishLessonPlanHomework" ], "reExportedFrom": "data-access.ts (向后兼容)" }, { "name": "persistAiGeneratedExamDraft", "signature": "persistAiGeneratedExamDraft(input: { examId, title, creatorId, subjectId, gradeId, scheduledAt?, description, structure, generated }): Promise", "purpose": "持久化AI生成考试草稿(P0-1 已修复:通过 questions/data-access.createQuestionWithRelations 创建题目,不再直查 questions 表)", "usedBy": [ "createAiExamAction" ] }, { "name": "getExamsDashboardStats", "signature": "(scope?: DataScope) => Promise", "file": "data-access-cross-module.ts", "purpose": "获取考试仪表盘统计数据(考试总数,支持数据范围过滤)", "usedBy": [ "dashboard/data-access.getAdminDashboardData" ], "reExportedFrom": "data-access.ts (向后兼容)" }, { "name": "omitScheduledAtFromDescription", "signature": "(description: string | null) => string", "purpose": "从描述中移除scheduledAt信息", "usedBy": [ "exams/data-access内部" ] }, { "name": "resolveSubjectGradeNames", "signature": "(input: { subjectId?, gradeId? }) => Promise<{ subjectName?, gradeName? }>", "purpose": "解析科目与年级名称", "usedBy": [ "exams/data-access内部" ] }, { "name": "buildExamDescription", "signature": "(input: { subject, grade, difficulty, totalScore, durationMin, scheduledAt?, questionCount? }) => string", "purpose": "构建考试描述文本", "usedBy": [ "exams/data-access内部" ] }, { "name": "getExamCreatorId", "signature": "(examId: string) => Promise", "purpose": "获取考试创建者ID(用于权限校验)", "usedBy": [ "updateExamAction", "deleteExamAction", "duplicateExamAction" ] }, { "name": "updateExamWithQuestions", "signature": "(input: { examId, title, subjectId, gradeId, difficulty, totalScore, durationMin, scheduledAt?, description?, structure }) => Promise", "purpose": "更新考试及题目关联(P1-2 新增,从 actions 下沉)", "usedBy": [ "updateExamAction" ] }, { "name": "updateExamWithRichEditorData", "signature": "(examId: string, data: { title: string; structure: unknown; orderedQuestions: Array<{ id: string; score: number }> }) => Promise", "purpose": "2026-06-24 新增(P0-4 修复):从富文本编辑器更新已有试卷的 DB 事务。单事务内更新 exams.title/structure + 删除旧 examQuestions + 插入新 examQuestions(带 order)。原 updateExamFromRichEditorAction 直接 db.transaction 的逻辑下沉至此,消除 actions 层直接 DB 操作违规。", "deps": [ "shared.db", "shared.db.schema.exams", "shared.db.schema.examQuestions", "drizzle-orm.eq" ], "usedBy": [ "exams/actions-rich-editor.updateExamFromRichEditorAction" ] }, { "name": "deleteExamById", "signature": "(examId: string) => Promise", "purpose": "按ID删除考试(P1-2 新增,从 actions 下沉)", "usedBy": [ "deleteExamAction" ] }, { "name": "duplicateExam", "signature": "(examId: string, creatorId: string) => Promise<{ newExamId: string }>", "purpose": "复制考试(P1-2 新增,从 actions 下沉)", "usedBy": [ "duplicateExamAction" ] }, { "name": "getExamPreview", "signature": "(examId: string) => Promise<{ structure: unknown; questions: Array<{ id: string }> } | null>", "purpose": "获取考试预览数据(P1-2 新增,从 actions 下沉)", "usedBy": [ "getExamPreviewAction" ] }, { "name": "getExamSubjects", "signature": "() => Promise>", "purpose": "获取科目列表(P1-2 新增,从 actions 下沉)", "usedBy": [ "getSubjectsAction" ] }, { "name": "getExamGrades", "signature": "() => Promise>", "purpose": "获取年级列表(P1-2 新增,从 actions 下沉)", "usedBy": [ "getGradesAction" ] }, { "name": "getExamsByGradeId", "signature": "(params: { gradeId: string; scope: DataScope }) => Promise", "file": "data-access-cross-module.ts", "purpose": "v4-P2-7 新增:年级仪表盘维度3,exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤", "deps": [ "shared.db", "shared.db.schema.exams", "shared.db.schema.examSubmissions", "classes/data-access.getClassGradeIdsByClassIds", "school/data-access.getSubjectOptions" ], "usedBy": [ "exams/actions.getExamsByGradeIdAction", "management/grade/dashboard" ], "reExportedFrom": "data-access.ts (向后兼容)" }, { "name": "getExamsForGradeEntry", "signature": "(scope: DataScope) => Promise", "file": "data-access-cross-module.ts", "purpose": "2026-06-24 新增:按 scope 过滤试卷列表,只返回有题目的试卷(供成绩录入页试卷选择器使用)。返回 id/title/subjectName/gradeName/questionCount/totalScore", "deps": [ "shared.db", "shared.db.schema.exams", "shared.db.schema.examQuestions", "shared.db.schema.subjects", "classes/data-access.getClassGradeIdsByClassIds", "school/data-access.getSubjectOptions" ], "usedBy": [ "teacher/grades/entry/page.tsx" ], "reExportedFrom": "data-access.ts (向后兼容)" }, { "name": "getExamForGradeEntry", "signature": "(examId: string, scope: DataScope) => Promise", "file": "data-access-cross-module.ts", "purpose": "2026-06-24 新增:获取单个试卷详情(含题目列表),innerJoin questions 获取 type,含 scope 校验。返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}]", "deps": [ "shared.db", "shared.db.schema.exams", "shared.db.schema.examQuestions", "shared.db.schema.questions", "classes/data-access.getClassGradeIdsByClassIds" ], "usedBy": [ "grades/actions.batchCreateGradeRecordsByExamAction", "teacher/grades/entry/page.tsx" ], "reExportedFrom": "data-access.ts (向后兼容)" }, { "name": "GetExamsParams", "type": "type", "definition": "{ q?, status?, difficulty?, page?, pageSize? }", "usedBy": [ "getExams", "getExamsAction" ] } ], "aiPipeline": [ { "name": "generateAiPreviewData", "signature": "(input: { title, subject?, grade?, difficulty, totalScore, durationMin, questionCount?, sourceText, aiProviderId? }) => Promise<{ ok, data?, rawOutput?, message? }>", "purpose": "AI预览生成", "deps": [ "shared/lib/ai.createAiChatCompletion" ], "usedBy": [ "previewAiExamAction" ] }, { "name": "generateAiCreateDraftFromSource", "signature": "同上", "purpose": "AI从源文本生成完整考试", "deps": [ "shared/lib/ai.createAiChatCompletion" ], "usedBy": [ "createAiExamAction" ] }, { "name": "regenerateAiQuestionByInstruction", "signature": "(input: { instruction, originalQuestion, sourceText?, aiProviderId? }) => Promise<{ ok, data?, message? }>", "purpose": "AI按指令重写题目", "deps": [ "shared/lib/ai.createAiChatCompletion" ], "usedBy": [ "regenerateAiQuestionAction" ] }, { "name": "loadAiDraftQuestionsAndStructure", "file": "ai-pipeline/index.ts", "signature": "(input: { rawAiQuestions, rawStructure, title, subject, grade, difficulty, totalScore, durationMin, aiSourceText?, aiQuestionCount?, aiProviderId? }) => Promise<{ ok: true, generated: AiGeneratedQuestion[], structure: AiGeneratedStructureNode[] } | { ok: false, message: string }>", "purpose": "2026-06-24 新增(从 actions.ts 下沉):加载 AI 草稿题目与结构。优先从 rawAiQuestions JSON 解析预览 payload(含 rawStructure 结构校验),否则从源文本调用 generateAiCreateDraftFromSource 生成。供 createAiExamAction 复用,消除 actions.ts 重复逻辑。", "deps": [ "zod", "@paralleldrive/cuid2.createId", "exams/ai-pipeline/parse.AiInsertQuestionSchema", "exams/ai-pipeline/parse.AiGeneratedStructureSchema", "exams/ai-pipeline.generateAiCreateDraftFromSource" ], "usedBy": [ "exams/actions.createAiExamAction" ] }, { "name": "AiQuestionSchema", "type": "const", "description": "zod schema 校验AI生成题目", "usedBy": [ "ai-pipeline内部" ] }, { "name": "AiInsertQuestionSchema", "type": "const", "description": "zod schema 校验AI插入题目", "usedBy": [ "ai-pipeline内部" ] }, { "name": "AiGeneratedQuestion", "type": "type", "definition": "{ id, type, difficulty, score, content }", "usedBy": [ "ai-pipeline内部", "exams/components" ] }, { "name": "AiGeneratedStructureNode", "type": "type", "definition": "{ id, type: \"group\"|\"question\", title?, questionId?, score?, children? }", "usedBy": [ "ai-pipeline内部", "exams/components" ] }, { "name": "AiGeneratedStructureNodeSchema", "type": "const", "description": "zod schema 校验AI生成结构节点(递归)", "usedBy": [ "ai-pipeline内部" ] }, { "name": "AiGeneratedStructureSchema", "type": "const", "description": "zod schema 校验AI生成结构", "usedBy": [ "ai-pipeline内部" ] }, { "name": "generateAiExamDraft", "type": "function", "signature": "(input) => Promise", "purpose": "生成AI考试草稿", "deps": [ "shared/lib/ai.createAiChatCompletion" ], "usedBy": [ "ai-pipeline内部" ] } ], "types": [ { "name": "Exam", "definition": "{ id, title, subject, grade, status, difficulty, totalScore, durationMin, questionCount, scheduledAt?, createdAt, updatedAt?, tags? }", "usedBy": [ "exams/components", "homework/types", "dashboard/types" ] }, { "name": "AiPreviewData", "definition": "{ title, rawOutput?, sections?, questions? }", "usedBy": [ "exams/actions", "exams/components" ] }, { "name": "AiRewriteQuestionData", "definition": "{ type, difficulty, score, content }", "usedBy": [ "exams/actions", "exams/components" ] }, { "name": "ExamStatus", "type": "type", "definition": "\"draft\" | \"published\" | \"archived\"", "usedBy": [ "exams/components", "exams/data-access" ] }, { "name": "ExamDifficulty", "type": "type", "definition": "1 | 2 | 3 | 4 | 5", "usedBy": [ "exams/components", "exams/data-access" ] }, { "name": "SubmissionStatus", "type": "type", "definition": "\"pending\" | \"graded\"", "usedBy": [ "exams/components", "exams/data-access" ] }, { "name": "ExamSubmission", "type": "interface", "definition": "{ id, examId, examTitle, studentName, submittedAt, score?, status }", "usedBy": [ "exams/components" ] }, { "name": "ExamQuestionItem", "type": "interface", "file": "types.ts", "definition": "{ id: string; order: number; score: number; type: string }", "purpose": "2026-06-24 新增:试卷中单个题目的精简结构(供成绩录入使用)", "usedBy": [ "types.ExamForGradeEntry", "grades/components/batch-grade-entry" ] }, { "name": "ExamForGradeEntry", "type": "interface", "file": "types.ts", "definition": "{ id: string; title: string; subjectId: string | null; gradeId: string | null; totalScore: number; questions: ExamQuestionItem[] }", "purpose": "2026-06-24 新增:成绩录入用的试卷详情(含题目列表,由 getExamForGradeEntry 返回)", "usedBy": [ "exams/data-access.getExamForGradeEntry", "grades/actions.batchCreateGradeRecordsByExamAction", "grades/components/batch-grade-entry", "teacher/grades/entry/page.tsx" ] }, { "name": "ExamOptionForEntry", "type": "interface", "file": "types.ts", "definition": "{ id: string; title: string; subjectName: string; gradeName: string; questionCount: number; totalScore: number }", "purpose": "2026-06-24 新增:成绩录入页试卷选择器的选项结构(由 getExamsForGradeEntry 返回)", "usedBy": [ "exams/data-access.getExamsForGradeEntry", "grades/components/batch-grade-entry", "teacher/grades/entry/page.tsx" ] }, { "name": "AutoMarkResult", "type": "interface", "file": "ai-pipeline/auto-mark.ts", "definition": "{ doc: unknown; title: string }", "purpose": "2026-06-24 新增:AI 自动标记结果。doc 为 Tiptap JSONContent 文档(可直接载入富文本编辑器),title 为解析出的试卷标题(如有)。2026-06-24 拆分:从 actions.ts 迁移至 ai-pipeline/auto-mark.ts,actions.ts re-export 保持向后兼容。", "usedBy": [ "exams/ai-pipeline/auto-mark.autoMarkExamAction", "exams/components/exam-rich-form.tsx" ] }, { "name": "AutoMarkSchema", "type": "const", "file": "ai-pipeline/auto-mark.ts", "definition": "z.object({ sourceText: z.string().min(1), aiProviderId: z.string().optional() })", "purpose": "2026-06-24 新增:autoMarkExamAction 的 Zod 输入校验 schema。2026-06-24 拆分:从 actions.ts 迁移至 ai-pipeline/auto-mark.ts。", "usedBy": [ "exams/ai-pipeline/auto-mark.autoMarkExamAction" ] }, { "name": "RichExamCreateSchema", "type": "const", "file": "actions-rich-editor.ts", "definition": "z.object({ title, subject, grade, difficulty: z.coerce.number().int().min(1).max(5), totalScore: z.coerce.number().int().min(1), durationMin: z.coerce.number().int().min(1), scheduledAt: z.string().optional().nullable(), editorDoc: z.string().min(1) })", "purpose": "2026-06-24 新增:createExamFromRichEditorAction 的 Zod 输入校验 schema。editorDoc 为 Tiptap JSONContent 的 JSON 字符串。2026-06-24 拆分:从 actions.ts 迁移至 actions-rich-editor.ts。", "usedBy": [ "exams/actions-rich-editor.createExamFromRichEditorAction" ] }, { "name": "RichExamUpdateSchema", "type": "const", "file": "actions-rich-editor.ts", "definition": "z.object({ examId: z.string().min(1), title: z.string().min(1), editorDoc: z.string().min(1) })", "purpose": "2026-06-24 新增:updateExamFromRichEditorAction 的 Zod 输入校验 schema。editorDoc 为 Tiptap JSONContent 的 JSON 字符串。2026-06-24 拆分:从 actions.ts 迁移至 actions-rich-editor.ts。", "usedBy": [ "exams/actions-rich-editor.updateExamFromRichEditorAction" ] } ], "components": [ { "name": "ExamPaperPreview", "file": "assembly/exam-paper-preview.tsx", "purpose": "试卷预览" }, { "name": "QuestionBankList", "file": "assembly/question-bank-list.tsx", "purpose": "题库列表(组卷选择)" }, { "name": "SelectedQuestionList", "file": "assembly/selected-question-list.tsx", "purpose": "已选题目列表", "types": [ "ExamNode" ] }, { "name": "StructureEditor", "file": "assembly/structure-editor.tsx", "purpose": "试卷结构编辑器" }, { "name": "ExamActions", "file": "exam-actions.tsx", "purpose": "考试操作按钮(删除/复制等)" }, { "name": "ExamAiGenerator", "file": "exam-ai-generator.tsx", "purpose": "AI生成考试面板" }, { "name": "ExamAssembly", "file": "exam-assembly.tsx", "purpose": "组卷主界面" }, { "name": "ExamBasicInfoForm", "file": "exam-basic-info-form.tsx", "purpose": "考试基本信息表单" }, { "name": "ExamCard", "file": "exam-card.tsx", "purpose": "考试卡片" }, { "name": "examColumns", "file": "exam-columns.tsx", "type": "ColumnDef[]", "purpose": "考试表格列定义" }, { "name": "ExamDataTable", "file": "exam-data-table.tsx", "purpose": "考试数据表格(✅ Phase 4.3:@tanstack/react-virtual 虚拟滚动,替换分页避免大数据渲染卡顿)" }, { "name": "ExamFilters", "file": "exam-filters.tsx", "purpose": "考试筛选器" }, { "name": "formSchema", "file": "exam-form-types.ts", "type": "const", "purpose": "表单zod schema" }, { "name": "ExamFormValues", "file": "exam-form-types.ts", "type": "type", "purpose": "表单值类型" }, { "name": "PreviewQuestion", "file": "exam-form-types.ts", "type": "type", "purpose": "预览题目类型" }, { "name": "EditableQuestionContent", "file": "exam-form-types.ts", "type": "type", "purpose": "可编辑题目内容" }, { "name": "PreviewSnapshotMeta", "file": "exam-form-types.ts", "type": "type", "purpose": "预览快照元数据" }, { "name": "PreviewBackgroundTask", "file": "exam-form-types.ts", "type": "type", "purpose": "预览后台任务" }, { "name": "aiProviderLabels", "file": "exam-form-types.ts", "type": "const", "purpose": "AI Provider标签映射" }, { "name": "defaultValues", "file": "exam-form-types.ts", "type": "const", "purpose": "表单默认值" }, { "name": "previewTaskStorageKey", "file": "exam-form-types.ts", "type": "const", "purpose": "预览任务localStorage key" }, { "name": "ExamForm", "file": "exam-form.tsx", "purpose": "考试表单(创建/编辑)" }, { "name": "ExamGrid", "file": "exam-grid.tsx", "purpose": "考试网格视图" }, { "name": "ExamModeSelector", "file": "exam-mode-selector.tsx", "purpose": "考试模式选择(手动/AI)" }, { "name": "ExamPreviewDialog", "file": "exam-preview-dialog.tsx", "purpose": "考试预览对话框" }, { "name": "ExamPreviewQuestionEditor", "file": "exam-preview-question-editor.tsx", "purpose": "预览题目编辑器" }, { "name": "buildPreviewNodes", "file": "exam-preview-utils.ts", "type": "function", "purpose": "构建预览节点" }, { "name": "parseEditableContent", "file": "exam-preview-utils.ts", "type": "function", "purpose": "解析可编辑内容" }, { "name": "flattenPreviewQuestions", "file": "exam-preview-utils.ts", "type": "function", "purpose": "扁平化预览题目" }, { "name": "findPreviewQuestionNode", "file": "exam-preview-utils.ts", "type": "function", "purpose": "查找预览题目节点" }, { "name": "updatePreviewQuestionNodeInList", "file": "exam-preview-utils.ts", "type": "function", "purpose": "更新预览题目节点" }, { "name": "buildPreviewSignature", "file": "exam-preview-utils.ts", "type": "function", "purpose": "构建预览签名" }, { "name": "buildPreviewPayload", "file": "exam-preview-utils.ts", "type": "function", "purpose": "构建预览payload" }, { "name": "buildPreviewRequestData", "file": "exam-preview-utils.ts", "type": "function", "signature": "(values: ExamFormValues) => { formData: FormData; meta: PreviewSnapshotMeta; signature: string } | null", "purpose": "构建预览请求数据(FormData + meta + signature);sourceText 为空时返回 null" }, { "name": "buildTaskFormValues", "file": "exam-preview-utils.ts", "type": "function", "signature": "(values: ExamFormValues) => NonNullable[\"formValues\"]", "purpose": "✅ 2026-06-24 新增:从 ExamFormValues 提取后台任务持久化所需的表单字段子集(title/subject/grade/difficulty/totalScore/durationMin/aiSourceText/aiQuestionCount/aiProviderId),用于任务完成后回填表单" }, { "name": "isPreviewBackgroundTask", "file": "exam-preview-utils.ts", "type": "function", "signature": "(v: unknown) => v is PreviewBackgroundTask", "purpose": "✅ 2026-06-24 新增:类型守卫,校验 unknown 对象是否符合 PreviewBackgroundTask 结构(id 字符串 + status 枚举 + createdAt 数字 + title 字符串);用于 localStorage 恢复时过滤非法数据" }, { "name": "persistPreviewTasksToStorage", "file": "exam-preview-utils.ts", "type": "function", "signature": "(tasks: PreviewBackgroundTask[]) => void", "purpose": "✅ 2026-06-24 新增:将预览后台任务列表持久化到 localStorage(key=previewTaskStorageKey,截断保留前 20 条);异常时 console.error 降级" }, { "name": "parseStoredPreviewTasks", "file": "exam-preview-utils.ts", "type": "function", "signature": "(raw: string, t: Translator) => PreviewBackgroundTask[] | null", "purpose": "✅ 2026-06-24 新增:从 localStorage 原始字符串解析预览后台任务列表;JSON.parse 失败返回 null;过滤非法条目;将 queued/running 状态标记为 failed(附 taskInterrupted 消息)防止僵尸任务" }, { "name": "executeBackgroundPreviewTask", "file": "exam-preview-utils.ts", "type": "function", "signature": "(taskId: string, taskTitle: string, values: ExamFormValues, requestData: NonNullable>, t: Translator, setPreviewTasks: Dispatch>) => Promise", "purpose": "✅ 2026-06-24 新增:执行单个后台预览任务(从 use-exam-preview-tasks Hook 提取的纯函数)。标记 running → 调用 previewAiExamAction → 成功构建 nodes 并 toast 成功 / 失败标记 failed 并 toast 错误;异常捕获并降级" }, { "name": "ExamViewer", "file": "exam-viewer.tsx", "purpose": "考试查看器" }, { "name": "QuestionOptionsEditor", "file": "question-options-editor.tsx", "purpose": "题目选项编辑器" }, { "name": "QuestionSubQuestionsEditor", "file": "question-sub-questions-editor.tsx", "purpose": "子题目编辑器" }, { "name": "ExamAnalyticsDashboard", "file": "exam-analytics-dashboard.tsx", "purpose": "V3-8 新增:考试分析仪表盘(汇总卡片:应考人数/已批改份数/平均分/及格率 + 分数段分布 + 逐题分析表:错误率/难度等级),对标智学网考试分析功能", "types": [ "ExamAnalyticsSummary" ] }, { "name": "ExamRichForm", "file": "exam-rich-form.tsx", "purpose": "2026-06-24 新增:富文本编辑器试卷创建/编辑表单(use client)。使用 ResizablePanel 左编辑右预览布局,集成 autoMarkExamAction 一键 AI 标记(粘贴试卷文本 → AI 解析为 Tiptap doc 载入编辑器)。支持 mode='create'|'edit' 双模式:create 模式调用 createExamFromRichEditorAction 保存草稿(含 subject/grade/difficulty/totalScore/durationMin/scheduledAt 字段),edit 模式调用 updateExamFromRichEditorAction 更新已有试卷(隐藏基本信息字段,显示 Back to Build 按钮)。调用 getSubjectsAction/getGradesAction 填充科目年级选项。2026-06-24 拆分后:import 路径直连 ai-pipeline/auto-mark.ts 和 actions-rich-editor.ts。" }, { "name": "ExamPreview", "file": "exam-preview.tsx", "purpose": "✅ P2-2 新增(从 exam-rich-form.tsx 提取):试卷结构预览组件(use client)。渲染 section/group/question 三级嵌套,支持题号自动编号、题型标签、分值显示、选项列表、复合题子题、题目图片。✅ P3-5 改进:题目图片从 迁移到 next/image (fill + sizes='128px' + object-contain),优化图片加载性能。", "deps": [ "next/image", "next-intl.useTranslations", "shared/lib/utils.cn", "exams/editor.EditorDoc" ], "usedBy": [ "exam-rich-form.tsx" ] } ], "hooks": [ { "name": "useExamPreview", "file": "use-exam-preview.ts", "signature": "(form: UseFormReturn, t: Translator) => { previewOpen, setPreviewOpen, previewLoading, previewNodes, previewTitle, previewRawOutput, previewSignature, previewMeta, selectedQuestionId, rewriteInstruction, rewritingQuestion, previewTasks, handlePreview, handleBackgroundPreview, handleOpenPreviewTask, handleRewriteSelectedQuestion, buildPreviewNodes, parseEditableContent, flattenPreviewQuestions, findPreviewQuestionNode, buildPreviewPayload, buildPreviewRequestData, buildPreviewSignature }", "purpose": "考试预览 Hook 主组合器(✅ 2026-06-24 拆分:从 318 行拆为 68 行,组合 state/tasks/rewrite 三个子 Hook,保留 handlePreview 编排逻辑,返回 API 与拆分前完全一致)", "usedBy": [ "exam-form.tsx" ], "deps": [ "exams/hooks/use-exam-preview-state.usePreviewState", "exams/hooks/use-exam-preview-tasks.usePreviewBackgroundTasks", "exams/hooks/use-exam-preview-rewrite.usePreviewRewrite", "exams/actions.previewAiExamAction", "exams/components/exam-preview-utils.*" ] }, { "name": "usePreviewState", "file": "use-exam-preview-state.ts", "signature": "() => { previewOpen, setPreviewOpen, previewLoading, setPreviewLoading, previewNodes, setPreviewNodes, previewTitle, setPreviewTitle, previewRawOutput, setPreviewRawOutput, previewSignature, setPreviewSignature, previewMeta, setPreviewMeta, selectedQuestionId, setSelectedQuestionId, rewriteInstruction, setRewriteInstruction, rewritingQuestion, setRewritingQuestion, updatePreviewQuestionNode, updateSelectedQuestionFromAi, applyPreviewResult }", "purpose": "✅ 2026-06-24 新增:考试预览状态子 Hook(67 行)。集中管理 10 个 useState 声明(previewOpen/previewLoading/previewNodes/previewTitle/previewRawOutput/previewSignature/previewMeta/selectedQuestionId/rewriteInstruction/rewritingQuestion)+ 3 个状态更新函数(updatePreviewQuestionNode 调用 updatePreviewQuestionNodeInList、updateSelectedQuestionFromAi 应用 AI 重写结果、applyPreviewResult 应用预览快照)", "usedBy": [ "exams/hooks/use-exam-preview.ts" ], "deps": [ "exams/components/exam-preview-utils.buildPreviewNodes", "exams/components/exam-preview-utils.flattenPreviewQuestions", "exams/components/exam-preview-utils.updatePreviewQuestionNodeInList", "exams/actions.AiPreviewData", "exams/actions.AiRewriteQuestionData" ] }, { "name": "usePreviewBackgroundTasks", "file": "use-exam-preview-tasks.ts", "signature": "(form: UseFormReturn, t: Translator, state: ReturnType) => { previewTasks, setPreviewTasks, handleBackgroundPreview, handleOpenPreviewTask }", "purpose": "✅ 2026-06-24 新增:考试预览后台任务子 Hook(80 行)。管理 PQueue 并发队列(concurrency:3)、localStorage 持久化恢复 effect(启动时将 queued/running 任务标记为 failed 防止僵尸态)、persistPreviewTasksToStorage 持久化 effect、handleBackgroundPreview 入队新任务、handleOpenPreviewTask 打开已完成任务并回填表单字段", "usedBy": [ "exams/hooks/use-exam-preview.ts" ], "deps": [ "exams/hooks/use-exam-preview-state.usePreviewState", "exams/components/exam-preview-utils.buildPreviewRequestData", "exams/components/exam-preview-utils.executeBackgroundPreviewTask", "exams/components/exam-preview-utils.flattenPreviewQuestions", "exams/components/exam-preview-utils.parseStoredPreviewTasks", "exams/components/exam-preview-utils.persistPreviewTasksToStorage", "exams/components/exam-form-types.previewTaskStorageKey" ] }, { "name": "usePreviewRewrite", "file": "use-exam-preview-rewrite.ts", "signature": "(form: UseFormReturn, t: Translator, state: ReturnType) => { handleRewriteSelectedQuestion }", "purpose": "✅ 2026-06-24 新增:考试预览 AI 重写子 Hook(79 行)。实现 handleRewriteSelectedQuestion:校验选中题目 → 解析可编辑内容 → 构造 questionPayload(type/difficulty/score/content)→ FormData 携带 instruction/questionJson/aiProviderId/sourceText → 调用 regenerateAiQuestionAction → 成功后 updateSelectedQuestionFromAi 应用结果", "usedBy": [ "exams/hooks/use-exam-preview.ts" ], "deps": [ "exams/hooks/use-exam-preview-state.usePreviewState", "exams/components/exam-preview-utils.findPreviewQuestionNode", "exams/components/exam-preview-utils.parseEditableContent", "exams/actions.regenerateAiQuestionAction" ] } ], "utils": [ { "name": "normalizeStructure", "file": "utils/normalize-structure.ts", "type": "function", "signature": "(nodes: unknown) => ExamNode[]", "purpose": "将持久化的 exam.structure(unknown JSON)运行时校验并归一化为类型安全的 ExamNode[](类型守卫模式,无 as 断言;递归处理 group children;保证 id 唯一)", "deps": [ "@paralleldrive/cuid2.createId", "exams/components/assembly/selected-question-list.ExamNode" ], "usedBy": [ "teacher/exams/[id]/build/page.tsx" ] } ], "statsService": [ { "name": "getExamAnalytics", "file": "stats-service.ts", "type": "function", "signature": "(examId: string) => Promise", "purpose": "V3-8 新增:考试分析数据聚合(cache 包装)。聚合该考试所有作业的已批改提交数据,计算平均分、及格率、分数段分布、逐题错误率与难度等级(easy/medium/hard)。对标智学网考试分析功能。", "deps": [ "shared.db", "shared.db.schema.exams", "shared.db.schema.examQuestions", "homework/data-access.getHomeworkAssignmentsByExamId", "homework/data-access.getGradedSubmissionsByExamId", "homework/lib/question-content-utils.getQuestionText", "react.cache" ], "usedBy": [ "teacher/exams/[id]/analytics/page.tsx" ] }, { "name": "ExamAnalyticsSummary", "file": "stats-service.ts", "type": "interface", "definition": "{ examId, examTitle, totalScore, assignmentCount, totalStudents, submittedCount, gradedCount, averageScore, maxScore, passRate, scoreDistribution: Array<{ range, count }>, questions: Array<{ questionId, questionType, questionText, maxScore, errorCount, errorRate, difficulty: 'easy'|'medium'|'hard' }> }", "usedBy": [ "getExamAnalytics", "exams/components/exam-analytics-dashboard.tsx" ] } ], "editor": { "_description": "2026-06-24 新增:基于 Tiptap 的试卷富文本编辑器子模块(src/modules/exams/editor/)。提供自定义扩展(题目块/分组/填空/加点字/图片)、Tiptap JSONContent ↔ ExamStructure 双向转换、浮动选择工具栏。Barrel 导出位于 editor/index.ts。", "components": [ { "name": "ExamRichEditor", "file": "editor/exam-rich-editor.tsx", "type": "component", "purpose": "富文本编辑器主体(use client + forwardRef)。基于 Tiptap StarterKit + 自定义扩展(DottedMark/BlankNode/ImageNode/QuestionBlock/GroupBlock)。通过 ExamRichEditorHandle 暴露 getJSON() 供父组件获取 JSONContent。集成 SelectionToolbar 浮动工具栏。", "usedBy": [ "exams/components/exam-rich-form.tsx" ] }, { "name": "ExamRichEditorHandle", "file": "editor/exam-rich-editor.tsx", "type": "interface", "definition": "{ getJSON: () => EditorJSONContent | null }", "purpose": "ExamRichEditor 的 ref 句柄类型,暴露 getJSON 方法获取当前编辑器内容的 Tiptap JSONContent", "usedBy": [ "exams/components/exam-rich-form.tsx" ] }, { "name": "SelectionToolbar", "file": "editor/selection-toolbar.tsx", "type": "component", "purpose": "浮动选择工具栏(use client)。当编辑器内有文本选中时显示,提供 DottedMark(加点字)等标记的快捷应用按钮。", "usedBy": [ "exams/editor/exam-rich-editor.tsx" ] } ], "extensions": [ { "name": "DottedMark", "file": "editor/extensions/dotted-mark.ts", "type": "tiptap-extension", "purpose": "加点字标记扩展(Mark)。用于拼音注音题中加点的字,渲染为带下点样式的 span。", "usedBy": [ "exams/editor/exam-rich-editor.tsx", "exams/editor/selection-toolbar.tsx" ] }, { "name": "BlankNode", "file": "editor/extensions/blank-node.tsx", "type": "tiptap-extension", "purpose": "填空空位节点扩展(Node,原子节点)。渲染为可填写的空位占位符,含 id/answer/score 属性。", "usedBy": [ "exams/editor/exam-rich-editor.tsx" ] }, { "name": "ImageNode", "file": "editor/extensions/image-node.ts", "type": "tiptap-extension", "purpose": "图片节点扩展(Node)。含 fileId/url/alt 属性,用于在题目中嵌入图片。", "usedBy": [ "exams/editor/exam-rich-editor.tsx" ] }, { "name": "GroupBlock", "file": "editor/extensions/group-block.tsx", "type": "tiptap-extension", "purpose": "大题分组节点扩展(Node,块级)。含 title 属性,用于「一、选择题」「二、填空题」等大题分组。", "usedBy": [ "exams/editor/exam-rich-editor.tsx" ] }, { "name": "QuestionBlock", "file": "editor/extensions/question-block.tsx", "type": "tiptap-extension", "purpose": "题目块节点扩展(Node,块级)。含 type(QuestionBlockType: single_choice|multiple_choice|judgment|text|composite)/score 等 attrs(QuestionBlockAttrs),用于包裹一道题目。", "usedBy": [ "exams/editor/exam-rich-editor.tsx" ] }, { "name": "QuestionBlockType", "file": "editor/extensions/question-block.tsx", "type": "type", "definition": "\"single_choice\" | \"multiple_choice\" | \"judgment\" | \"text\" | \"composite\"", "purpose": "题目块类型枚举", "usedBy": [ "exams/editor/extensions/question-block.tsx" ] }, { "name": "QuestionBlockAttrs", "file": "editor/extensions/question-block.tsx", "type": "interface", "definition": "{ type: QuestionBlockType; score: number; questionId?: string }", "purpose": "题目块属性接口", "usedBy": [ "exams/editor/extensions/question-block.tsx" ] } ], "types": [ { "name": "RichQuestionType", "file": "editor/exam-rich-editor-types.ts", "type": "type", "definition": "\"single_choice\" | \"multiple_choice\" | \"judgment\" | \"text\" | \"composite\"", "purpose": "富文本编辑器题目类型枚举", "usedBy": [ "exams/editor/*", "exams/editor/editor-to-structure.ts", "exams/editor/structure-to-editor.ts" ] }, { "name": "RichQuestionContent", "file": "editor/exam-rich-editor-types.ts", "type": "interface", "definition": "{ text: string; options?: Array<{ id, text, isCorrect? }>; blanks?: Array<{ id, answer?, score? }>; images?: Array<{ fileId, url, alt? }>; subQuestions?: Array<{ id, text, answer?, score? }>; correctAnswer?: unknown }", "purpose": "富文本编辑器题目内容结构(题干文本+选项+填空+图片+子题+正确答案)", "usedBy": [ "exams/editor/exam-rich-editor-types.ts.EditorQuestion", "exams/editor/editor-to-structure.ts", "exams/editor/structure-to-editor.ts" ] }, { "name": "EditorQuestion", "file": "editor/exam-rich-editor-types.ts", "type": "interface", "definition": "{ id: string; type: RichQuestionType; score: number; content: RichQuestionContent }", "purpose": "富文本编辑器题目结构", "usedBy": [ "exams/editor/exam-rich-editor-types.ts.EditorDoc", "exams/editor/editor-to-structure.ts", "exams/editor/structure-to-editor.ts" ] }, { "name": "EditorStructureNode", "file": "editor/exam-rich-editor-types.ts", "type": "interface", "definition": "{ id: string; type: \"group\" | \"question\"; title?: string; questionId?: string; score?: number; children?: EditorStructureNode[] }", "purpose": "富文本编辑器结构节点(递归,group|question)", "usedBy": [ "exams/editor/exam-rich-editor-types.ts.EditorDoc", "exams/editor/editor-to-structure.ts", "exams/editor/structure-to-editor.ts" ] }, { "name": "EditorDoc", "file": "editor/exam-rich-editor-types.ts", "type": "interface", "definition": "{ title: string; questions: EditorQuestion[]; structure: EditorStructureNode[] }", "purpose": "富文本编辑器文档结构(标题+题目列表+结构树),由 editorDocToStructure 产出", "usedBy": [ "exams/editor/editor-to-structure.ts", "exams/editor/structure-to-editor.ts", "exams/components/exam-rich-form.tsx" ] }, { "name": "EditorJSONContent", "file": "editor/exam-rich-editor-types.ts", "type": "type", "definition": "JSONContent (from @tiptap/react)", "purpose": "Tiptap JSONContent 别名,用于编辑器与试卷结构互转", "usedBy": [ "exams/editor/exam-rich-editor.tsx", "exams/components/exam-rich-form.tsx" ] } ], "converters": [ { "name": "editorDocToStructure", "file": "editor/editor-to-structure.ts", "type": "function", "signature": "(doc: EditorJSONContent, fallbackTitle: string) => EditorDoc", "purpose": "将 Tiptap JSONContent 转换为 EditorDoc 结构(title + questions + structure)。遍历 doc.nodes,识别 GroupBlock/QuestionBlock/BlankNode/DottedMark/ImageNode 等自定义节点,提取题目内容与结构树。供 createExamFromRichEditorAction/updateExamFromRichEditorAction 持久化使用。", "deps": [ "@paralleldrive/cuid2.createId", "exams/editor/exam-rich-editor-types.ts" ], "usedBy": [ "exams/actions-rich-editor.createExamFromRichEditorAction", "exams/actions-rich-editor.updateExamFromRichEditorAction", "exams/components/exam-rich-form.tsx" ] }, { "name": "structureToEditorDoc", "file": "editor/structure-to-editor.ts", "type": "function", "signature": "(doc: EditorDoc) => EditorJSONContent", "purpose": "将 EditorDoc 结构转换回 Tiptap JSONContent,供编辑器回填已保存的试卷内容。", "deps": [ "exams/editor/exam-rich-editor-types.ts" ], "usedBy": [ "exams/editor/exam-rich-editor.tsx" ] }, { "name": "isRichQuestionType", "file": "editor/exam-rich-editor-types.ts", "type": "function", "signature": "(v: unknown) => v is RichQuestionType", "purpose": "2026-06-25 P2-1 新增:运行时类型守卫,验证 unknown 值是否为合法的 RichQuestionType。用于消除 Tiptap attrs / AI 返回值 / DB 字段等边界处的 as RichQuestionType 断言。", "deps": [], "usedBy": [ "exams/editor/editor-to-structure.ts", "exams/editor/exam-nodes-to-editor-doc.ts" ] }, { "name": "isStandaloneQuestionType", "file": "editor/exam-rich-editor-types.ts", "type": "function", "signature": "(v: unknown) => v is StandaloneQuestionType", "purpose": "2026-06-25 P2-1 新增:运行时类型守卫,验证 unknown 值是否为可直接持久化的题型(不含 composite)。用于 actions/data-access 在写入 questions 表前收窄类型。", "deps": [], "usedBy": [ "exams/actions-rich-editor.ts" ] }, { "name": "toRichQuestionType", "file": "editor/exam-rich-editor-types.ts", "type": "function", "signature": "(v: unknown, fallback?: RichQuestionType) => RichQuestionType", "purpose": "2026-06-25 P2-1 新增:安全转换函数,将 unknown 收窄为 RichQuestionType,非法值回退到 fallback(默认 'single_choice')。", "deps": [], "usedBy": [ "exams/editor/editor-to-structure.ts", "exams/editor/exam-nodes-to-editor-doc.ts" ] }, { "name": "toStandaloneQuestionType", "file": "editor/exam-rich-editor-types.ts", "type": "function", "signature": "(v: RichQuestionType, fallback?: StandaloneQuestionType) => StandaloneQuestionType", "purpose": "2026-06-25 P2-1 新增:安全转换函数,将 RichQuestionType 收窄为 StandaloneQuestionType。composite 会回退到 fallback(默认 'text'),调用方应优先过滤 composite。", "deps": [], "usedBy": [ "exams/actions-rich-editor.ts" ] }, { "name": "ExamErrorBoundary", "file": "components/exam-boundaries.tsx", "type": "component", "purpose": "2026-06-25 P2-3 新增:考试错误边界,薄包装 shared 层 SectionErrorBoundary,固定使用 examHomework 命名空间。捕获渲染异常并显示统一的降级 UI。", "deps": [ "shared/components/section-error-boundary.SectionErrorBoundary" ], "usedBy": [] }, { "name": "ExamEmptyState", "file": "components/exam-boundaries.tsx", "type": "component", "purpose": "2026-06-25 P2-3 新增:考试空状态组件。标题/描述/操作均通过 i18n 键传入,默认显示 exam.list.empty 与 exam.list.emptyDescription。复用 shared 层 EmptyState。", "deps": [ "shared/components/ui/empty-state.EmptyState", "next-intl.useTranslations" ], "usedBy": [] }, { "name": "ExamSkeleton", "file": "components/exam-boundaries.tsx", "type": "component", "signature": "(props: { variant?: 'list' | 'card' | 'detail' | 'analytics' | 'editor'; className?: string }) => ReactNode", "purpose": "2026-06-25 P2-3 新增:考试骨架屏,5 种变体匹配不同页面类型。复用 shared 层 Skeleton。", "deps": [ "shared/components/ui/skeleton.Skeleton" ], "usedBy": [] }, { "name": "ExamServicePort", "file": "services/exam-service-port.ts", "type": "interface", "purpose": "2026-06-25 P2-4 新增:exams 模块对内的服务端口契约。导出 12 个方法(listExams/getExam/createExam/createAiExam/createExamFromRichEditor/updateExam/updateExamFromRichEditor/deleteExam/duplicateExam/autoMarkExam/getExamPreview/getAnalytics/getSubjects/getGrades)+ ExamDetail/ExamCreateInput/ExamUpdateInput 类型。区别于 shared 层 ExamHomeworkServicePort(跨模块调用方契约,单例注册),本接口面向模块内部组件,通过 React Context 注入。", "deps": [ "shared/types/action-state.ActionState", "shared/types/permissions.DataScope", "exams/types.Exam", "exams/stats-service.ExamAnalyticsSummary", "exams/data-access.GetExamsParams" ], "usedBy": [ "exams/services/exam-service-context.tsx" ] }, { "name": "ExamServiceProvider", "file": "services/exam-service-context.tsx", "type": "component", "signature": "(props: { service: ExamServicePort; children: ReactNode }) => ReactNode", "purpose": "2026-06-25 P2-4 新增:ExamServicePort 的 React Context 注入点。Provider 组件。", "deps": [ "react.createContext", "exams/services/exam-service-port.ExamServicePort" ], "usedBy": [] }, { "name": "useExamService", "file": "services/exam-service-context.tsx", "type": "hook", "signature": "() => ExamServicePort", "purpose": "2026-06-25 P2-4 新增:获取注入的 ExamService 实例。必须在 ExamServiceProvider 子树内调用,否则抛出明确错误。", "deps": [ "react.useContext" ], "usedBy": [] }, { "name": "TEACHER_EXAM_WIDGET_CONFIG / ADMIN_EXAM_WIDGET_CONFIG / STUDENT_EXAM_WIDGET_CONFIG / PARENT_EXAM_WIDGET_CONFIG", "file": "config/exam-widgets.ts", "type": "constant", "purpose": "2026-06-25 P2-5 新增:四个角色的 exams 模块 Widget 布局配置。配置驱动决定每个角色在 exams 模块渲染哪些子模块/区块,新增角色或调整 Widget 只需修改配置。", "deps": [], "usedBy": [] }, { "name": "getExamWidgetConfig", "file": "config/exam-widgets.ts", "type": "function", "signature": "(role: ExamRole) => ExamWidgetLayoutConfig", "purpose": "2026-06-25 P2-5 新增:按角色获取 exams 模块 Widget 配置。", "deps": [], "usedBy": [] }, { "name": "getWidgetsBySlot", "file": "config/exam-widgets.ts", "type": "function", "signature": "(config: ExamWidgetLayoutConfig, slot: ExamWidgetSlot) => ExamWidgetConfig[]", "purpose": "2026-06-25 P2-5 新增:从配置中筛选指定 slot 的 Widget(按 order 升序)。", "deps": [], "usedBy": [] } ] } } }, "homework": { "path": "src/modules/homework", "description": "作业全生命周期:创建(源自考试)、发布、学生作答、教师批改、数据分析", "exports": { "actions": [ { "name": "createHomeworkAssignmentAction", "permission": "HOMEWORK_CREATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "从已有考试创建作业", "deps": [ "requirePermission", "data-access-write.createHomeworkAssignment", "exams/data-access.getExams" ], "usedBy": [ "homework-assignment-form.tsx" ] }, { "name": "startHomeworkSubmissionAction", "permission": "HOMEWORK_SUBMIT", "signature": "同上", "purpose": "学生开始作答", "deps": [ "requirePermission", "data-access-write.startHomeworkSubmission" ], "usedBy": [ "homework-take-view.tsx" ] }, { "name": "saveHomeworkAnswerAction", "permission": "HOMEWORK_SUBMIT", "signature": "同上", "purpose": "保存单题答案", "deps": [ "requirePermission", "data-access-write.saveHomeworkAnswer" ], "usedBy": [ "homework-take-view.tsx" ] }, { "name": "submitHomeworkAction", "permission": "HOMEWORK_SUBMIT", "signature": "同上", "purpose": "提交作业", "deps": [ "requirePermission", "data-access-write.submitHomework" ], "usedBy": [ "homework-take-view.tsx" ] }, { "name": "gradeHomeworkSubmissionAction", "permission": "HOMEWORK_GRADE", "signature": "同上", "purpose": "教师批改作业", "deps": [ "requirePermission", "data-access-write.gradeHomeworkSubmission" ], "usedBy": [ "homework-grading-view.tsx" ] }, { "name": "batchAutoGradeSubmissionsAction", "permission": "HOMEWORK_GRADE", "signature": "(prevState: ActionState<{ successCount: number; failedCount: number; fullyGradedCount: number }> | null, formData: FormData) => Promise>", "purpose": "V3-7 新增:批量自动批改提交。教师在提交列表页勾选多份提交后一键自动批改所有客观题(选择题/判断题),主观题保持原分数。非管理员仅可批改自己创建的作业提交(通过 getHomeworkSubmissionForGrading 校验 creatorId===ctx.userId)。", "deps": [ "requirePermission", "shared/lib/parse.safeJsonParse", "data-access-write.getHomeworkSubmissionForGrading", "data-access-write.batchAutoGradeSubmissions", "shared/lib/track-event.trackExamEvent", "revalidatePath" ], "usedBy": [ "homework-batch-grading-view.tsx" ] }, { "name": "getScansAction", "permission": "HOMEWORK_GRADE | HOMEWORK_SUBMIT", "signature": "(submissionId: string) => Promise>", "purpose": "2026-06-24 新增:获取某次提交的所有答题扫描图。扫描图存储在 fileAttachments 表中 targetType=\"homework\"/targetId=submissionId。支持两类访问者:教师 HOMEWORK_GRADE(仅可访问自己创建的作业的提交,通过 getHomeworkSubmissionForGrading 校验 creatorId===ctx.userId 或 dataScope.type===\"all\")/ 学生 HOMEWORK_SUBMIT(仅可访问自己的提交,通过 getHomeworkSubmissionForPermission 校验 studentId===ctx.userId)。按 createdAt asc 排序,page 从 1 开始递增。✅ 2026-06-25 审计:DB 查询下沉至 data-access-scans.ts::getScansBySubmissionId。", "deps": [ "requirePermission", "shared/lib/action-utils.handleActionError", "data-access-write.getHomeworkSubmissionForGrading", "data-access-write.getHomeworkSubmissionForPermission", "data-access-scans.getScansBySubmissionId" ], "usedBy": [ "homework/components/homework-scan-grading-view.tsx" ] }, { "name": "deleteScanAction", "permission": "HOMEWORK_SUBMIT", "signature": "(submissionId: string, fileId: string) => Promise>", "purpose": "2026-06-24 新增:删除答题扫描图。仅允许提交者本人删除(通过 getHomeworkSubmissionForPermission 校验 studentId===ctx.userId),且仅在提交状态为 started 时允许(已提交/已批改的提交锁定不可修改)。调用 files/data-access.deleteFileAttachment 删除 DB 记录(磁盘文件由 files 模块处理)。", "deps": [ "requirePermission", "shared/lib/action-utils.handleActionError", "data-access-write.getHomeworkSubmissionForPermission", "files/data-access.deleteFileAttachment" ], "usedBy": [ "homework/components/scan-uploader.tsx" ] }, { "name": "getExcellentSubmissionsAction", "permission": "HOMEWORK_GRADE", "signature": "(params: { assignmentId: string; minPercentage?: number; limit?: number }) => Promise>", "purpose": "P3-1 新增:查询某次作业的优秀提交列表 Server Action。HOMEWORK_GRADE 权限,自动按 ctx.dataScope 过滤,避免越权访问。调用 data-access.getExcellentSubmissions 查询,调用前埋点 homework.excellent_viewed 事件(含 count/minPercentage 属性)。", "deps": [ "requirePermission", "shared/lib/action-utils.handleActionError", "data-access.getExcellentSubmissions", "shared/lib/track-event.trackExamEvent" ], "usedBy": [ "homework/components/excellent-submissions.ExcellentSubmissions" ] }, { "name": "remindUnsubmittedAction", "permission": "HOMEWORK_GRADE", "signature": "(params: { assignmentId: string }) => Promise>", "purpose": "P3-2 新增:催交提醒 Server Action。HOMEWORK_GRADE 权限,按 ctx.dataScope 过滤学生。流程:查询未提交学生 → 获取作业标题与截止时间 → 通过动态 import 调用 notifications/data-access.createNotification 为每个学生创建 type=homework/priority=high 的站内通知(link 跳转作业页)→ Promise.allSettled 容错统计成功/失败数 → 埋点 homework.remind_unsubmitted 事件。返回 remindedCount(成功数)。", "deps": [ "requirePermission", "shared/lib/action-utils.handleActionError", "data-access.getUnsubmittedStudents", "data-access.getHomeworkAssignmentById", "notifications/data-access.createNotification (动态 import)", "shared/lib/track-event.trackExamEvent" ], "usedBy": [ "teacher作业详情页催交按钮" ] } ], "dataAccess": [ { "name": "getHomeworkAssignments", "signature": "(params?: { creatorId?: string; ids?: string[]; classId?: string; scope?: DataScope }) => Promise", "usedBy": [ "teacher作业列表页", "homework-assignment-form.tsx" ] }, { "name": "getHomeworkAssignmentReviewList", "signature": "(params: { creatorId: string; scope?: DataScope }) => Promise", "usedBy": [ "teacher批改列表" ] }, { "name": "getHomeworkSubmissions", "signature": "(params?: { assignmentId?: string; classId?: string; creatorId?: string; scope?: DataScope }) => Promise", "usedBy": [ "teacher提交列表" ] }, { "name": "getStudentHomeworkAssignments", "signature": "(studentId: string) => Promise", "usedBy": [ "student/dashboard" ] }, { "name": "getHomeworkAssignmentById", "signature": "(id: string, scope?: DataScope) => Promise<...>", "purpose": "按ID获取作业详情", "usedBy": [ "homework详情页" ] }, { "name": "getHomeworkSubmissionDetails", "signature": "(submissionId: string) => Promise", "purpose": "获取提交详情(含答案)", "usedBy": [ "homework-grading-view.tsx" ] }, { "name": "getDemoStudentUser", "signature": "() => Promise<{ id: string; name: string } | null>", "purpose": "获取演示学生用户(已迁移至 users 模块 getCurrentStudentUser,此处为 re-export 向后兼容)", "usedBy": [ "homework内部" ] }, { "name": "getStudentHomeworkTakeData", "signature": "(assignmentId: string, studentId: string) => Promise", "purpose": "获取学生作答数据", "usedBy": [ "homework-take-view.tsx" ] }, { "name": "getHomeworkAssignmentsByExamId", "signature": "(examId: string) => Promise>", "purpose": "V3-8 新增:按考试 ID 查询关联作业列表(含目标/已提交/已批改学生计数,3 个聚合查询并行执行)。供 exams/stats-service.getExamAnalytics 跨模块调用。", "deps": [ "shared.db", "shared.db.schema.homeworkAssignments", "shared.db.schema.homeworkAssignmentTargets", "shared.db.schema.homeworkSubmissions", "drizzle-orm.eq/inArray/and/count/sql", "react.cache" ], "usedBy": [ "exams/stats-service.getExamAnalytics" ] }, { "name": "getGradedSubmissionsByExamId", "signature": "(examId: string) => Promise }>>", "purpose": "V3-8 新增:按考试 ID 查询所有已批改提交(按学生去重,保留最新一条;含答案列表)。供 exams/stats-service.getExamAnalytics 跨模块调用。", "deps": [ "shared.db", "shared.db.schema.homeworkAssignments", "shared.db.schema.homeworkSubmissions", "drizzle-orm.eq/inArray/and", "react.cache" ], "usedBy": [ "exams/stats-service.getExamAnalytics" ] }, { "name": "getStudentSubmissionResult", "signature": "(assignmentId: string, studentId: string) => Promise", "purpose": "V3-9 新增:获取学生在指定作业的最新提交结果(用于提交后反馈页)。查找学生最近一次已提交/已批改的 submission,返回完整详情含答案(复用 getHomeworkSubmissionDetails)。", "deps": [ "shared.db", "shared.db.schema.homeworkSubmissions", "drizzle-orm.eq/and/inArray/desc", "data-access.getHomeworkSubmissionDetails", "react.cache" ], "usedBy": [ "student/learning/assignments/[assignmentId]/result/page.tsx" ] }, { "name": "getStudentExamResults", "signature": "(studentId: string) => Promise>", "purpose": "V3-11 新增:获取学生的考试结果列表(供家长端展示)。查找学生所有已批改的、关联到考试的作业提交,按 examId 去重,limit 50。供 parent/data-access.getChildDashboardData 跨模块调用。", "deps": [ "shared.db", "shared.db.schema.homeworkSubmissions", "drizzle-orm.eq/and/desc", "react.cache" ], "usedBy": [ "parent/data-access.getChildDashboardData" ] }, { "name": "getExcellentSubmissions", "signature": "(params: { assignmentId: string; minPercentage?: number; limit?: number; scope?: DataScope }) => Promise", "purpose": "P3-1 新增:查询某次作业的优秀提交列表。按得分率(score/maxScore*100)过滤,minPercentage 默认 80,limit 默认 10(上限 50)。同一学生取最高分,按得分率降序返回。支持 scope 行级过滤(class_taught 按 classIds 过滤学生、owned 按 creatorId 过滤作业),grade_managed/all 依赖 assignment 维度。maxScore 来自 homeworkAssignmentQuestions.score 之和。", "deps": [ "shared.db", "shared.db.schema.homeworkSubmissions", "shared.db.schema.homeworkAssignments", "shared.db.schema.homeworkAssignmentQuestions", "drizzle-orm.eq/and/inArray/sql", "classes/data-access.getStudentIdsByClassIds", "react.cache" ], "usedBy": [ "homework/actions.getExcellentSubmissionsAction", "homework/components/excellent-submissions.ExcellentSubmissions" ] }, { "name": "getUnsubmittedStudents", "signature": "(params: { assignmentId: string; scope?: DataScope }) => Promise>", "purpose": "P3-2 新增:查询某次作业未提交的学生列表。对比 homeworkAssignmentTargets 目标学生与 homeworkSubmissions 已提交(status=submitted/graded)学生集合,返回差集(未提交学生)。studentName 取 users.name,缺失时回退 'Unknown'。供 remindUnsubmittedAction 催交提醒使用。", "deps": [ "shared.db", "shared.db.schema.homeworkAssignmentTargets", "shared.db.schema.homeworkSubmissions", "drizzle-orm.eq/and/inArray", "react.cache" ], "usedBy": [ "homework/actions.remindUnsubmittedAction" ] } ], "dataAccessScans": [ { "name": "getScansBySubmissionId", "file": "data-access-scans.ts", "signature": "(submissionId: string) => Promise", "purpose": "2026-06-25 审计新增:从 actions.ts 下沉的扫描图查询。查询 fileAttachments 表中 targetType=\"homework\"/targetId=submissionId 的记录,按 createdAt asc 排序,page 从 1 开始递增。", "deps": [ "shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.eq/and/asc" ], "usedBy": [ "homework/actions.getScansAction" ] } ], "dataAccessClasses": [ { "name": "getAssignmentIdsForStudents", "file": "data-access-classes.ts", "signature": "(studentIds: string[]) => Promise", "purpose": "返回目标学生涉及的作业ID列表(P0-7 新增,供 classes 模块跨模块调用)", "usedBy": [ "classes/data-access-stats.getClassHomeworkInsights", "classes/data-access-stats.getGradeHomeworkInsights", "classes/data-access-students.getStudentsSubjectScores" ] }, { "name": "getHomeworkAssignmentsWithSubject", "file": "data-access-classes.ts", "signature": "(params: { assignmentIds, subjectIdFilter?, limit? }) => Promise", "purpose": "返回作业(含科目信息,via source exam),可选按科目过滤(P0-7 新增)", "usedBy": [ "classes/data-access-stats.getClassHomeworkInsights" ] }, { "name": "getHomeworkAssignmentsByIds", "file": "data-access-classes.ts", "signature": "(params: { assignmentIds, limit? }) => Promise", "purpose": "按 ID 返回作业简要信息(不含科目,P0-7 新增)", "usedBy": [ "classes/data-access-stats.getGradeHomeworkInsights" ] }, { "name": "getAssignmentTargetCounts", "file": "data-access-classes.ts", "signature": "(params: { assignmentIds, studentIds }) => Promise>", "purpose": "返回每个作业在指定学生中的目标计数(P0-7 新增)", "usedBy": [ "classes/data-access-stats.getClassHomeworkInsights", "classes/data-access-stats.getGradeHomeworkInsights" ] }, { "name": "getHomeworkSubmissionsForStudents", "file": "data-access-classes.ts", "signature": "(params: { assignmentIds, studentIds }) => Promise", "purpose": "返回指定作业和学生的提交记录(按 createdAt desc,P0-7 新增)", "usedBy": [ "classes/data-access-stats.getClassHomeworkInsights", "classes/data-access-stats.getGradeHomeworkInsights" ] }, { "name": "getPublishedHomeworkAssignmentsWithSubject", "file": "data-access-classes.ts", "signature": "(params: { assignmentIds }) => Promise", "purpose": "返回已发布作业(含科目信息,via source exam,P0-7 新增)", "usedBy": [ "classes/data-access-students.getStudentsSubjectScores" ] }, { "name": "getHomeworkSubmissionsForAssignments", "file": "data-access-classes.ts", "signature": "(assignmentIds: string[]) => Promise", "purpose": "返回指定作业的提交记录(按 createdAt desc,P0-7 新增)", "usedBy": [ "classes/data-access-students.getStudentsSubjectScores" ] } ], "dataAccessWrite": [ { "name": "createHomeworkAssignment", "file": "data-access-write.ts", "signature": "(input: { examId, classId, title, description, dueAt, allowLate?, lateDueAt?, maxAttempts? }) => Promise<{ assignmentId: string }>", "purpose": "创建作业(P1-2 新增,从 actions 下沉)", "usedBy": [ "createHomeworkAssignmentAction" ] }, { "name": "startHomeworkSubmission", "file": "data-access-write.ts", "signature": "(assignmentId: string, studentId: string) => Promise<{ submissionId: string }>", "purpose": "学生开始作答(P1-2 新增,从 actions 下沉)", "usedBy": [ "startHomeworkSubmissionAction" ] }, { "name": "saveHomeworkAnswer", "file": "data-access-write.ts", "signature": "(submissionId: string, questionId: string, answer: string) => Promise", "purpose": "保存单题答案(P1-2 新增,从 actions 下沉)", "usedBy": [ "saveHomeworkAnswerAction" ] }, { "name": "submitHomework", "file": "data-access-write.ts", "signature": "(submissionId: string, studentId: string) => Promise", "purpose": "提交作业(P1-2 新增,从 actions 下沉)", "usedBy": [ "submitHomeworkAction" ] }, { "name": "gradeHomeworkSubmission", "file": "data-access-write.ts", "signature": "(submissionId: string, grades: Array<{ answerId: string; isCorrect: boolean; score: number; feedback?: string }>) => Promise", "purpose": "教师批改作业(P1-2 新增,从 actions 下沉)", "usedBy": [ "gradeHomeworkSubmissionAction" ] }, { "name": "batchAutoGradeSubmissions", "file": "data-access-write.ts", "signature": "(submissionIds: string[]) => Promise>", "purpose": "V3-7 新增:对多个提交执行自动批改(复用 autoGradeSubmission 逻辑)。仅批改客观题(选择题/判断题),主观题保持原分数。返回每个提交的批改结果摘要。", "deps": [ "shared.db", "shared.db.schema.homeworkSubmissions", "shared.db.schema.homeworkAnswers", "shared.db.schema.homeworkAssignmentQuestions", "drizzle-orm.eq" ], "usedBy": [ "batchAutoGradeSubmissionsAction" ] } ], "statsService": [ { "name": "getTeacherGradeTrends", "file": "stats-service.ts", "signature": "(teacherId: string, limit?: number) => Promise", "purpose": "教师仪表盘年级趋势数据", "deps": [ "data-access.getAssignmentMaxScoreById" ], "usedBy": [ "dashboard (教师仪表盘)" ], "reExportedFrom": "data-access.ts (向后兼容)" }, { "name": "getHomeworkAssignmentAnalytics", "file": "stats-service.ts", "signature": "(assignmentId: string) => Promise", "purpose": "作业整体分析(含题目错误率/错答样本)", "deps": [ "shared/lib/type-guards.isRecord", "data-access.toQuestionContent" ], "usedBy": [ "homework错误分析组件" ], "reExportedFrom": "data-access.ts (向后兼容)" }, { "name": "getStudentDashboardGrades", "file": "stats-service.ts", "signature": "(studentId: string) => Promise", "purpose": "学生仪表盘成绩(趋势/近期/班级排名)", "deps": [ "data-access.getAssignmentMaxScoreById" ], "usedBy": [ "dashboard/data-access.ts" ], "reExportedFrom": "data-access.ts (向后兼容)" }, { "name": "getHomeworkDashboardStats", "file": "stats-service.ts", "signature": "(scope?: DataScope) => Promise", "purpose": "获取作业仪表盘统计数据(作业数/已发布数/提交数/待批改数,支持数据范围过滤)", "usedBy": [ "dashboard/data-access.getAdminDashboardData" ], "reExportedFrom": "data-access.ts (向后兼容)" } ], "schema": [ { "name": "CreateHomeworkAssignmentSchema", "type": "const", "description": "zod schema 创建作业", "usedBy": [ "createHomeworkAssignmentAction", "homework-assignment-form.tsx" ] }, { "name": "CreateHomeworkAssignmentInput", "type": "type", "definition": "z.infer", "usedBy": [ "createHomeworkAssignmentAction" ] }, { "name": "GradeHomeworkSchema", "type": "const", "description": "zod schema 批改作业", "usedBy": [ "gradeHomeworkSubmissionAction", "homework-grading-view.tsx" ] } ], "types": [ { "name": "StudentDashboardGradeProps", "definition": "{ trend, recent, ranking }", "usedBy": [ "dashboard/types.ts" ] }, { "name": "HomeworkAssignmentListItem", "usedBy": [ "homework列表页" ] }, { "name": "StudentHomeworkTakeData", "usedBy": [ "homework-take-view.tsx" ] }, { "name": "HomeworkAssignmentStatus", "type": "type", "definition": "作业状态枚举", "usedBy": [ "homework/components", "homework/data-access" ] }, { "name": "HomeworkSubmissionStatus", "type": "type", "definition": "提交状态枚举", "usedBy": [ "homework/components", "homework/data-access" ] }, { "name": "TeacherGradeTrendItem", "type": "type", "definition": "教师年级趋势项", "usedBy": [ "dashboard (教师仪表盘)" ] }, { "name": "HomeworkAssignmentReviewListItem", "type": "type", "definition": "批改列表项", "usedBy": [ "teacher批改列表" ] }, { "name": "HomeworkSubmissionListItem", "type": "type", "definition": "提交列表项", "usedBy": [ "teacher提交列表" ] }, { "name": "HomeworkQuestionContent", "type": "type", "definition": "作业题目内容", "usedBy": [ "homework/components" ] }, { "name": "HomeworkSubmissionAnswerDetails", "type": "type", "definition": "提交答案详情", "usedBy": [ "homework-grading-view.tsx" ] }, { "name": "HomeworkSubmissionDetails", "type": "type", "definition": "提交详情(含答案列表)", "usedBy": [ "homework-grading-view.tsx" ] }, { "name": "StudentHomeworkProgressStatus", "type": "type", "definition": "学生作业进度状态", "usedBy": [ "student/dashboard" ] }, { "name": "StudentHomeworkAssignmentListItem", "type": "type", "definition": "学生作业列表项", "usedBy": [ "student/dashboard" ] }, { "name": "StudentHomeworkPerformanceItem", "type": "type", "definition": "学生表现项", "usedBy": [ "student/dashboard" ] }, { "name": "StudentHomeworkPerformanceSummary", "type": "type", "definition": "学生表现汇总", "usedBy": [ "student/dashboard" ] }, { "name": "StudentHomeworkTakeQuestion", "type": "type", "definition": "学生作答题目", "usedBy": [ "homework-take-view.tsx" ] }, { "name": "HomeworkAssignmentQuestionAnalytics", "type": "type", "definition": "作业题目分析", "usedBy": [ "homework错误分析组件" ] }, { "name": "HomeworkAssignmentAnalytics", "type": "type", "definition": "作业整体分析", "usedBy": [ "homework错误分析组件" ] }, { "name": "StudentHomeworkScoreAnalytics", "type": "type", "definition": "学生成绩分析", "usedBy": [ "student/dashboard" ] }, { "name": "StudentRanking", "type": "type", "definition": "学生排名", "usedBy": [ "student/dashboard" ] }, { "name": "ScanAttachment", "type": "interface", "file": "types.ts", "definition": "{ fileId: string; url: string; filename: string; originalName: string; page: number }", "purpose": "2026-06-24 新增;✅ 2026-06-25 审计迁移:从 actions.ts 迁入 types.ts(消除内联类型定义)。扫描图附件结构(由 getScansAction 返回)。page 为页码(按 createdAt asc 排序,从 1 开始递增)。", "usedBy": [ "homework/actions.getScansAction", "homework/components/homework-scan-grading-view.tsx" ] }, { "name": "ScanImage", "type": "interface", "file": "components/scan-uploader.tsx", "definition": "{ fileId: string; url: string; filename: string; originalName?: string; page: number }", "purpose": "2026-06-24 新增:扫描图 UI 结构(由 ScanUploader 导出,供 ScanImageViewer 复用)。与 ScanAttachment 字段一致,但 originalName 可选。", "usedBy": [ "homework/components/scan-uploader.tsx", "homework/components/scan-image-viewer.tsx", "homework/components/homework-scan-grading-view.tsx" ] }, { "name": "ExcellentSubmissionItem", "type": "interface", "file": "types.ts", "definition": "{ submissionId: string; assignmentId: string; assignmentTitle: string; studentName: string; totalScore: number; maxScore: number; percentage: number; submittedAt: string; isLate: boolean }", "purpose": "P3-1 新增:优秀作业展示项结构。percentage 为得分率(0-100,保留 1 位小数);isLate 标注迟交但质量优秀的样例;totalScore/maxScore 用于展示实际分数。由 getExcellentSubmissions 返回,供 ExcellentSubmissions 组件渲染。", "usedBy": [ "homework/data-access.getExcellentSubmissions", "homework/actions.getExcellentSubmissionsAction", "homework/components/excellent-submissions.ExcellentSubmissions" ] }, { "name": "ExcellentSubmissionQuery", "type": "interface", "file": "types.ts", "definition": "{ assignmentId: string; minPercentage?: number; limit?: number }", "purpose": "P3-1 新增:优秀作业展示查询参数。assignmentId 必填(按作业维度展示);minPercentage 最低得分百分比阈值(默认 80);limit 返回数量上限(默认 10)。", "usedBy": [ "homework/data-access.getExcellentSubmissions" ] } ], "components": [ { "name": "AssignmentFilters", "file": "assignment-filters.tsx", "purpose": "作业列表筛选器(use client)。基于 nuqs 的 URL 状态管理,提供搜索框(q)和状态下拉(status: all/pending/submitted/graded)。", "deps": [ "nuqs.useQueryState", "nuqs.parseAsString", "next-intl.useTranslations", "shared/components/ui/select", "shared/components/ui/filter-bar" ] }, { "name": "HomeworkAssignmentExamContentCard", "file": "homework-assignment-exam-content-card.tsx", "purpose": "作业考试内容卡片", "deps": [ "next-intl.useTranslations" ] }, { "name": "HomeworkAssignmentExamErrorExplorerLazy", "file": "homework-assignment-exam-error-explorer-lazy.tsx", "purpose": "作业错误分析(懒加载)", "deps": [ "next-intl.useTranslations" ] }, { "name": "HomeworkAssignmentExamErrorExplorer", "file": "homework-assignment-exam-error-explorer.tsx", "purpose": "作业错误分析探索器" }, { "name": "HomeworkAssignmentExamPreviewPane", "file": "homework-assignment-exam-preview-pane.tsx", "purpose": "作业考试预览面板", "deps": [ "next-intl.useTranslations" ] }, { "name": "HomeworkAssignmentForm", "file": "homework-assignment-form.tsx", "purpose": "作业创建表单" }, { "name": "HomeworkAssignmentQuestionErrorDetailPanel", "file": "homework-assignment-question-error-detail-panel.tsx", "purpose": "题目错误详情面板", "deps": [ "next-intl.useTranslations" ] }, { "name": "HomeworkAssignmentQuestionErrorOverviewCard", "file": "homework-assignment-question-error-overview-card.tsx", "purpose": "题目错误概览卡片", "deps": [ "next-intl.useTranslations" ] }, { "name": "HomeworkGradingView", "file": "homework-grading-view.tsx", "purpose": "作业批改视图" }, { "name": "HomeworkTakeView", "file": "homework-take-view.tsx", "purpose": "学生作答视图(✅ 2026-06-25 重构:从 520 行拆分为 3 个组件,主容器 396 行)" }, { "name": "HomeworkTakeSidebar", "file": "homework-take-sidebar.tsx", "purpose": "2026-06-25 新增:从 homework-take-view 拆出的侧边栏组件。展示自动保存状态、作业信息、截止时间、尝试次数、描述、进度网格(题目作答状态点击跳转)、提交按钮。", "usedBy": [ "homework/components/homework-take-view.HomeworkTakeView" ] }, { "name": "HomeworkTakeConfirmDialog", "file": "homework-take-confirm-dialog.tsx", "purpose": "2026-06-25 新增:从 homework-take-view 拆出的提交确认对话框。展示未作答题目数量警告,支持继续提交/继续作答。", "usedBy": [ "homework/components/homework-take-view.HomeworkTakeView" ] }, { "name": "HomeworkReviewView", "file": "student-homework-review-view.tsx", "purpose": "学生作业复习视图" }, { "name": "HomeworkBatchGradingView", "file": "homework-batch-grading-view.tsx", "purpose": "V3-7 新增:批量批改视图(use client)。教师在提交列表页可勾选多份提交,一键自动批改所有客观题。含全选/反选、 selectable 过滤(仅 status='submitted' 可选)、useTransition 异步提交、toast 反馈、revalidate 刷新。对标智学网批量批改功能。" }, { "name": "HomeworkSubmissionResult", "file": "homework-submission-result.tsx", "purpose": "V3-9 新增:提交后即时反馈页。学生提交后立即看到分数汇总(总分/满分、得分率 Progress)、对错分布(正确/错误/部分正确/待批改)、错题预览(题目文本、学生答案、正确答案)。对标智学网/猿题库提交后反馈。" }, { "name": "ScanUploader", "file": "scan-uploader.tsx", "purpose": "2026-06-24 新增:学生扫描图上传组件(use client)。学生在纸上作答后按页拍摄上传,调用 /api/upload 上传图片(targetType=\"homework\"/targetId=submissionId),返回 fileId+url。上传后将 fileId 列表通过 onChange 暴露给父组件。支持增删/排序,onDeleteScan 回调用于删除 fileAttachments 记录(调用 deleteScanAction)。已提交(disabled)时禁止操作。导出 ScanImage 类型供 ScanImageViewer 复用。", "deps": [ "shared/components/ui/button", "shared/lib/utils.cn", "next-intl.useTranslations", "sonner.toast", "lucide-react (Upload/X/Loader2/ImageIcon)" ], "usedBy": [ "homework/components/homework-scan-grading-view.tsx" ] }, { "name": "ScanImageViewer", "file": "scan-image-viewer.tsx", "purpose": "2026-06-24 新增:扫描图查看器(use client)。用于阅卷式批改时查看学生答题图片。支持翻页(上一页/下一页+页码)、缩放(ZoomIn/ZoomOut,0.25 步进,1-3 范围)、旋转(RotateCw,90° 步进)、全屏(Maximize2)。切换页面时重置缩放与旋转。", "deps": [ "shared/components/ui/button", "shared/lib/utils.cn", "lucide-react (ChevronLeft/ChevronRight/ZoomIn/ZoomOut/Maximize2/RotateCw)", "homework/components/scan-uploader.ScanImage", "next-intl.useTranslations" ], "usedBy": [ "homework/components/homework-scan-grading-view.tsx" ] }, { "name": "HomeworkScanGradingView", "file": "homework-scan-grading-view.tsx", "purpose": "2026-06-24 新增:教师阅卷式批改视图(use client)。使用 ResizablePanel 左侧题目+批改表单、右侧 ScanImageViewer 扫描图查看器。集成 getScansAction 拉取扫描图、gradeHomeworkSubmissionAction 批改。支持上一份/下一份提交快速切换(prevSubmissionId/nextSubmissionId)。显示学生姓名、作业标题、提交时间、状态、总分。左侧每题显示题干、学生答案、得分输入、反馈输入。", "deps": [ "shared/components/ui/button", "shared/components/ui/card", "shared/components/ui/input", "shared/components/ui/label", "shared/components/ui/textarea", "shared/components/ui/badge", "shared/components/ui/scroll-area", "shared/components/ui/resizable-panel.ResizablePanel", "shared/lib/utils.formatDate", "shared/lib/utils.cn", "next-intl.useTranslations", "sonner.toast", "homework/actions.gradeHomeworkSubmissionAction", "homework/actions.getScansAction", "homework/components/question-renderer.QuestionRenderer", "homework/components/scan-image-viewer.ScanImageViewer", "lucide-react (Save/ChevronLeft/ChevronRight/User/Clock)" ], "usedBy": [ "app/(dashboard)/teacher/homework/submissions/[submissionId]/scan-grading/page.tsx" ] }, { "name": "ExcellentSubmissions", "file": "excellent-submissions.tsx", "purpose": "P3-1 新增:优秀作业展示 async 服务端组件。在作业详情页向学生展示班级优秀样例,激发学习动力。按得分率降序排列,前三名分别用金/银/铜徽章(Trophy/Medal/Award)标识,迟交样例用 Clock 图标标注。仅展示姓名不展示学号,保护学生隐私。直接调用 data-access.getExcellentSubmissions(避免多余 action 往返),通过 getTranslations('examHomework') 服务端 i18n。", "deps": [ "shared/components/ui/card", "shared/components/ui/badge", "shared/components/ui/skeleton", "shared/components/ui/empty-state", "shared/lib/utils.cn", "shared/lib/utils.formatDate", "next/link", "next-intl/server.getTranslations", "lucide-react (Award/Trophy/Medal/Clock/ChevronRight)", "homework/data-access.getExcellentSubmissions" ], "usedBy": [ "作业详情页" ] }, { "name": "ExcellentSubmissionsWithBoundary", "file": "excellent-submissions.tsx", "purpose": "P3-1 新增:优秀作业展示组件的带边界包裹版本。使用 SectionErrorBoundary + Suspense 包裹 ExcellentSubmissions,提供加载骨架屏(Skeleton)与局部错误降级,避免本区块故障影响整个作业详情页渲染。", "deps": [ "shared/components/section-error-boundary.SectionErrorBoundary", "shared/components/ui/skeleton", "homework/components/excellent-submissions.ExcellentSubmissions", "react.Suspense" ], "usedBy": [ "作业详情页" ] } ], "hooks": [ { "name": "useExamCountdown", "file": "hooks/use-exam-countdown.ts", "signature": "(options: { durationMinutes: number | null, startedAt: string | null, onExpire?: () => void, enabled?: boolean }) => ExamCountdownState | null", "purpose": "P0-竞品新增:考试倒计时 Hook(对标智学网/猿题库)。每秒更新剩余时间,剩余≤5分钟标记紧急状态,到时触发 onExpire 回调自动提交。setInterval + setState 仅在 interval 回调中异步调用,符合 react-hooks/purity 与 set-state-in-effect 规则。", "usedBy": [ "homework/components/homework-take-view.HomeworkTakeView" ] } ] } }, "questions": { "path": "src/modules/questions", "description": "题库管理:题目CRUD、知识点关联、多题型支持、教材→章节→知识点级联筛选、批量操作、导入导出", "exports": { "actions": [ { "name": "createQuestionAction", "permission": "QUESTION_CREATE", "signature": "(prevState: ActionState | undefined, formData: FormData | CreateQuestionInput) => Promise>", "purpose": "创建题目(含嵌套)", "deps": [ "requirePermission", "data-access.createQuestionWithRelations", "utils/track-event.trackQuestionCreated" ], "usedBy": [ "create-question-dialog.tsx" ] }, { "name": "updateQuestionAction", "permission": "QUESTION_UPDATE", "signature": "(prevState, formData) => Promise>", "purpose": "更新题目", "deps": [ "requirePermission", "data-access.updateQuestionById", "utils/track-event.trackQuestionUpdated" ], "usedBy": [ "question-actions.tsx" ] }, { "name": "deleteQuestionAction", "permission": "QUESTION_DELETE", "signature": "(prevState, formData) => Promise>", "purpose": "删除题目", "deps": [ "requirePermission", "data-access.deleteQuestionByIdRecursive", "utils/track-event.trackQuestionDeleted" ], "usedBy": [ "question-actions.tsx" ] }, { "name": "getQuestionsAction", "permission": "QUESTION_READ", "signature": "(params: GetQuestionsParams) => Promise<...>", "purpose": "查询题目列表", "deps": [ "requirePermission", "data-access.getQuestions" ], "usedBy": [ "teacher/questions/page.tsx", "admin/questions/page.tsx" ] }, { "name": "getKnowledgePointOptionsAction", "permission": "QUESTION_READ", "signature": "() => Promise", "purpose": "获取知识点选项", "deps": [ "requirePermission", "data-access.getKnowledgePointOptions" ], "usedBy": [ "knowledge-point-selector.tsx" ] }, { "name": "getTextbookOptionsAction", "permission": "QUESTION_READ", "signature": "() => Promise", "purpose": "获取教材选项列表(级联筛选第一级)", "deps": [ "requirePermission", "data-access.getTextbookOptions" ], "usedBy": [ "question-cascade-filter.tsx" ] }, { "name": "getChapterOptionsAction", "permission": "QUESTION_READ", "signature": "(textbookId: string) => Promise", "purpose": "获取指定教材下的章节选项列表(级联筛选第二级)", "deps": [ "requirePermission", "data-access.getChapterOptions" ], "usedBy": [ "question-cascade-filter.tsx" ] }, { "name": "getKnowledgePointOptionsByChapterAction", "permission": "QUESTION_READ", "signature": "(chapterId: string) => Promise<{ id: string; name: string }[]>", "purpose": "获取指定章节下的知识点选项列表(级联筛选第三级)", "deps": [ "requirePermission", "data-access.getKnowledgePointOptionsByChapter" ], "usedBy": [ "question-cascade-filter.tsx" ] }, { "name": "deleteQuestionsBatchAction", "permission": "QUESTION_DELETE", "signature": "(prevState, formData) => Promise>", "purpose": "批量删除题目(事务原子性,权限范围过滤)", "deps": [ "requirePermission", "data-access.deleteQuestionsBatch", "utils/track-event.trackQuestionDeleted" ], "usedBy": [ "batch-operations.tsx" ] }, { "name": "exportQuestionsAction", "permission": "QUESTION_READ", "signature": "(formData: FormData) => Promise>", "purpose": "导出题目为 JSON 格式(权限范围过滤)", "deps": [ "requirePermission", "data-access.exportQuestions" ], "usedBy": [ "import-export-buttons.tsx" ] }, { "name": "importQuestionsAction", "permission": "QUESTION_CREATE", "signature": "(prevState, formData) => Promise>", "purpose": "批量导入题目(事务原子性)", "deps": [ "requirePermission", "data-access.importQuestions", "utils/track-event.trackQuestionCreated" ], "usedBy": [ "import-export-buttons.tsx" ] } ], "dataAccess": [ { "name": "getQuestions", "signature": "(params?: GetQuestionsParams) => Promise<{ data: Question[], meta: { page, pageSize, total, totalPages } }>", "type": "cache function", "purpose": "查询题目列表(缓存)", "usedBy": [ "getQuestionsAction", "teacher/questions/page.tsx" ] }, { "name": "getQuestionsDashboardStats", "signature": "() => Promise", "purpose": "获取题目仪表盘统计数据(题目总数)", "usedBy": [ "dashboard/data-access.getAdminDashboardData" ] }, { "name": "createQuestionWithRelations", "signature": "(input: CreateQuestionInput, authorId: string) => Promise", "purpose": "创建题目(含嵌套子题目、知识点关联)(P1-2 新增,从 actions 下沉)", "usedBy": [ "createQuestionAction", "importQuestions" ] }, { "name": "updateQuestionById", "signature": "(questionId: string, input: Partial) => Promise", "purpose": "按ID更新题目(P1-2 新增,从 actions 下沉)", "usedBy": [ "updateQuestionAction" ] }, { "name": "deleteQuestionByIdRecursive", "signature": "(questionId: string) => Promise", "purpose": "递归删除题目(含子题目、知识点关联)(P1-2 新增,从 actions 下沉)", "usedBy": [ "deleteQuestionAction" ] }, { "name": "getKnowledgePointOptions", "signature": "() => Promise", "purpose": "获取知识点选项(含章节/教材信息)(P1-2 新增,从 actions 下沉;委托 textbooks data-access)", "usedBy": [ "getKnowledgePointOptionsAction" ] }, { "name": "getTextbookOptions", "signature": "() => Promise", "purpose": "获取教材选项列表(级联筛选第一级,委托 textbooks data-access)", "usedBy": [ "getTextbookOptionsAction" ] }, { "name": "getChapterOptions", "signature": "(textbookId: string) => Promise", "purpose": "获取指定教材下的章节选项列表(级联筛选第二级,委托 textbooks data-access)", "usedBy": [ "getChapterOptionsAction" ] }, { "name": "getKnowledgePointOptionsByChapter", "signature": "(chapterId: string) => Promise<{ id: string; name: string }[]>", "purpose": "获取指定章节下的知识点选项列表(级联筛选第三级,委托 textbooks data-access)", "usedBy": [ "getKnowledgePointOptionsByChapterAction" ] }, { "name": "deleteQuestionsBatch", "signature": "(questionIds: string[], canDeleteAll: boolean, authorId: string) => Promise", "purpose": "批量删除题目(事务原子性,权限范围过滤)", "usedBy": [ "deleteQuestionsBatchAction" ] }, { "name": "exportQuestions", "signature": "(questionIds?: string[], canExportAll?: boolean, authorId?: string) => Promise", "purpose": "导出题目为结构化 JSON 格式(权限范围过滤)", "usedBy": [ "exportQuestionsAction" ] }, { "name": "importQuestions", "signature": "(items: QuestionImportItem[], authorId: string) => Promise", "purpose": "批量导入题目(事务原子性)", "usedBy": [ "importQuestionsAction" ] }, { "name": "GetQuestionsParams", "type": "type", "definition": "{ q?, page?, pageSize?, ids?, knowledgePointId?, textbookId?, chapterId?, type?, difficulty? }", "usedBy": [ "getQuestions", "getQuestionsAction" ] } ], "schema": [ { "name": "QuestionTypeEnum", "type": "const", "description": "zod enum: z.enum([\"single_choice\", \"multiple_choice\", \"text\", \"judgment\", \"composite\"])", "usedBy": [ "CreateQuestionSchema", "questions/components" ] }, { "name": "BaseQuestionSchema", "type": "const", "description": "zod schema 基础题目校验", "usedBy": [ "CreateQuestionSchema" ] }, { "name": "CreateQuestionInput", "type": "type", "definition": "z.infer", "usedBy": [ "createQuestionAction", "create-question-dialog.tsx" ] }, { "name": "CreateQuestionSchema", "type": "const", "description": "zod schema 创建题目(递归支持嵌套)", "usedBy": [ "createQuestionAction", "create-question-dialog.tsx" ] } ], "types": [ { "name": "Question", "definition": "{ id, content, type, difficulty, createdAt, updatedAt, author, knowledgePoints, childrenCount? }", "usedBy": [ "exams (题目选择)", "homework (作业题目)" ] }, { "name": "KnowledgePointOption", "definition": "{ id, name, chapterId, chapterTitle, textbookId, textbookTitle, subject, grade }", "usedBy": [ "knowledge-point-selector.tsx" ] }, { "name": "TextbookOption", "definition": "{ id, title, subject, grade }", "usedBy": [ "question-cascade-filter.tsx" ] }, { "name": "ChapterOption", "definition": "{ id, title, parentId, depth }", "usedBy": [ "question-cascade-filter.tsx" ] }, { "name": "QuestionContent", "definition": "{ text, options?, answer?, explanation? }", "usedBy": [ "question-content-renderer.tsx", "utils/parse-content.ts" ] }, { "name": "QuestionOption", "definition": "{ id, text, isCorrect }", "usedBy": [ "question-content-renderer.tsx", "utils/parse-content.ts" ] }, { "name": "QuestionType", "type": "type", "definition": "z.infer", "usedBy": [ "questions/components", "exams/components" ] }, { "name": "QUESTION_TYPE_I18N_KEY", "type": "const", "definition": "Record", "usedBy": [ "questions/components" ] }, { "name": "DIFFICULTY_I18N_KEY", "type": "const", "definition": "Record", "usedBy": [ "questions/components" ] }, { "name": "QuestionExportItem", "definition": "{ id, type, difficulty, content, knowledgePoints, createdAt, updatedAt }", "usedBy": [ "exportQuestionsAction" ] }, { "name": "QuestionImportItem", "definition": "{ type, difficulty, content, knowledgePointIds? }", "usedBy": [ "importQuestionsAction" ] } ], "components": [ { "name": "CreateQuestionButton", "file": "create-question-button.tsx", "purpose": "创建题目按钮(权限感知)" }, { "name": "CreateQuestionDialog", "file": "create-question-dialog.tsx", "purpose": "创建/编辑题目对话框(组合 KnowledgePointSelector + OptionsEditor)" }, { "name": "QuestionActions", "file": "question-actions.tsx", "purpose": "题目行操作(权限感知 + 类型守卫 + QuestionContentRenderer 结构化预览)" }, { "name": "useQuestionColumns", "file": "question-columns.tsx", "type": "Hook", "purpose": "题目表格列定义 Hook(因 useTranslations 是 Hook)" }, { "name": "QuestionDataTable", "file": "question-data-table.tsx", "purpose": "题目数据表格(i18n + 集成 BatchOperations + ✅ Phase 4.3:@tanstack/react-virtual 虚拟滚动替换分页)" }, { "name": "QuestionFilters", "file": "question-filters.tsx", "purpose": "题目筛选栏(组合 QuestionCascadeFilter)" }, { "name": "QuestionCascadeFilter", "file": "question-cascade-filter.tsx", "purpose": "三级级联筛选:教材→章节→知识点(nuqs URL 状态同步)" }, { "name": "KnowledgePointSelector", "file": "knowledge-point-selector.tsx", "purpose": "知识点选择器(支持搜索过滤,从 create-question-dialog 拆分)" }, { "name": "OptionsEditor", "file": "options-editor.tsx", "purpose": "选项编辑器(单选/多选模式,从 create-question-dialog 拆分)" }, { "name": "QuestionBankResultsClient", "file": "question-bank-results-client.tsx", "purpose": "RSC 与客户端 Hook 桥接(解决 useQuestionColumns 在客户端使用)" }, { "name": "QuestionContentRenderer", "file": "question-content-renderer.tsx", "purpose": "题目结构化渲染(题干/选项/答案/解析,单选圆点/多选方框,正确答案高亮)" }, { "name": "BatchOperations", "file": "batch-operations.tsx", "purpose": "批量操作工具栏(批量删除,权限感知,事务原子性)" }, { "name": "ImportExportButtons", "file": "import-export-buttons.tsx", "purpose": "导入导出组件(JSON 格式,导出下载/上传导入,导入前预览确认)" } ] } }, "textbooks": { "path": "src/modules/textbooks", "description": "教材与知识体系:教材/章节树形结构、知识点CRUD、Markdown内容编辑、知识图谱", "exports": { "actions": [ { "name": "createTextbookAction", "permission": "TEXTBOOK_CREATE", "signature": "(prevState, formData) => Promise", "purpose": "创建教材" }, { "name": "updateTextbookAction", "permission": "TEXTBOOK_UPDATE", "signature": "(textbookId, prevState, formData) => Promise", "purpose": "更新教材元信息" }, { "name": "deleteTextbookAction", "permission": "TEXTBOOK_DELETE", "signature": "(textbookId) => Promise", "purpose": "删除教材" }, { "name": "createChapterAction", "permission": "TEXTBOOK_CREATE", "signature": "(textbookId, parentId?, prevState, formData) => Promise", "purpose": "创建章节" }, { "name": "updateChapterContentAction", "permission": "TEXTBOOK_UPDATE", "signature": "(chapterId, content, textbookId) => Promise", "purpose": "更新章节内容(Markdown)" }, { "name": "deleteChapterAction", "permission": "TEXTBOOK_DELETE", "signature": "(chapterId, textbookId) => Promise", "purpose": "删除章节" }, { "name": "createKnowledgePointAction", "permission": "TEXTBOOK_CREATE", "signature": "(chapterId, textbookId, prevState, formData) => Promise", "purpose": "创建知识点" }, { "name": "updateKnowledgePointAction", "permission": "TEXTBOOK_UPDATE", "signature": "(kpId, textbookId, prevState, formData) => Promise", "purpose": "更新知识点" }, { "name": "deleteKnowledgePointAction", "permission": "TEXTBOOK_DELETE", "signature": "(kpId, textbookId) => Promise", "purpose": "删除知识点" }, { "name": "reorderChaptersAction", "permission": "TEXTBOOK_UPDATE", "signature": "(chapterId, newIndex, parentId, textbookId) => Promise", "purpose": "章节排序" }, { "name": "getKnowledgePointsByChapterAction", "permission": "TEXTBOOK_READ", "signature": "(chapterId, textbookId) => Promise>", "purpose": "获取章节下的知识点列表(含资源归属校验)" }, { "name": "getKnowledgeGraphDataAction", "permission": "TEXTBOOK_READ", "signature": "(textbookId, viewMode: GraphViewMode) => Promise>", "purpose": "Task 7 新增:获取知识图谱数据,支持 structure/student-mastery/class-mastery 三种视图模式" }, { "name": "createPrerequisiteAction", "permission": "TEXTBOOK_UPDATE", "signature": "(formData) => Promise", "purpose": "Task 7 新增:声明前置依赖(含循环检测 + 归属校验)" }, { "name": "deletePrerequisiteAction", "permission": "TEXTBOOK_UPDATE", "signature": "(formData) => Promise", "purpose": "Task 7 新增:删除前置依赖" } ], "dataAccess": [ { "name": "getTextbooks", "signature": "(query?, subject?, grade?) => Promise", "usedBy": [ "teacher/textbooks/page.tsx", "student/learning/textbooks/page.tsx" ] }, { "name": "getTextbookById", "signature": "(id) => Promise", "usedBy": [ "teacher/textbooks/[id]/page.tsx", "student/learning/textbooks/[id]/page.tsx" ] }, { "name": "getChaptersByTextbookId", "signature": "(textbookId) => Promise", "usedBy": [ "textbook-reader.tsx" ] }, { "name": "getKnowledgePointsByChapterId", "signature": "(chapterId) => Promise", "usedBy": [ "textbook-reader.tsx" ] }, { "name": "getKnowledgePointsByTextbookId", "signature": "(textbookId) => Promise", "usedBy": [ "textbook-reader.tsx" ] }, { "name": "createTextbook", "signature": "(input: CreateTextbookInput) => Promise", "purpose": "创建教材", "usedBy": [ "createTextbookAction" ] }, { "name": "updateTextbook", "signature": "(id: string, input: UpdateTextbookInput) => Promise", "purpose": "更新教材", "usedBy": [ "updateTextbookAction" ] }, { "name": "deleteTextbook", "signature": "(id: string) => Promise", "purpose": "删除教材", "usedBy": [ "deleteTextbookAction" ] }, { "name": "createChapter", "signature": "(input: CreateChapterInput) => Promise", "purpose": "创建章节", "usedBy": [ "createChapterAction" ] }, { "name": "updateChapterContent", "signature": "(input: UpdateChapterContentInput) => Promise", "purpose": "更新章节内容", "usedBy": [ "updateChapterContentAction" ] }, { "name": "deleteChapter", "signature": "(chapterId: string, textbookId: string) => Promise", "purpose": "删除章节", "usedBy": [ "deleteChapterAction" ] }, { "name": "createKnowledgePoint", "signature": "(input: CreateKnowledgePointInput) => Promise", "purpose": "创建知识点", "usedBy": [ "createKnowledgePointAction" ] }, { "name": "updateKnowledgePoint", "signature": "(input: UpdateKnowledgePointInput) => Promise", "purpose": "更新知识点", "usedBy": [ "updateKnowledgePointAction" ] }, { "name": "deleteKnowledgePoint", "signature": "(kpId: string, textbookId: string) => Promise", "purpose": "删除知识点", "usedBy": [ "deleteKnowledgePointAction" ] }, { "name": "reorderChapters", "signature": "(chapterId: string, newIndex: number, parentId: string | null, textbookId: string) => Promise", "purpose": "章节排序", "usedBy": [ "reorderChaptersAction" ] }, { "name": "getTextbooksDashboardStats", "signature": "() => Promise", "purpose": "获取教材仪表盘统计数据(教材总数、章节总数)", "usedBy": [ "dashboard/data-access.getAdminDashboardData" ] }, { "name": "getKnowledgePointOptions", "signature": "() => Promise", "purpose": "跨模块接口:获取所有知识点选项(含章节/教材信息),供 questions 模块调用", "usedBy": [ "questions/data-access.getKnowledgePointOptions" ] }, { "name": "getTextbooksWithScope", "signature": "(query?, subject?, grade?, scope?: TextbookQueryScope) => Promise", "purpose": "P1-1 新增:按数据范围获取教材列表,学生端强制按年级过滤", "usedBy": [ "student/learning/textbooks/page.tsx" ] }, { "name": "verifyChapterBelongsToTextbook", "signature": "(chapterId, textbookId) => Promise", "purpose": "P0-4 新增:资源归属校验,防止跨教材越权操作章节", "usedBy": [ "reorderChaptersAction", "updateChapterContentAction", "deleteChapterAction", "createKnowledgePointAction" ] }, { "name": "verifyKnowledgePointBelongsToTextbook", "signature": "(kpId, textbookId) => Promise", "purpose": "P0-4 新增:资源归属校验,防止跨教材越权操作知识点", "usedBy": [ "updateKnowledgePointAction", "deleteKnowledgePointAction", "createPrerequisiteAction" ] }, { "name": "createPrerequisite", "signature": "(input: CreatePrerequisiteInput) => Promise", "purpose": "Task 7 新增:创建知识点前置依赖", "usedBy": [ "createPrerequisiteAction" ] }, { "name": "deletePrerequisite", "signature": "(input: DeletePrerequisiteInput) => Promise", "purpose": "Task 7 新增:删除知识点前置依赖", "usedBy": [ "deletePrerequisiteAction" ] }, { "name": "getPrerequisiteEdgesForTextbook", "signature": "(textbookId) => Promise>", "purpose": "Task 7 新增:获取教材下所有前置依赖边列表,用于循环检测", "usedBy": [ "createPrerequisiteAction" ] }, { "name": "getSubjectLabelKey", "signature": "(subject: string) => string", "purpose": "获取学科的 i18n 标签键", "usedBy": [ "textbooks/components/textbook-filters.tsx", "textbooks/components/textbook-card.tsx" ] }, { "name": "getGradeLabelKey", "signature": "(grade: string) => string", "purpose": "获取年级的 i18n 标签键", "usedBy": [ "textbooks/components/textbook-filters.tsx", "textbooks/components/textbook-card.tsx" ] } ], "hooks": [ { "name": "useTextSelection", "file": "hooks/use-text-selection.ts", "signature": "() => { selectedText, setSelectedText, selectionRef, contentRef, setCreateDialogOpen, setIsCreating, createDialogOpen, isCreating, handleContentPointerDown, handleContextMenuChange }", "purpose": "文本选区Hook(无参数)", "usedBy": [ "textbook-content-panel.tsx" ] }, { "name": "useKpDialogState", "file": "hooks/use-kp-dialog-state.ts", "signature": "() => { editingKp, setEditingKp, editKpDialogOpen, setEditKpDialogOpen, isUpdatingKp, setIsUpdatingKp, questionDialogOpen, setQuestionDialogOpen, targetKpForQuestion, setTargetKpForQuestion, deleteConfirmOpen, setDeleteConfirmOpen, pendingDeleteKpId, setPendingDeleteKpId }", "purpose": "知识点对话框状态管理 Hook(编辑/题目/删除确认),从 use-knowledge-point-actions 拆分", "usedBy": [ "hooks/use-knowledge-point-actions.ts" ] }, { "name": "useKpCrud", "file": "hooks/use-kp-crud.ts", "signature": "(args: UseKpCrudArgs) => { handleCreateKnowledgePoint, requestDeleteKnowledgePoint, confirmDeleteKnowledgePoint, handleUpdateKnowledgePoint }", "purpose": "知识点 CRUD 操作 Hook,依赖 useKpDialogState 提供的状态,从 use-knowledge-point-actions 拆分", "usedBy": [ "hooks/use-knowledge-point-actions.ts" ] }, { "name": "useKnowledgePointActions", "file": "hooks/use-knowledge-point-actions.ts", "signature": "(textbookId, selectedChapterId, selectedChapterTextbookId, highlightedKpId, setHighlightedKpId, onKpCreated?) => { editingKp, setEditingKp, editKpDialogOpen, setEditKpDialogOpen, isUpdatingKp, questionDialogOpen, setQuestionDialogOpen, targetKpForQuestion, setTargetKpForQuestion, deleteConfirmOpen, setDeleteConfirmOpen, handleCreateKnowledgePoint, requestDeleteKnowledgePoint, confirmDeleteKnowledgePoint, handleUpdateKnowledgePoint }", "purpose": "知识点操作门面 Hook(组合 useKpDialogState + useKpCrud,对外保持原有 API 不变)", "usedBy": [ "textbook-reader.tsx" ] }, { "name": "useGraphData", "file": "hooks/use-graph-data.ts", "signature": "(textbookId: string, viewMode: GraphViewMode) => { data: KnowledgeGraphData | null, isLoading: boolean, isRefreshing: boolean, error: string | null, reload: () => void }", "purpose": "Task 11 新增:知识图谱数据加载 Hook,按 textbookId + viewMode 加载,使用派生值模式避免 effect 中 setState。区分 isLoading(首次加载)和 isRefreshing(切换模式刷新,保留旧数据避免 UI 闪烁)", "usedBy": [ "components/knowledge-graph.tsx" ] } ], "types": [ { "name": "Chapter", "definition": "{ id, textbookId, title, order, parentId, content?, children? }", "usedBy": [ "textbooks/components", "questions (知识点关联)" ] }, { "name": "ChapterTreeNode", "definition": "Chapter & { children: ChapterTreeNode[] }", "purpose": "P1-5 新增:buildChapterTree 返回类型,强制 children 为非空数组", "usedBy": [ "textbooks/utils.buildChapterTree", "textbooks/components/chapter-sidebar-list.tsx" ] }, { "name": "KnowledgePoint", "definition": "{ id, name, description?, anchorText?, parentId?, chapterId?, level, order }", "usedBy": [ "textbooks/components", "questions/types" ] }, { "name": "Textbook", "type": "type", "definition": "{ id, title, subject, grade, publisher, createdAt, updatedAt }", "usedBy": [ "textbooks/components", "textbooks/data-access" ] }, { "name": "CreateTextbookInput", "type": "type", "definition": "创建教材输入", "usedBy": [ "createTextbook", "createTextbookAction" ] }, { "name": "UpdateTextbookInput", "type": "type", "definition": "更新教材输入", "usedBy": [ "updateTextbook", "updateTextbookAction" ] }, { "name": "CreateChapterInput", "type": "type", "definition": "创建章节输入", "usedBy": [ "createChapter", "createChapterAction" ] }, { "name": "UpdateChapterContentInput", "type": "type", "definition": "更新章节内容输入", "usedBy": [ "updateChapterContent", "updateChapterContentAction" ] }, { "name": "CreateKnowledgePointInput", "type": "type", "definition": "创建知识点输入", "usedBy": [ "createKnowledgePoint", "createKnowledgePointAction" ] }, { "name": "UpdateKnowledgePointInput", "type": "type", "definition": "更新知识点输入", "usedBy": [ "updateKnowledgePoint", "updateKnowledgePointAction" ] }, { "name": "GraphViewMode", "type": "type", "definition": "\"structure\" | \"student-mastery\" | \"class-mastery\"", "purpose": "Task 7 新增:图谱数据视角(结构 / 学生掌握度 / 班级掌握度)", "usedBy": [ "getKnowledgeGraphDataAction" ] }, { "name": "GraphLayoutMode", "type": "type", "definition": "\"hierarchical\" | \"force\"", "purpose": "v4 新增:图谱渲染布局(hierarchical=分层有向图 ReactFlow+dagre;force=Obsidian 风格力导向图 react-force-graph-2d)", "usedBy": [ "components/knowledge-graph.tsx", "components/graph-toolbar.tsx" ] }, { "name": "MasteryInfo", "type": "interface", "definition": "{ masteryLevel, totalQuestions, correctQuestions, lastAssessedAt }", "purpose": "Task 7 新增:掌握度信息", "usedBy": [ "getKnowledgeGraphDataAction", "data-access-graph.getStudentKpMastery", "data-access-graph.getClassKpMastery" ] }, { "name": "KpWithRelations", "type": "interface", "definition": "{ id, name, description, parentId, chapterId, level, order, questionCount, prerequisiteIds, chapterTitle }", "purpose": "Task 7 新增:带关联关系的知识点(图谱数据)", "usedBy": [ "data-access-graph.getKnowledgePointsWithRelations", "getKnowledgeGraphDataAction" ] }, { "name": "KnowledgeGraphData", "type": "interface", "definition": "{ knowledgePoints: KpWithRelations[], masteryMap: Record, viewMode: GraphViewMode }", "purpose": "Task 7 新增:图谱完整数据(Server Action 返回)", "usedBy": [ "getKnowledgeGraphDataAction" ] }, { "name": "GraphNodeData", "type": "interface", "definition": "{ kp: KpWithRelations, mastery: MasteryInfo | null, viewMode: GraphViewMode, isSelected: boolean, isHighlighted: boolean, chapterColor: string }", "purpose": "Task 9 新增:React Flow 自定义节点数据(传递给 graph-kp-node 组件)", "usedBy": [ "components/graph-kp-node.tsx", "components/knowledge-graph.tsx" ] }, { "name": "GraphEdgeData", "type": "interface", "definition": "{ edgeType: 'parent' | 'prerequisite', isHighlighted: boolean }", "purpose": "Task 9 新增:React Flow 自定义边数据", "usedBy": [ "components/graph-prerequisite-edge.tsx", "components/knowledge-graph.tsx" ] }, { "name": "MasteryLevel", "type": "type", "definition": "'low' | 'medium' | 'high' | 'unassessed'", "purpose": "Task 9 新增:掌握度等级(红/黄/绿/灰)", "usedBy": [ "components/graph-kp-node.tsx" ] }, { "name": "CreatePrerequisiteInput", "type": "type", "definition": "{ knowledgePointId, prerequisiteKpId }", "purpose": "Task 7 新增:创建前置依赖输入", "usedBy": [ "createPrerequisite", "createPrerequisiteAction" ] }, { "name": "DeletePrerequisiteInput", "type": "type", "definition": "{ knowledgePointId, prerequisiteKpId }", "purpose": "Task 7 新增:删除前置依赖输入", "usedBy": [ "deletePrerequisite", "deletePrerequisiteAction" ] } ], "constants": [ { "name": "SUBJECTS", "file": "constants.ts", "purpose": "学科常量数组(含 Chinese/Mathematics/Physics/Chemistry/Biology/English/History/Geography)" }, { "name": "GRADES", "file": "constants.ts", "purpose": "年级常量数组(含 Grade 1/Grade 2/Grade 7-12)" }, { "name": "SUBJECT_COLORS", "file": "constants.ts", "purpose": "学科颜色映射" }, { "name": "getSubjectColor", "file": "constants.ts", "signature": "(subject: string) => string", "purpose": "获取学科对应颜色" }, { "name": "getSubjectLabelKey", "file": "constants.ts", "signature": "(subject: string) => string", "purpose": "获取学科 i18n 标签键" }, { "name": "getGradeLabelKey", "file": "constants.ts", "signature": "(grade: string) => string", "purpose": "获取年级 i18n 标签键" } ], "utils": [ { "name": "sortChapters", "file": "utils.ts", "purpose": "章节排序" }, { "name": "buildChapterTree", "file": "utils.ts", "purpose": "构建章节树(返回 ChapterTreeNode[])" }, { "name": "buildChapterIndex", "file": "utils.ts", "purpose": "构建章节索引 Map" }, { "name": "findChapterParent", "file": "utils.ts", "purpose": "查找章节父节点" }, { "name": "filterKnowledgePointsByChapter", "file": "utils.ts", "purpose": "按章节过滤知识点" }, { "name": "normalizeOptional", "file": "utils.ts", "purpose": "可选字段归一化" }, { "name": "highlightKnowledgePoints", "file": "utils.ts", "purpose": "高亮知识点" }, { "name": "hasCycleAfterAddingEdge", "file": "utils.ts", "purpose": "检测添加有向边后是否形成环(DFS)" }, { "name": "findChapterById", "file": "utils.ts", "purpose": "V3 续审计新增:递归查找章节树中指定 ID 的章节(替代各模块本地重复实现)" } ], "graphLayout": [ { "name": "computeGraphLayout", "file": "graph-layout.ts", "purpose": "知识图谱布局计算纯函数(Task 8:dagre 集成,输入 KpWithRelations[],输出 React Flow Node[]/Edge[] + 画布尺寸)" } ], "analytics": [ { "name": "TextbookAnalytics", "file": "analytics.tsx", "type": "context", "purpose": "教材分析 Context" }, { "name": "TextbookAnalyticsProvider", "file": "analytics.tsx", "type": "component", "purpose": "教材分析 Provider" }, { "name": "useTextbookAnalytics", "file": "analytics.tsx", "type": "hook", "purpose": "教材分析 Hook" } ], "components": [ { "name": "ChapterSidebarList", "purpose": "章节侧边栏列表" }, { "name": "CreateChapterDialog", "purpose": "创建章节对话框" }, { "name": "KnowledgeGraph", "purpose": "知识图谱可视化(v4 新增:支持 hierarchical/force 两种布局模式切换,hierarchical=ReactFlow+dagre 分层有向图,force=Obsidian 风格力导向图;外层共享状态/详情面板/前置依赖增删对话框)" }, { "name": "ForceKnowledgeGraph", "file": "components/force-graph.tsx", "purpose": "v4 新增:Obsidian 风格力导向图谱组件,基于 react-force-graph-2d + d3-force;圆形节点+曲线连接+物理力导向+邻居高亮+拖拽联动;通过 next/dynamic ssr:false 加载避免 SSR canvas/window 报错" }, { "name": "GraphToolbar", "file": "components/graph-toolbar.tsx", "purpose": "图谱工具栏(v4 新增 hierarchical/force 布局模式切换 Select;保留视图模式切换/搜索/重置视图)" }, { "name": "GraphNodeDetailPanel", "purpose": "知识图谱节点详情侧边面板(Task 13 新增:展示知识点名称/描述/掌握度/关联题目/前置后置知识点,支持跳转与前置关系增删)" }, { "name": "KnowledgePointDialogs", "purpose": "知识点对话框集合" }, { "name": "KnowledgePointList", "purpose": "知识点列表" }, { "name": "SectionErrorBoundary", "purpose": "章节内容错误边界(class component + i18n + router.refresh 重试)" }, { "name": "TextbookCard", "purpose": "教材卡片" }, { "name": "TextbookContentPanel", "purpose": "教材内容面板" }, { "name": "TextbookFilters", "purpose": "教材筛选器" }, { "name": "TextbookFormDialog", "purpose": "教材表单对话框" }, { "name": "TextbookReader", "purpose": "教材阅读器" }, { "name": "TextbookSettingsDialog", "purpose": "教材设置对话框" } ], "uiDeps": [], "uiDepsNote": "已通过 render prop 解耦,TeacherTextbookReader 已移至 app 层(src/app/(dashboard)/teacher/textbooks/[id]/_components/),textbooks 模块不再直接 import questions 模块组件", "npmDeps": [ "@xyflow/react", "@dagrejs/dagre", "react-force-graph-2d" ], "npmDepsNote": "v4 新增 react-force-graph-2d(基于 d3-force + Canvas)实现 Obsidian 风格力导向图谱;通过 next/dynamic ssr:false 加载避免 SSR canvas/window 报错;@xyflow/react + @dagrejs/dagre 用于分层有向图布局", "knownIssues": [ "v4 新增(2026-07-03):知识点图谱新增 Obsidian 风格力导向布局(force 模式),与原分层有向图(hierarchical 模式)共存,工具栏可切换;新增组件 force-graph.tsx,新增类型 GraphLayoutMode;保留全部现有功能(掌握度视图/搜索高亮/邻居高亮/前置依赖增删)", "i18n 覆盖率约 99%(v3 新增 notFound/noClassMasteryPermission/close/prerequisiteAddFailed/prerequisiteRemoveFailed 等键,data-access 层硬编码英文异常文案已改为 NotFoundError 错误码化)", "v3 审计修复(2026-06-24):学生端两个页面添加 requirePermission(Permissions.TEXTBOOK_READ) 显式权限校验", "v3 审计修复(2026-06-24):getKnowledgeGraphDataAction 添加学生/家长年级 scope 过滤,class-mastery 视图模式限制为 teacher/admin", "v3 审计修复(2026-06-24):deleteChapter 级联清理 knowledgePointPrerequisites 孤儿记录,保证数据完整性", "v3 审计修复(2026-06-24):knowledge-graph handleAddPrerequisite/handleRemovePrerequisite 添加 try/catch/finally,避免加载状态卡死", "v3 审计修复(2026-06-24):textbook-reader onCreateKnowledgePoint 添加 try/finally,移除已废弃 canEdit prop", "v3 审计修复(2026-06-24):section-error-boundary 添加 componentDidCatch 错误日志,role=alert + aria-live=assertive", "v3 审计修复(2026-06-24):抽取 shared/components/route-error.tsx 通用 RouteError 组件,4 个 error.tsx 文件去重", "v3 审计修复(2026-06-24):4 个 loading.tsx 添加 role=status/aria-busy/aria-label,[id]/loading.tsx 重写匹配 reader 布局", "v3 审计修复(2026-06-24):chapter-sidebar-list 章节项添加 role=button/tabIndex/onKeyDown 键盘导航", "v3 审计修复(2026-06-24):graph-node-detail-panel 关闭/移除前置按钮添加 aria-label", "v3 审计修复(2026-06-24):graph-kp-node 用类型守卫 extractNodeData 替代 as unknown as 双重断言", "v3 审计修复(2026-06-24):knowledge-point-dialogs 用 void 包装替代 as 断言", "v3 审计修复(2026-06-24):schema.ts 移除死代码 ReorderChaptersSchema/ReorderChaptersInput", "v3 审计修复(2026-06-24):data-access.ts 移除死代码 verifyKnowledgePointBelongsToChapter,修复 as [string, string] 断言", "v3 审计修复(2026-06-24):textbook-settings-dialog 新增 redirectAfterDelete prop 消除硬编码 URL", "v3 审计修复(2026-06-24):textbook-card 添加注释说明默认 hrefBase", "v3 审计修复(2026-06-24):graph-layout GraphLayoutNodeData 接口新增 graphData? 字段", "v3 P2 修复(2026-06-24):data-access 层 3 处硬编码英文异常文案改为 NotFoundError(教材/章节),消息可安全返回客户端", "v3 P2 修复(2026-06-24):graph-layout.ts O(n²) some() 查找改为 Set 查找,性能降为 O(n)", "v3 P2 修复(2026-06-24):抽取 textbook-form-fields.tsx 共享组件,消除 textbook-form-dialog 与 textbook-settings-dialog 表单字段重复", "v3 P2 修复(2026-06-24):textbook-content-panel.tsx 17 个 props 按 chapter/editing/selection/highlight 四组重构,提升可读性", "v3 P2 修复(2026-06-24):textbook-reader.tsx 移除命令式 classList 操作,视觉高亮改为 isHighlighted 状态驱动,仅保留 scrollIntoView", "v3 P2 修复(2026-06-24):use-kp-crud.ts(122 行)拆分为 use-kp-create/use-kp-delete/use-kp-update 三个子 Hook + 门面,均在 80 行限制内", "v1 测试已修复:textbook-reader.tsx SheetTrigger 越界、Server→Client 函数 prop 序列化、seed 数据 i18n key 不匹配", "v2 核查修复:class-mastery 视图模式已实现、跨教材前置依赖双向 IN 过滤、ChapterSidebarList canEdit 默认 false、isTeacher 冗余变量移除、graph-kp-node 节点宽度常量化、GraphNodeDetailPanel textbookId 未使用 prop 移除、use-knowledge-point-actions 拆分为门面+状态+CRUD 三 Hook、use-graph-data 区分 isLoading/isRefreshing 避免 UI 闪烁、TeacherTextbookReader 移至 app 层解耦跨模块依赖", "⚠️ 待办 P2:data-access 函数未导出非缓存版本,单测难以 mock", "⚠️ 待办 P2:图谱方向键导航未实现", "⚠️ 待办 P3:admin/parent 视图、版本管理、PDF 导出、阅读进度跟踪等长期功能未实现" ], "files": { "actions.ts": 502, "data-access.ts": 593, "data-access-graph.ts": 188, "types.ts": 94, "schema.ts": 56, "constants.ts": 91, "utils.ts": 226, "graph-layout.ts": 109, "graph-layout.test.ts": 116, "utils.test.ts": 246, "analytics.tsx": 37, "hooks/use-knowledge-point-actions.ts": 45, "hooks/use-kp-dialog-state.ts": 33, "hooks/use-kp-crud.ts": 52, "hooks/use-kp-create.ts": 43, "hooks/use-kp-delete.ts": 49, "hooks/use-kp-update.ts": 36, "hooks/use-text-selection.ts": 49, "hooks/use-graph-data.ts": 65, "components/chapter-sidebar-list.tsx": 325, "components/create-chapter-dialog.tsx": 105, "components/graph-kp-node.tsx": 92, "components/graph-node-detail-panel.tsx": 171, "components/graph-prerequisite-edge.tsx": 43, "components/graph-toolbar.tsx": 121, "components/force-graph.tsx": 454, "components/knowledge-graph.tsx": 408, "components/knowledge-point-dialogs.tsx": 167, "components/knowledge-point-list.tsx": 117, "components/section-error-boundary.tsx": 53, "components/textbook-card.tsx": 172, "components/textbook-content-panel.tsx": 199, "components/textbook-filters.tsx": 66, "components/textbook-form-dialog.tsx": 68, "components/textbook-form-fields.tsx": 103, "components/textbook-reader.tsx": 412, "components/textbook-settings-dialog.tsx": 136 }, "auditReport": "audit/textbooks-audit-report-v3.md" } }, "classes": { "path": "src/modules/classes", "description": "班级管理:班级CRUD、学生注册/退班、邀请码、课表、学科教师分配", "exports": { "actions": [ { "name": "createTeacherClassAction", "permission": "CLASS_CREATE", "signature": "(prevState, formData) => Promise>", "purpose": "教师创建班级" }, { "name": "updateTeacherClassAction", "permission": "CLASS_UPDATE", "signature": "(classId, prevState, formData) => Promise", "purpose": "教师更新班级" }, { "name": "deleteTeacherClassAction", "permission": "CLASS_DELETE", "signature": "(classId) => Promise", "purpose": "教师删除班级" }, { "name": "createGradeClassAction", "permission": "CLASS_CREATE", "signature": "(prevState, formData) => Promise>", "purpose": "年级主任创建班级" }, { "name": "updateGradeClassAction", "permission": "CLASS_UPDATE", "signature": "(classId, prevState, formData) => Promise", "purpose": "年级主任更新班级" }, { "name": "deleteGradeClassAction", "permission": "CLASS_DELETE", "signature": "(classId) => Promise", "purpose": "年级主任删除班级" }, { "name": "enrollStudentByEmailAction", "permission": "CLASS_ENROLL", "signature": "(classId, prevState, formData) => Promise", "purpose": "通过邮箱注册学生" }, { "name": "joinClassByInvitationCodeAction", "permission": "CLASS_ENROLL", "signature": "(prevState, formData) => Promise>", "purpose": "通过邀请码加入" }, { "name": "ensureClassInvitationCodeAction", "permission": "CLASS_ENROLL", "signature": "(classId) => Promise>", "purpose": "确保邀请码存在" }, { "name": "regenerateClassInvitationCodeAction", "permission": "CLASS_ENROLL", "signature": "(classId) => Promise>", "purpose": "重新生成邀请码" }, { "name": "setStudentEnrollmentStatusAction", "permission": "CLASS_ENROLL", "signature": "(classId, studentId, status) => Promise", "purpose": "设置学生状态" }, { "name": "createClassScheduleItemAction", "permission": "CLASS_SCHEDULE", "signature": "(prevState, formData) => Promise>", "purpose": "创建课表项" }, { "name": "updateClassScheduleItemAction", "permission": "CLASS_SCHEDULE", "signature": "(scheduleId, prevState, formData) => Promise", "purpose": "更新课表项" }, { "name": "deleteClassScheduleItemAction", "permission": "CLASS_SCHEDULE", "signature": "(scheduleId) => Promise", "purpose": "删除课表项" }, { "name": "createAdminClassAction", "permission": "CLASS_CREATE", "signature": "(prevState, formData) => Promise>", "purpose": "管理员创建班级" }, { "name": "updateAdminClassAction", "permission": "CLASS_UPDATE", "signature": "(classId, prevState, formData) => Promise", "purpose": "管理员更新班级" }, { "name": "deleteAdminClassAction", "permission": "CLASS_DELETE", "signature": "(classId) => Promise", "purpose": "管理员删除班级" } ], "dataAccess": [ { "name": "getTeacherClasses", "signature": "(params?: { teacherId?: string }) => Promise", "usedBy": [ "teacher/classes/my", "dashboard" ] }, { "name": "getAdminClasses", "signature": "() => Promise", "usedBy": [ "admin班级管理" ] }, { "name": "getGradeManagedClasses", "signature": "(userId) => Promise", "usedBy": [ "grade_head班级管理" ] }, { "name": "getStudentClasses", "signature": "(studentId) => Promise", "usedBy": [ "student/dashboard" ] }, { "name": "getStudentSchedule", "signature": "(studentId) => Promise", "usedBy": [ "student课表" ] }, { "name": "getClassStudents", "signature": "(params?: { classId?, q?, status?, teacherId? }) => Promise", "usedBy": [ "teacher/classes/students" ] }, { "name": "getClassSchedule", "signature": "(params?: { classId?, teacherId? }) => Promise", "usedBy": [ "teacher/classes/schedule" ] }, { "name": "getClassHomeworkInsights", "signature": "(params: { classId, teacherId?, limit? }) => Promise", "usedBy": [ "classes作业洞察" ] }, { "name": "getGradeHomeworkInsights", "signature": "(params: { gradeId, limit? }) => Promise", "usedBy": [ "年级作业洞察" ] }, { "name": "getTeacherIdForMutations", "signature": "() => Promise", "purpose": "获取当前教师ID(用于写操作)", "usedBy": [ "classes写操作内部" ] }, { "name": "getClassSubjects", "signature": "(classId: string) => Promise", "purpose": "获取班级学科列表", "usedBy": [ "class-detail组件" ] }, { "name": "getTeacherOptions", "signature": "() => Promise", "purpose": "获取教师选项列表", "usedBy": [ "class-detail组件" ] }, { "name": "getTeacherTeachingSubjects", "signature": "(teacherId: string) => Promise<...>", "purpose": "获取教师所教学科", "usedBy": [ "classes内部" ] }, { "name": "getManagedGrades", "signature": "(userId: string) => Promise<...>", "purpose": "获取所管年级", "usedBy": [ "grade_head视图" ] }, { "name": "getStudentsSubjectScores", "signature": "(...) => Promise<...>", "purpose": "获取学生学科成绩", "usedBy": [ "classes内部" ] }, { "name": "getClassStudentSubjectScoresV2", "signature": "(...) => Promise<...>", "purpose": "获取班级学生学科成绩V2", "usedBy": [ "classes内部" ] }, { "name": "getClassesDashboardStats", "signature": "() => Promise", "purpose": "获取班级仪表盘统计数据(班级总数)", "usedBy": [ "dashboard/data-access.getAdminDashboardData" ] }, { "name": "createTeacherClass", "signature": "(input) => Promise", "purpose": "教师创建班级", "usedBy": [ "createTeacherClassAction" ] }, { "name": "createAdminClass", "signature": "(input) => Promise", "purpose": "管理员创建班级", "usedBy": [ "createAdminClassAction" ] }, { "name": "ensureClassInvitationCode", "signature": "(classId: string) => Promise<{ code: string }>", "purpose": "确保邀请码存在", "usedBy": [ "ensureClassInvitationCodeAction" ] }, { "name": "regenerateClassInvitationCode", "signature": "(classId: string) => Promise<{ code: string }>", "purpose": "重新生成邀请码", "usedBy": [ "regenerateClassInvitationCodeAction" ] }, { "name": "enrollStudentByInvitationCode", "signature": "(code: string, studentId: string) => Promise<{ classId: string }>", "purpose": "通过邀请码注册学生", "usedBy": [ "joinClassByInvitationCodeAction" ] }, { "name": "enrollTeacherByInvitationCode", "signature": "(code: string, teacherId: string) => Promise<...>", "purpose": "通过邀请码注册教师", "usedBy": [ "classes内部" ] }, { "name": "updateTeacherClass", "signature": "(classId: string, input) => Promise", "purpose": "教师更新班级", "usedBy": [ "updateTeacherClassAction" ] }, { "name": "updateAdminClass", "signature": "(classId: string, input) => Promise", "purpose": "管理员更新班级", "usedBy": [ "updateAdminClassAction" ] }, { "name": "setClassSubjectTeachers", "signature": "(classId: string, assignments: ClassSubjectTeacherAssignment[]) => Promise", "purpose": "设置班级学科教师", "usedBy": [ "classes内部" ] }, { "name": "deleteTeacherClass", "signature": "(classId: string) => Promise", "purpose": "教师删除班级", "usedBy": [ "deleteTeacherClassAction" ] }, { "name": "deleteAdminClass", "signature": "(classId: string) => Promise", "purpose": "管理员删除班级", "usedBy": [ "deleteAdminClassAction" ] }, { "name": "enrollStudentByEmail", "signature": "(classId: string, email: string) => Promise", "purpose": "通过邮箱注册学生", "usedBy": [ "enrollStudentByEmailAction" ] }, { "name": "setStudentEnrollmentStatus", "signature": "(classId: string, studentId: string, status: string) => Promise", "purpose": "设置学生状态", "usedBy": [ "setStudentEnrollmentStatusAction" ] }, { "name": "verifyTeacherOwnsClass", "signature": "(classId: string, teacherId: string) => Promise", "purpose": "校验教师是否拥有班级(P0-5 新增,供 scheduling 模块跨模块校验 classSchedule 写权限)", "usedBy": [ "scheduling/data-access-class-schedule.createClassScheduleItem", "scheduling/data-access-class-schedule.updateClassScheduleItem", "scheduling/data-access-class-schedule.deleteClassScheduleItem" ] }, { "name": "getStudentIdsByClassId", "signature": "(classId: string) => Promise", "purpose": "获取班级所有学生 ID(跨模块接口)", "usedBy": [ "notifications/actions.sendClassNotificationAction" ] }, { "name": "getStudentIdsByClassIds", "signature": "(classIds: string[]) => Promise", "purpose": "获取多个班级的所有学生 ID(跨模块接口)", "usedBy": [ "homework/data-access", "homework/stats-service" ] }, { "name": "getActiveStudentIdsByClassId", "signature": "(classId: string) => Promise", "purpose": "获取班级所有活跃学生 ID(跨模块接口)", "usedBy": [ "homework/data-access", "homework/stats-service", "grades/data-access", "diagnostic/data-access" ] }, { "name": "getTeacherSubjectIdsByClass", "signature": "(classId: string, teacherId: string) => Promise", "purpose": "获取教师在班级所教科目标识(跨模块接口)", "usedBy": [ "homework/data-access-write" ] }, { "name": "getStudentActiveClassId", "signature": "(studentId: string) => Promise", "purpose": "获取学生当前活跃班级 ID(跨模块接口)", "usedBy": [ "homework/stats-service", "grades/data-access-ranking", "parent/data-access" ] }, { "name": "getStudentActiveGradeId", "signature": "(studentId: string) => Promise", "purpose": "获取学生当前活跃班级对应的年级 ID(跨模块接口)", "usedBy": [ "elective/data-access-selections.getStudentGradeId" ] }, { "name": "getClassExists", "signature": "(classId: string) => Promise", "purpose": "校验班级是否存在(跨模块接口)", "usedBy": [ "notifications/actions.sendClassNotificationAction", "grades/data-access", "diagnostic/data-access" ] }, { "name": "getClassNameById", "signature": "(classId: string) => Promise", "purpose": "获取班级名称(跨模块接口)", "usedBy": [ "grades/data-access", "grades/data-access-analytics", "grades/export", "parent/data-access", "diagnostic/data-access" ] }, { "name": "getClassGradeId", "signature": "(classId: string) => Promise", "purpose": "获取班级关联的年级 ID(跨模块接口)", "usedBy": [ "classes/actions.updateGradeClassAction", "classes/actions.deleteGradeClassAction" ] }, { "name": "getGradeIdsByClassIds", "signature": "(classIds: string[]) => Promise", "purpose": "获取多个班级关联的年级 ID 列表(跨模块接口)", "usedBy": [ "homework/stats-service" ] }, { "name": "getClassNamesByIds", "signature": "(classIds: string[]) => Promise>", "purpose": "批量获取班级名称(跨模块接口)", "usedBy": [ "grades/data-access" ] }, { "name": "getClassesByGradeId", "signature": "(gradeId: string) => Promise>", "purpose": "获取指定年级下的所有班级(跨模块接口)", "usedBy": [ "grades/data-access-analytics" ] }, { "name": "getTeacherIdsByClassIds", "signature": "(classIds: string[]) => Promise", "purpose": "获取多个班级的所有教师 ID(班主任 + 任课教师,跨模块接口)", "usedBy": [ "messaging/data-access.getRecipients" ] }, { "name": "getClassIdByScheduleId", "signature": "(scheduleId: string) => Promise", "purpose": "✅ 2026-06-26 审计修复新增:根据课表项 ID 获取其所属班级 ID。供 actions-schedule 的 create/update/delete 三个 Action 做越权校验,防止教师通过 scheduleId 修改他人班级课表。classes 模块对 classSchedule 表有读权限", "usedBy": [ "classes/actions-schedule.assertClassOwnership", "classes/actions-schedule.createClassScheduleItemAction", "classes/actions-schedule.updateClassScheduleItemAction", "classes/actions-schedule.deleteClassScheduleItemAction" ] } ], "schema": [ { "name": "CreateTeacherClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 教师创建班级(name, grade 必填,schoolName/schoolId/gradeId/homeroom/room 可选)", "usedBy": [ "actions.createTeacherClassAction" ] }, { "name": "UpdateTeacherClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 教师更新班级(classId 必填,其余可选)", "usedBy": [ "actions.updateTeacherClassAction" ] }, { "name": "DeleteTeacherClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 教师删除班级(classId)", "usedBy": [ "actions.deleteTeacherClassAction" ] }, { "name": "CreateAdminClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 管理员创建班级(name, grade, teacherId 必填,其余可选)", "usedBy": [ "actions.createAdminClassAction" ] }, { "name": "UpdateAdminClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 管理员更新班级(classId 必填,其余可选)", "usedBy": [ "actions.updateAdminClassAction" ] }, { "name": "DeleteAdminClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 管理员删除班级(classId)", "usedBy": [ "actions.deleteAdminClassAction" ] }, { "name": "CreateGradeClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 年级主任创建班级(name, gradeId, teacherId 必填,其余可选)", "usedBy": [ "actions.createGradeClassAction" ] }, { "name": "UpdateGradeClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 年级主任更新班级(classId 必填,其余可选)", "usedBy": [ "actions.updateGradeClassAction" ] }, { "name": "DeleteGradeClassSchema", "type": "const", "file": "schema.ts", "description": "zod schema 年级主任删除班级(classId)", "usedBy": [ "actions.deleteGradeClassAction" ] }, { "name": "CreateClassScheduleItemSchema", "type": "const", "file": "schema.ts", "description": "zod schema 创建课表项(classId, weekday 1-7, course, startTime, endTime 必填,location 可选)", "usedBy": [ "actions.createClassScheduleItemAction" ] }, { "name": "UpdateClassScheduleItemSchema", "type": "const", "file": "schema.ts", "description": "zod schema 更新课表项(scheduleId 必填,其余可选)", "usedBy": [ "actions.updateClassScheduleItemAction" ] }, { "name": "DeleteClassScheduleItemSchema", "type": "const", "file": "schema.ts", "description": "zod schema 删除课表项(scheduleId)", "usedBy": [ "actions.deleteClassScheduleItemAction" ] }, { "name": "EnrollStudentByEmailSchema", "type": "const", "file": "schema.ts", "description": "zod schema 通过邮箱注册学生(classId, email)", "usedBy": [ "actions.enrollStudentByEmailAction" ] } ], "types": [ { "name": "TeacherClass", "type": "type", "definition": "教师班级对象", "usedBy": [ "getTeacherClasses", "dashboard" ] }, { "name": "AssignmentSummary", "type": "type", "definition": "作业摘要", "usedBy": [ "classes/components" ] }, { "name": "TeacherOption", "type": "type", "definition": "教师选项", "usedBy": [ "getTeacherOptions", "class-detail组件" ] }, { "name": "DEFAULT_CLASS_SUBJECTS", "type": "const", "definition": "默认班级学科列表", "usedBy": [ "classes内部" ] }, { "name": "ClassSubject", "type": "type", "definition": "班级学科", "usedBy": [ "getClassSubjects", "classes/components" ] }, { "name": "ClassSubjectTeacherAssignment", "type": "type", "definition": "班级学科教师分配", "usedBy": [ "setClassSubjectTeachers" ] }, { "name": "AdminClassListItem", "type": "type", "definition": "管理员班级列表项", "usedBy": [ "getAdminClasses", "getGradeManagedClasses" ] }, { "name": "CreateTeacherClassInput", "type": "type", "definition": "教师创建班级输入", "usedBy": [ "createTeacherClass" ] }, { "name": "UpdateTeacherClassInput", "type": "type", "definition": "教师更新班级输入", "usedBy": [ "updateTeacherClass" ] }, { "name": "ClassStudent", "type": "type", "definition": "班级学生", "usedBy": [ "getClassStudents", "classes/components" ] }, { "name": "ClassScheduleItem", "type": "type", "definition": "班级课表项", "usedBy": [ "getClassSchedule", "classes/components" ] }, { "name": "StudentEnrolledClass", "type": "type", "definition": "学生已注册班级", "usedBy": [ "getStudentClasses", "dashboard" ] }, { "name": "StudentScheduleItem", "type": "type", "definition": "学生课表项", "usedBy": [ "getStudentSchedule", "dashboard" ] }, { "name": "ScoreStats", "type": "type", "definition": "成绩统计", "usedBy": [ "classes/components" ] }, { "name": "ClassHomeworkAssignmentStats", "type": "type", "definition": "班级作业统计", "usedBy": [ "classes/components" ] }, { "name": "ClassHomeworkInsights", "type": "type", "definition": "班级作业洞察", "usedBy": [ "getClassHomeworkInsights", "classes/components" ] }, { "name": "GradeHomeworkClassSummary", "type": "type", "definition": "年级作业班级汇总", "usedBy": [ "classes/components" ] }, { "name": "GradeHomeworkInsights", "type": "type", "definition": "年级作业洞察", "usedBy": [ "getGradeHomeworkInsights", "classes/components" ] }, { "name": "ClassFormGrade", "type": "type", "definition": "表单年级选项(统一 admin/grade 年级形状,P1-D 抽取)", "usedBy": [ "classes/components/class-form-utils", "classes/hooks/use-class-filters", "classes/components/class-form-dialog" ] }, { "name": "SubjectTeacherState", "type": "type", "definition": "编辑弹窗科目-教师分配表单状态(P1-D 抽取)", "usedBy": [ "classes/hooks/use-class-data", "classes/components/class-form-dialog" ] } ], "components": [ { "name": "StudentsTable", "purpose": "学生表格" }, { "name": "StudentsFilters", "purpose": "学生筛选器" }, { "name": "ScheduleView", "purpose": "课表视图" }, { "name": "MyClassesGrid", "purpose": "我的班级网格" }, { "name": "AdminClassesClient", "purpose": "管理员班级客户端" }, { "name": "ScheduleFilters", "purpose": "课表筛选器" }, { "name": "GradeClassesClient", "purpose": "年级班级客户端" }, { "name": "transformAssignmentsToChartData", "file": "class-detail/transformAssignmentsToChartData", "type": "function", "purpose": "转换作业为图表数据" }, { "name": "ClassSubmissionTrendChart", "file": "class-detail/ClassSubmissionTrendChart", "purpose": "班级提交趋势图表" }, { "name": "ClassTrendsWidget", "file": "class-detail/ClassTrendsWidget", "purpose": "班级趋势组件" }, { "name": "ClassStudentsWidget", "file": "class-detail/ClassStudentsWidget", "purpose": "班级学生组件" }, { "name": "ClassScheduleGrid", "file": "class-detail/ClassScheduleGrid", "purpose": "班级课表网格" }, { "name": "ClassScheduleWidget", "file": "class-detail/ClassScheduleWidget", "purpose": "班级课表组件" }, { "name": "ClassQuickActions", "file": "class-detail/ClassQuickActions", "purpose": "班级快捷操作" }, { "name": "EditClassDialog", "file": "class-detail/EditClassDialog", "purpose": "编辑班级对话框" }, { "name": "ClassOverviewStats", "file": "class-detail/ClassOverviewStats", "purpose": "班级概览统计" }, { "name": "ClassHeader", "file": "class-detail/ClassHeader", "purpose": "班级头部" }, { "name": "ClassAssignmentsWidget", "file": "class-detail/ClassAssignmentsWidget", "purpose": "班级作业组件" }, { "name": "ClassListTable", "file": "class-list-table", "purpose": "班级列表表格(admin/grade 共享,P1-D 抽取)" }, { "name": "ClassFormDialog", "file": "class-form-dialog", "purpose": "班级创建/编辑表单弹窗(admin/grade 共享,P1-D 抽取;schools 可选控制学校选择显隐)" }, { "name": "ClassDeleteDialog", "file": "class-delete-dialog", "purpose": "班级删除确认弹窗(admin/grade 共享,P1-D 抽取,基于 ConfirmDeleteDialog)" }, { "name": "ClassListToolbar", "file": "class-list-toolbar", "purpose": "班级列表工具栏:新建按钮+计数徽章(admin/grade 共享,P1-D 抽取)" }, { "name": "ClassErrorBoundary", "file": "class-error-boundary", "purpose": "班级模块 Error Boundary(class component + i18n + router.refresh 重试,P1-C 新增)" }, { "name": "ClassListSkeleton", "file": "class-skeleton", "purpose": "班级列表骨架屏(P1-C 新增)" }, { "name": "ClassCardSkeleton", "file": "class-skeleton", "purpose": "班级卡片骨架屏(P1-C 新增)" }, { "name": "ClassGridSkeleton", "file": "class-skeleton", "purpose": "班级网格骨架屏(P1-C 新增)" }, { "name": "StudentListSkeleton", "file": "class-skeleton", "purpose": "学生列表骨架屏(P1-C 新增)" }, { "name": "ScheduleGridSkeleton", "file": "class-skeleton", "purpose": "课表网格骨架屏(P1-C 新增)" } ], "utils": [ { "name": "getSubjectColor", "file": "components/schedule-utils.ts", "signature": "getSubjectColor(subject: string): string", "purpose": "✅ 2026-06-26 审计修复新增:根据科目名称返回对应的颜色类名(中英文双语关键词匹配)。修复原实现仅匹配英文关键词('math'/'english')但 DEFAULT_CLASS_SUBJECTS 为中文(\"数学\"/\"英语\")导致所有中文科目落到 default 分支的颜色分组失效 bug。导出为纯函数便于单测", "usedBy": [ "classes/components/schedule-view.tsx", "classes/components/class-detail/class-schedule-widget.tsx" ] }, { "name": "getPositionStyle", "file": "components/schedule-utils.ts", "signature": "getPositionStyle(startTime: string, endTime: string): { top: string; height: string }", "purpose": "✅ 2026-06-26 审计修复新增:根据课程的开始/结束时间计算课表块在 8:00-18:00 时间轴上的定位样式(top% / height%)", "usedBy": [ "classes/components/schedule-view.tsx", "classes/components/class-detail/class-schedule-widget.tsx" ] }, { "name": "timeToMinutes", "file": "components/schedule-utils.ts", "signature": "timeToMinutes(time: string): number", "purpose": "✅ 2026-06-26 审计修复新增:将 \"HH:MM\" 时间字符串转换为当天从 0 点起的分钟数。供 getPositionStyle 内部使用", "usedBy": [ "classes/components/schedule-utils.getPositionStyle" ] }, { "name": "SCHEDULE_WEEKDAYS", "file": "components/schedule-utils.ts", "signature": "SCHEDULE_WEEKDAYS: string[]", "purpose": "✅ P2 修复新增:课表周常量数组,供 schedule-view 及拆分出的 3 个对话框子组件复用(替换原 schedule-view.tsx 内联的 WEEKDAYS 常量)", "usedBy": [ "classes/components/schedule-view.tsx", "classes/components/schedule-create-dialog.tsx", "classes/components/schedule-edit-dialog.tsx", "classes/components/schedule-delete-dialog.tsx" ] } ], "hooks": [ { "name": "useClassData", "file": "hooks/use-class-data.ts", "signature": "useClassData(config: { defaultTeacherId: string; defaultSchoolId?: string; defaultGradeId?: string }): UseClassDataReturn", "purpose": "班级列表/弹窗共享状态:对话框开关、表单字段、提交中标记、重置逻辑(P1-E 抽取,admin/grade 共享)", "usedBy": [ "classes/components/admin-classes-view", "classes/components/grade-classes-view" ] }, { "name": "useClassFilters", "file": "hooks/use-class-filters.ts", "signature": "useClassFilters(grades: ClassFormGrade[], createSchoolId: string, editSchoolId: string): { createGrades: ClassFormGrade[]; editGrades: ClassFormGrade[] }", "purpose": "按学校筛选年级(admin 模式使用,P1-E 抽取)", "usedBy": [ "classes/components/admin-classes-view" ] } ] }, "files": [ { "path": "data-access.ts", "lines": 548, "description": "核心班级 CRUD + 邀请码 + 教师班级管理(含 re-export 向后兼容)", "exports": [ "getTeacherClasses", "getTeacherOptions", "getTeacherTeachingSubjects", "createTeacherClass", "ensureClassInvitationCode", "regenerateClassInvitationCode", "enrollStudentByInvitationCode", "enrollTeacherByInvitationCode", "updateTeacherClass", "setClassSubjectTeachers", "deleteTeacherClass", "enrollStudentByEmail", "setStudentEnrollmentStatus", "getSessionTeacherId", "getTeacherIdForMutations", "getAccessibleClassIdsForTeacher", "getClassGradeIdsByClassIds", "getTeacherSubjectIdsForClass", "getClassSubjects", "compareClassLike", "isDuplicateInvitationCodeError", "generateUniqueInvitationCode" ] }, { "path": "data-access-stats.ts", "lines": 513, "description": "作业统计查询(班级/年级作业洞察,通过 homework/data-access-classes 获取数据)", "exports": [ "getClassHomeworkInsights", "getGradeHomeworkInsights", "getClassesDashboardStats" ] }, { "path": "data-access-schedule.ts", "lines": 93, "description": "课表查询(学生/班级课表只读,P0-5 已修复:写函数已迁移至 scheduling/data-access-class-schedule.ts)", "exports": [ "getStudentSchedule", "getClassSchedule" ] }, { "path": "data-access-students.ts", "lines": 253, "description": "学生相关查询(科目成绩、学生名单、学生班级,通过 homework/data-access-classes 获取数据)", "exports": [ "getStudentsSubjectScores", "getClassStudentSubjectScoresV2", "getStudentClasses", "getClassStudents" ] }, { "path": "data-access-admin.ts", "lines": 406, "description": "管理员班级管理(管理员班级 CRUD、年级管理班级查询)", "exports": [ "getAdminClasses", "getGradeManagedClasses", "getManagedGrades", "createAdminClass", "updateAdminClass", "deleteAdminClass" ] }, { "path": "actions.ts", "lines": 50, "description": "Barrel re-export(P0-3 修复:从 974 行拆分为 6 个职责文件)", "exports": [ "createTeacherClassAction", "updateTeacherClassAction", "deleteTeacherClassAction", "createAdminClassAction", "updateAdminClassAction", "deleteAdminClassAction", "createGradeClassAction", "updateGradeClassAction", "deleteGradeClassAction", "enrollStudentByEmailAction", "joinClassByInvitationCodeAction", "ensureClassInvitationCodeAction", "regenerateClassInvitationCodeAction", "createClassInvitationCodeAction", "revokeClassInvitationCodeAction", "listClassInvitationCodesAction", "setStudentEnrollmentStatusAction", "bulkEnrollStudentsAction", "bulkAssignSubjectTeachersAction", "createClassScheduleItemAction", "updateClassScheduleItemAction", "deleteClassScheduleItemAction", "ClassInvitationCodeOption" ] }, { "path": "actions-teacher.ts", "lines": 100, "description": "教师班级 CRUD(3 个 Action,P0-3 修复)" }, { "path": "actions-admin.ts", "lines": 120, "description": "管理员班级 CRUD(3 个 Action,P0-3 修复)" }, { "path": "actions-grade.ts", "lines": 110, "description": "年级组长班级 CRUD(3 个 Action,P0-3 修复)" }, { "path": "actions-invitations.ts", "lines": 502, "description": "邀请码与注册 + 批量操作(10 个 Action,P0-3 修复;P2-4 新增批量导入学生/批量分配教师)" }, { "path": "actions-schedule.ts", "lines": 90, "description": "班级课表 CRUD(3 个 Action,P0-3 修复)" }, { "path": "actions-shared.ts", "lines": 60, "description": "共享工具(hasAdminScope/hasTeacherScope/hasStudentScope/parseSubjectTeachers/toWeekday,P1-1 修复)" }, { "path": "components/class-error-boundary.tsx", "lines": 45, "description": "班级模块 Error Boundary(class component + i18n + router.refresh 重试,P1-C 新增)" }, { "path": "components/class-skeleton.tsx", "lines": 80, "description": "班级模块骨架屏组件(5 个:ClassListSkeleton/ClassCardSkeleton/ClassGridSkeleton/StudentListSkeleton/ScheduleGridSkeleton,P1-C 新增)" }, { "path": "components/class-list-table.tsx", "lines": 115, "description": "班级列表表格(admin/grade 共享,P1-D 抽取)" }, { "path": "components/class-form-dialog.tsx", "lines": 200, "description": "班级创建/编辑表单弹窗(admin/grade 共享,schools 可选控制学校选择显隐,P1-D 抽取)" }, { "path": "components/class-delete-dialog.tsx", "lines": 35, "description": "班级删除确认弹窗(admin/grade 共享,基于 ConfirmDeleteDialog,P1-D 抽取)" }, { "path": "components/class-list-toolbar.tsx", "lines": 30, "description": "班级列表工具栏:新建按钮+计数徽章(admin/grade 共享,P1-D 抽取)" }, { "path": "components/class-form-utils.ts", "lines": 35, "description": "formatSubjectTeachers 工具函数 + ClassFormGrade/SubjectTeacherState 类型(P1-D 抽取)" }, { "path": "components/schedule-utils.ts", "lines": 89, "description": "✅ 2026-06-26 审计修复新增:课表纯函数工具集(getSubjectColor/getPositionStyle/timeToMinutes;✅ P2 修复新增 SCHEDULE_WEEKDAYS 周常量)。修复原 getSubjectColor 仅匹配英文关键词导致中文 DEFAULT_CLASS_SUBJECTS 全部落到 default 分支的颜色分组失效 bug;导出为纯函数便于单测", "exports": [ "getSubjectColor", "getPositionStyle", "timeToMinutes", "SCHEDULE_WEEKDAYS" ] }, { "path": "components/schedule-create-dialog.tsx", "description": "✅ P2 修复新增:课表新增对话框(从 schedule-view.tsx 拆分,承接原 isWorking/handleCreate 状态)", "exports": [ "ScheduleCreateDialog" ] }, { "path": "components/schedule-edit-dialog.tsx", "description": "✅ P2 修复新增:课表编辑对话框(从 schedule-view.tsx 拆分,承接原 isWorking/handleUpdate 状态)", "exports": [ "ScheduleEditDialog" ] }, { "path": "components/schedule-delete-dialog.tsx", "description": "✅ P2 修复新增:课表删除确认对话框(从 schedule-view.tsx 拆分,承接原 isWorking/handleDelete 状态)", "exports": [ "ScheduleDeleteDialog" ] }, { "path": "hooks/use-class-data.ts", "lines": 95, "description": "对话框+表单状态管理 hook(P1-E 抽取,admin/grade 共享)" }, { "path": "hooks/use-class-filters.ts", "lines": 30, "description": "按学校筛选年级 hook(P1-E 抽取,admin 模式使用)" } ] }, "school": { "path": "src/modules/school", "description": "学校基础数据管理:学校、年级、部门、学年的CRUD。actions 层仅做编排(权限校验+Zod+revalidatePath+after(logAudit)),所有 DB 操作下沉至 data-access。学校CRUD actions 在写操作成功后通过 Next.js after() 异步调用 logAudit() 记录操作日志", "exports": { "actions": [ { "name": "createSchoolAction", "permission": "SCHOOL_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "创建学校", "auditLog": "school.create" }, { "name": "updateSchoolAction", "permission": "SCHOOL_MANAGE", "signature": "(schoolId, prevState, formData) => Promise>", "purpose": "更新学校", "auditLog": "school.update" }, { "name": "deleteSchoolAction", "permission": "SCHOOL_MANAGE", "signature": "(schoolId) => Promise>", "purpose": "删除学校", "auditLog": "school.delete" }, { "name": "createGradeAction", "permission": "GRADE_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "创建年级" }, { "name": "updateGradeAction", "permission": "GRADE_MANAGE", "signature": "(gradeId, prevState, formData) => Promise>", "purpose": "更新年级" }, { "name": "deleteGradeAction", "permission": "GRADE_MANAGE", "signature": "(gradeId) => Promise>", "purpose": "删除年级" }, { "name": "promoteGradesAction", "permission": "GRADE_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "年级升级(order +1 + 名称升级)", "auditLog": "grade.promote" }, { "name": "createDepartmentAction", "permission": "SCHOOL_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "创建部门" }, { "name": "updateDepartmentAction", "permission": "SCHOOL_MANAGE", "signature": "(departmentId, prevState, formData) => Promise>", "purpose": "更新部门" }, { "name": "deleteDepartmentAction", "permission": "SCHOOL_MANAGE", "signature": "(departmentId) => Promise>", "purpose": "删除部门" }, { "name": "createAcademicYearAction", "permission": "SCHOOL_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "创建学年" }, { "name": "updateAcademicYearAction", "permission": "SCHOOL_MANAGE", "signature": "(academicYearId, prevState, formData) => Promise>", "purpose": "更新学年" }, { "name": "deleteAcademicYearAction", "permission": "SCHOOL_MANAGE", "signature": "(academicYearId) => Promise>", "purpose": "删除学年" } ], "dataAccess": [ { "name": "getSchools", "signature": "() => Promise", "usedBy": [ "admin学校管理", "onboarding" ] }, { "name": "getGrades", "signature": "() => Promise", "usedBy": [ "admin年级管理", "exams", "onboarding" ] }, { "name": "getDepartments", "signature": "() => Promise", "usedBy": [ "admin部门管理" ] }, { "name": "getAcademicYears", "signature": "() => Promise", "usedBy": [ "admin学年管理" ] }, { "name": "getStaffOptions", "signature": "() => Promise", "usedBy": [ "school组件" ] }, { "name": "getGradesForStaff", "signature": "(staffId) => Promise", "usedBy": [ "grade_head视图" ] }, { "name": "getSchoolsForUser", "signature": "(userId) => Promise", "usedBy": [ "非admin角色学校视图" ] }, { "name": "getGradesForUser", "signature": "(userId) => Promise", "usedBy": [ "非admin角色年级视图" ] }, { "name": "createDepartment", "signature": "(data: DepartmentInsertData) => Promise", "usedBy": [ "createDepartmentAction" ] }, { "name": "updateDepartment", "signature": "(id: string, data: DepartmentUpdateData) => Promise", "usedBy": [ "updateDepartmentAction" ] }, { "name": "deleteDepartment", "signature": "(id: string) => Promise", "usedBy": [ "deleteDepartmentAction" ] }, { "name": "createSchool", "signature": "(data: SchoolInsertData) => Promise", "usedBy": [ "createSchoolAction" ] }, { "name": "updateSchool", "signature": "(id: string, data: SchoolUpdateData) => Promise", "usedBy": [ "updateSchoolAction" ] }, { "name": "deleteSchool", "signature": "(id: string) => Promise", "usedBy": [ "deleteSchoolAction" ] }, { "name": "createGrade", "signature": "(data: GradeInsertData) => Promise", "usedBy": [ "createGradeAction" ] }, { "name": "updateGrade", "signature": "(id: string, data: GradeUpdateData) => Promise", "usedBy": [ "updateGradeAction" ] }, { "name": "deleteGrade", "signature": "(id: string) => Promise", "usedBy": [ "deleteGradeAction" ] }, { "name": "createAcademicYear", "signature": "(data: AcademicYearInsertData) => Promise", "usedBy": [ "createAcademicYearAction" ] }, { "name": "updateAcademicYear", "signature": "(id: string, data: AcademicYearUpdateData) => Promise", "usedBy": [ "updateAcademicYearAction" ] }, { "name": "deleteAcademicYear", "signature": "(id: string) => Promise", "usedBy": [ "deleteAcademicYearAction" ] }, { "name": "getSubjectOptions", "signature": "() => Promise", "purpose": "获取科目选项列表(跨模块接口)", "usedBy": [ "homework/data-access", "grades/data-access", "grades/data-access-analytics", "grades/export", "elective/data-access-selections" ] }, { "name": "getGradeOptions", "signature": "() => Promise", "purpose": "获取年级选项列表(跨模块接口)", "usedBy": [ "grades/data-access", "grades/export", "parent/data-access", "elective/data-access-selections" ] }, { "name": "isGradeHead", "signature": "(gradeId: string, userId: string) => Promise", "purpose": "校验用户是否为指定年级的年级主任(跨模块接口)", "usedBy": [ "classes/actions.createTeacherClassAction" ] }, { "name": "isGradeManager", "signature": "(gradeId: string, userId: string) => Promise", "purpose": "校验用户是否为指定年级的年级主任或教学主任(跨模块接口)", "usedBy": [ "classes/actions.createGradeClassAction", "classes/actions.updateGradeClassAction", "classes/actions.deleteGradeClassAction" ] }, { "name": "findGradeIdByHeadAndName", "signature": "(userId: string, gradeName: string) => Promise", "purpose": "根据年级名称查找用户担任年级主任的年级 ID(跨模块接口)", "usedBy": [ "classes/actions.createTeacherClassAction" ] }, { "name": "getSubjectNameMapByIds", "signature": "(subjectIds: string[]) => Promise>", "purpose": "按 ID 批量获取科目名称映射(跨模块接口,P1-1 新增供 homework/data-access-classes 调用,替代直查 subjects 表)", "usedBy": [ "homework/data-access-classes.getHomeworkAssignmentsWithSubject", "homework/data-access-classes.getPublishedHomeworkAssignmentsWithSubject" ] }, { "name": "getGradeOverviewStats", "signature": "() => Promise", "purpose": "v4-P2-6 新增:年级概览统计,返回每个年级的 classCount/studentCount/teacherCount,供年级管理卡片视图使用。动态导入 classes/data-access 避免循环依赖", "usedBy": [ "admin/school/grades/page.tsx", "school/components/grades-view.tsx" ] } ], "schema": [ { "name": "UpsertDepartmentSchema", "type": "const", "description": "zod schema 部门upsert", "usedBy": [ "createDepartmentAction", "updateDepartmentAction" ] }, { "name": "UpsertAcademicYearSchema", "type": "const", "description": "zod schema 学年upsert", "usedBy": [ "createAcademicYearAction", "updateAcademicYearAction" ] }, { "name": "UpsertSchoolSchema", "type": "const", "description": "zod schema 学校upsert", "usedBy": [ "createSchoolAction", "updateSchoolAction" ] }, { "name": "UpsertGradeSchema", "type": "const", "description": "zod schema 年级upsert", "usedBy": [ "createGradeAction", "updateGradeAction" ] } ], "types": [ { "name": "DepartmentListItem", "type": "type", "definition": "部门列表项", "usedBy": [ "getDepartments", "school/components" ] }, { "name": "AcademicYearListItem", "type": "type", "definition": "学年列表项", "usedBy": [ "getAcademicYears", "school/components" ] }, { "name": "SchoolListItem", "type": "type", "definition": "学校列表项", "usedBy": [ "getSchools", "school/components" ] }, { "name": "StaffOption", "type": "type", "definition": "员工选项", "usedBy": [ "getStaffOptions", "school/components" ] }, { "name": "GradeListItem", "type": "type", "definition": "年级列表项", "usedBy": [ "getGrades", "school/components", "exams" ] }, { "name": "DepartmentInsertData", "type": "type", "definition": "部门创建入参", "usedBy": [ "createDepartment" ] }, { "name": "DepartmentUpdateData", "type": "type", "definition": "部门更新入参", "usedBy": [ "updateDepartment" ] }, { "name": "SchoolInsertData", "type": "type", "definition": "学校创建入参", "usedBy": [ "createSchool" ] }, { "name": "SchoolUpdateData", "type": "type", "definition": "学校更新入参", "usedBy": [ "updateSchool" ] }, { "name": "GradeInsertData", "type": "type", "definition": "年级创建入参", "usedBy": [ "createGrade" ] }, { "name": "GradeUpdateData", "type": "type", "definition": "年级更新入参", "usedBy": [ "updateGrade" ] }, { "name": "AcademicYearInsertData", "type": "type", "definition": "学年创建入参", "usedBy": [ "createAcademicYear" ] }, { "name": "AcademicYearUpdateData", "type": "type", "definition": "学年更新入参", "usedBy": [ "updateAcademicYear" ] }, { "name": "OrgTreeNode", "type": "type", "definition": "组织架构树节点(学校/年级/班级三级,P2-2 修复)", "usedBy": [ "getOrgTree", "school/components/org-tree-nav.tsx" ] }, { "name": "GradeOverviewStats", "type": "interface", "file": "data-access.ts", "definition": "{ gradeId: string; classCount: number; studentCount: number; teacherCount: number }", "purpose": "v4-P2-6 新增:年级概览统计接口,每个年级的班级数/学生数/教师数", "usedBy": [ "data-access.getGradeOverviewStats", "school/components/grades-view.tsx", "school/components/grade-overview-cards.tsx" ] } ], "components": [ { "name": "SchoolsClient", "purpose": "学校管理客户端(组合容器,P1-5/P2-1 重构后仅负责组合子组件与 Table 渲染)" }, { "name": "SchoolFormDialog", "file": "components/school-form-dialog.tsx", "purpose": "学校创建/编辑表单对话框(内部管理 createMutation/updateMutation)", "props": "open, onOpenChange, editItem?, onSuccess" }, { "name": "SchoolDeleteDialog", "file": "components/school-delete-dialog.tsx", "purpose": "学校删除确认对话框(内部管理 deleteMutation)", "props": "deleteItem, onOpenChange, onSuccess" }, { "name": "SchoolListToolbar", "file": "components/school-list-toolbar.tsx", "purpose": "学校列表工具栏(数量 Badge + 新建按钮)", "props": "count, onCreate, isWorking" }, { "name": "GradesClient", "file": "components/grades-view.tsx", "purpose": "年级管理客户端(P1-A 修复:拆分为组合模式,仅负责列表渲染+筛选+组合子组件;v4-P2-6 gradeStats prop 保留)", "props": "grades: GradeListItem[], schools: SchoolListItem[], staff: StaffOption[], gradeStats: GradeOverviewStats[]" }, { "name": "GradeFormDialog", "file": "components/grade-form-dialog.tsx", "purpose": "P1-A 新增:年级创建/编辑表单对话框(内部管理表单状态+客户端校验:必填/长度/order格式/同校重名检测/isDirty检测,useActionMutation 管理 mutation)", "props": "open, onOpenChange, editItem, schools, staff, grades, onSuccess" }, { "name": "GradeDeleteDialog", "file": "components/grade-delete-dialog.tsx", "purpose": "P1-A 新增:年级删除确认对话框(内部管理 deleteMutation)", "props": "deleteItem, onOpenChange, onSuccess" }, { "name": "GradeListToolbar", "file": "components/grade-list-toolbar.tsx", "purpose": "P1-A 新增:年级列表工具栏(搜索框+学校筛选+年级主任筛选+排序选择器+新建按钮,筛选状态由父组件通过 nuqs useQueryState 管理)", "props": "q, setQ, school, setSchool, head, setHead, sort, setSort, hasFilters, onReset, schools, onCreate, isWorking" }, { "name": "GradeOverviewCards", "file": "components/grade-overview-cards.tsx", "purpose": "P1-A 新增:年级概览卡片视图(前8个年级卡片,展示班级/学生/教师数+年级主任/教学主任+快捷操作入口)", "props": "grades, statsMap, isWorking, onEdit, onDelete" }, { "name": "GradeInsightsFilters", "file": "components/grade-insights-filters.tsx", "purpose": "v4-P2-6 新增:年级洞察筛选器,使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新", "props": "grades: Array<{id, name, schoolName}>, currentGradeId: string, buildHref: (gradeId: string) => string" }, { "name": "DepartmentsClient", "purpose": "部门管理客户端" }, { "name": "AcademicYearClient", "purpose": "学年管理客户端" }, { "name": "SchoolErrorBoundary", "file": "components/school-error-boundary.tsx", "purpose": "school 模块共享 Error Boundary(class component + i18n + router.refresh 重试),4 个页面均包裹", "props": "children, fallback?" }, { "name": "SchoolListSkeleton", "file": "components/school-skeleton.tsx", "purpose": "表格加载骨架屏(animate-pulse)", "props": "rows?" }, { "name": "SchoolCardSkeleton", "file": "components/school-skeleton.tsx", "purpose": "卡片加载骨架屏(animate-pulse)", "props": "" }, { "name": "OrgTreeNav", "file": "components/org-tree-nav.tsx", "purpose": "学校→年级→班级三级树形导航(P2-2 修复):搜索过滤 + 选中高亮 + 展开/折叠 + 不同节点类型图标(School/GraduationCap/Users)+ 默认展开第一级", "props": "nodes: OrgTreeNode[], onSelect?: (node: OrgTreeNode) => void, selectedId?: string" } ], "hooks": [ { "name": "useSchoolData", "file": "hooks/use-school-data.ts", "signature": "useSchoolData(): { createOpen, editItem, deleteItem, setCreateOpen, setEditItem, setDeleteItem, isWorking }", "purpose": "学校管理客户端状态 Hook,集中管理创建/编辑/删除对话框开关状态与当前操作项,isWorking 表示任意对话框处于打开状态", "usedBy": [ "school/components/schools-view.tsx" ] }, { "name": "useGradeData", "file": "hooks/use-grade-data.ts", "signature": "useGradeData(): { createOpen, editItem, deleteItem, setCreateOpen, setEditItem, setDeleteItem, isWorking }", "purpose": "P1-A 新增:年级管理客户端状态 Hook,集中管理创建/编辑/删除对话框开关状态与当前操作项,isWorking 表示任意对话框处于打开状态", "usedBy": [ "school/components/grades-view.tsx" ] } ] } }, "dashboard": { "path": "src/modules/dashboard", "description": "各角色仪表盘数据聚合与展示(含权限校验 + i18n + 纯逻辑工具函数)", "exports": { "actions": [ { "name": "getAdminDashboardAction", "signature": "() => Promise", "deps": [ "shared/lib/auth-guard.requirePermission", "dashboard/data-access.getAdminDashboardData", "Permissions.DASHBOARD_ADMIN_READ" ], "usedBy": [ "admin/dashboard/page.tsx" ] }, { "name": "getTeacherDashboardAction", "signature": "() => Promise", "deps": [ "shared/lib/auth-guard.requirePermission", "classes/data-access.getTeacherClasses/getClassSchedule/getTeacherIdForMutations", "homework/data-access.getHomeworkAssignments/getHomeworkSubmissions/getTeacherGradeTrends", "users/data-access.getUserBasicInfo", "dashboard/lib/dashboard-utils.computeTeacherMetrics", "Permissions.DASHBOARD_TEACHER_READ" ], "usedBy": [ "teacher/dashboard/page.tsx" ] }, { "name": "getStudentDashboardAction", "signature": "() => Promise<{ student, dashboardProps }>", "deps": [ "shared/lib/auth-guard.requirePermission", "users/data-access.getCurrentStudentUser", "classes/data-access.getStudentClasses/getStudentSchedule", "homework/data-access.getStudentHomeworkAssignments/getStudentDashboardGrades", "dashboard/lib/dashboard-utils.countStudentAssignments/sortUpcomingAssignments/toWeekday/filterTodaySchedule", "Permissions.DASHBOARD_STUDENT_READ" ], "usedBy": [ "student/dashboard/page.tsx" ] }, { "name": "getParentDashboardAction", "signature": "() => Promise<{ data, hasChildren }>", "deps": [ "shared/lib/auth-guard.requirePermission", "parent/data-access.getParentDashboardData", "Permissions.DASHBOARD_PARENT_READ" ], "usedBy": [ "parent/dashboard/page.tsx" ] } ], "lib": [ { "name": "toWeekday", "signature": "(d: Date) => Weekday", "purpose": "Date 转 1-7 周几(周一=1,周日=7)" }, { "name": "countStudentAssignments", "signature": "(assignments, now, dueSoonWindowDays?) => StudentAssignmentStats", "purpose": "单次遍历统计学生作业:即将到期/已逾期/已批改" }, { "name": "sortUpcomingAssignments", "signature": "(assignments, limit?) => StudentHomeworkAssignmentListItem[]", "purpose": "按截止日期升序排序取前 N 条" }, { "name": "filterTodaySchedule", "signature": "(schedule: readonly ClassScheduleItem[], weekday: Weekday, classNameById?: ReadonlyMap) => StudentTodayScheduleItem[]", "purpose": "筛选指定周几课表并按开始时间排序(V4 修复:移除泛型与 as T[] 断言,改为非泛型函数返回 StudentTodayScheduleItem[],可通过结构化类型赋值给 TeacherTodayScheduleItem[])" }, { "name": "computeTeacherMetrics", "signature": "(classes, schedule, assignments, submissions, gradeTrends, now) => TeacherDashboardMetrics", "purpose": "计算教师仪表盘派生指标:待批改数/进行中作业/平均分/提交率/今日课表/待批改列表" }, { "name": "getGreetingKey", "signature": "(now: Date) => 'morning' | 'afternoon' | 'evening'", "purpose": "根据当前小时返回问候语时段 key" }, { "name": "timeToMinutes", "signature": "(t: string) => number", "purpose": "V4(P1-1)新增:将 HH:MM 时间字符串转换为当天分钟数,无效输入返回 0" }, { "name": "getScheduleStatus", "signature": "(start: string, end: string, now: Date) => ScheduleStatus", "purpose": "V4(P1-1)新增:根据当前时间判断课程状态 live/upcoming/past,消除 teacher-schedule 本地 getStatus 重复" }, { "name": "getDueUrgency", "signature": "(dueAt: string | null, now: Date) => DueUrgency", "purpose": "V4(P1-1)新增:根据截止时间计算紧急度 overdue/urgent/warning/normal/null,消除 student-upcoming-assignments-card 本地重复" }, { "name": "getActionLabelKey", "signature": "(status: string) => ActionLabelKey", "purpose": "V4(P1-1)新增:根据作业进度状态返回操作按钮 i18n 键 action.review/view/continue/start" }, { "name": "getActionVariant", "signature": "(status: string) => ActionVariant", "purpose": "V4(P1-1)新增:根据作业进度状态返回操作按钮视觉变体 default/secondary/outline" }, { "name": "computeComparison", "signature": "(current: number, previous: number) => ComparisonResult", "purpose": "L3 新增:计算两个周期的变化百分比,返回 trend up/down/flat,纯函数便于单测" }, { "name": "toCSV", "signature": "(rows: readonly ExportRow[], columns: readonly ExportColumn[]) => string", "purpose": "L7 新增:将数据行数组转换为 CSV 字符串(含 UTF-8 BOM 支持中文 Excel)" }, { "name": "downloadFile", "signature": "(content: string, filename: string, mimeType?: string) => void", "purpose": "L7 新增:触发浏览器下载文件(Blob URL + 模拟点击)" }, { "name": "exportDashboardCSV", "signature": "(rows: readonly ExportRow[], columns: readonly ExportColumn[], filename: string) => void", "purpose": "L7 新增:导出仪表盘统计数据为 CSV 便捷方法" } ], "components": [ { "name": "AdminDashboardView", "file": "admin-dashboard/AdminDashboardView", "purpose": "管理员仪表盘视图(V1 新增趋势图表区域:用户增长趋势 + 作业提交趋势)" }, { "name": "UserGrowthChart", "file": "admin-dashboard/user-growth-chart", "purpose": "recharts 折线图组件(V1 新增,复用于用户增长趋势与作业提交趋势两个卡片;V3 新增 labelKey prop 区分图表标签 + 空数据时渲染 EmptyState)" }, { "name": "StudentDashboard", "file": "student-dashboard/StudentDashboard", "purpose": "学生仪表盘(注意:非StudentDashboardView)" }, { "name": "StudentDashboardHeader", "file": "student-dashboard/StudentDashboardHeader", "purpose": "学生仪表盘头部(V3 从客户端组件转为服务端组件)" }, { "name": "StudentGradesCard", "file": "student-dashboard/StudentGradesCard", "purpose": "学生成绩卡片(V3 新增 useLocale 传入 formatDate 实现日期本地化)" }, { "name": "StudentStatsGrid", "file": "student-dashboard/StudentStatsGrid", "purpose": "学生统计网格" }, { "name": "StudentTodayScheduleCard", "file": "student-dashboard/StudentTodayScheduleCard", "purpose": "学生今日课表卡片(V3 空状态新增 CTA 跳转 /student/schedule)" }, { "name": "StudentUpcomingAssignmentsCard", "file": "student-dashboard/StudentUpcomingAssignmentsCard", "purpose": "学生即将到来作业卡片(V3 新增 getLocale 日期本地化 + 空状态 CTA 跳转 /student/learning/assignments)" }, { "name": "TeacherDashboardView", "file": "teacher-dashboard/TeacherDashboardView", "purpose": "教师仪表盘视图" }, { "name": "TeacherClassesCard", "file": "teacher-dashboard/TeacherClassesCard", "purpose": "教师班级卡片" }, { "name": "TeacherDashboardHeader", "file": "teacher-dashboard/TeacherDashboardHeader", "purpose": "教师仪表盘头部(V3 从客户端组件转为服务端组件)" }, { "name": "TeacherGradeTrends", "file": "teacher-dashboard/TeacherGradeTrends", "purpose": "教师年级趋势" }, { "name": "TeacherHomeworkCard", "file": "teacher-dashboard/TeacherHomeworkCard", "purpose": "教师作业卡片(V3 新增 getLocale 日期本地化 + aria-label 无障碍标签)" }, { "name": "TeacherQuickActions", "file": "teacher-dashboard/TeacherQuickActions", "purpose": "教师快捷操作(V3 从客户端组件转为异步服务端组件)" }, { "name": "TeacherSchedule", "file": "teacher-dashboard/TeacherSchedule", "purpose": "教师课表(V3 getStatus 函数新增显式返回类型)" }, { "name": "TeacherStats", "file": "teacher-dashboard/TeacherStats", "purpose": "教师统计(V3 移除未使用的 isLoading prop)" }, { "name": "RecentSubmissions", "file": "teacher-dashboard/RecentSubmissions", "purpose": "最近提交(V3 移除 AvatarImage 死代码 + 新增 getLocale 日期本地化)" }, { "name": "DashboardGreetingHeader", "file": "dashboard-greeting-header", "purpose": "共享问候头部组件(V2 抽象,消除 teacher/student 头部 90% 重复代码,接收 userName 和可选 actions slot;V3 从客户端组件转为异步服务端组件,使用 getTranslations/getLocale)" }, { "name": "DashboardLoadingSkeleton", "file": "dashboard-loading-skeleton", "purpose": "V3 新增:仪表盘共享骨架屏组件,消除 5 个 loading.tsx 路由文件的重复代码" }, { "name": "DashboardErrorFallback", "file": "dashboard-error-fallback", "purpose": "V3 新增:仪表盘共享错误边界组件,含 i18n + reset() 重试,消除 5 个 error.tsx 路由文件的重复代码" }, { "name": "AdminStatsBar", "file": "admin-dashboard/admin-sections", "purpose": "V4(P2-1)新增:管理员顶部统计栏流式组件,用 React use() 独立消费 usersStats/classesStats/homeworkStats Promise" }, { "name": "AdminContentCard", "file": "admin-dashboard/admin-sections", "purpose": "V4(P2-1)新增:管理员内容统计卡片流式组件,用 React use() 消费 textbooksStats/questionsStats/examsStats Promise" }, { "name": "AdminHomeworkActivityCard", "file": "admin-dashboard/admin-sections", "purpose": "V4(P2-1)新增:管理员作业活跃度卡片流式组件,用 React use() 消费 homeworkStats Promise" }, { "name": "AdminUserRolesCard", "file": "admin-dashboard/admin-sections", "purpose": "V4(P2-1)新增:管理员用户角色分布卡片流式组件,用 React use() 消费 usersStats Promise" }, { "name": "AdminRecentUsersTable", "file": "admin-dashboard/admin-sections", "purpose": "V4(P2-1)新增:管理员最近注册用户表流式组件,用 React use() 消费 usersStats Promise + getLocale" }, { "name": "AdminTrendCharts", "file": "admin-dashboard/admin-sections", "purpose": "V4(P2-1)新增:管理员趋势图表组件(静态,趋势数据待接入)" }, { "name": "AdminHeaderBadges", "file": "admin-dashboard/admin-sections", "purpose": "V4(P2-1)新增:管理员页头徽章流式组件,用 React use() 消费 usersStats Promise" }, { "name": "ParentDashboard", "file": "parent-dashboard/parent-dashboard", "purpose": "V4(P1-4)从 parent 模块迁移至 dashboard 模块:家长仪表盘视图,使用 slots 组合注入 ChildCard 和 ParentAttentionBanner,避免直接 import parent 模块组件" }, { "name": "DashboardTimeRangeFilter", "file": "dashboard-time-range-filter", "purpose": "L2 新增:时间范围筛选器客户端组件,URL search param 持久化选择 today/week/month" }, { "name": "ComparisonBadge", "file": "comparison-badge", "purpose": "L3 新增:数据对比徽章组件,显示当前值与上一周期的变化百分比,带方向图标和颜色" }, { "name": "DashboardNotificationWidget", "file": "dashboard-notification-widget", "purpose": "L5 新增:通知中心 Widget,仪表盘内显示最近通知摘要,完整下拉由 SiteHeader 处理" }, { "name": "DashboardResponsiveLayout", "file": "dashboard-responsive-layout", "purpose": "L8 新增:移动端响应式布局组件,根据屏幕尺寸自动调整 Widget 布局" }, { "name": "MobileSwipeContainer", "file": "dashboard-responsive-layout", "purpose": "L8 新增:移动端水平滑动卡片容器,snap-x snap-mandatory 吸附效果" }, { "name": "DesktopGrid", "file": "dashboard-responsive-layout", "purpose": "L8 新增:桌面端网格容器,与 MobileSwipeContainer 配对使用" } ], "dataAccess": [ { "name": "getAdminDashboardData", "signature": "(scope?: DataScope) => Promise", "deps": [ "users/data-access.getUsersDashboardStats", "classes/data-access.getClassesDashboardStats", "textbooks/data-access.getTextbooksDashboardStats", "questions/data-access.getQuestionsDashboardStats", "exams/data-access.getExamsDashboardStats", "homework/stats-service.getHomeworkDashboardStats", "DataScope" ], "usedBy": [ "dashboard/actions.getAdminDashboardAction" ] }, { "name": "getAdminDashboardStreams", "signature": "() => Promise", "deps": [ "auth-guard.requirePermission", "users/data-access.getUsersDashboardStats", "classes/data-access.getClassesDashboardStats", "textbooks/data-access.getTextbooksDashboardStats", "questions/data-access.getQuestionsDashboardStats", "exams/data-access.getExamsDashboardStats", "homework/stats-service.getHomeworkDashboardStats" ], "purpose": "V4(P2-1)新增:管理员仪表盘流式数据源,返回各独立数据源的未解析 Promise,供各分区组件用 React use() 独立消费实现流式渲染", "usedBy": [ "app/(dashboard)/admin/dashboard/page.tsx" ] } ], "types": [ { "name": "StudentDashboardProps", "definition": "{ studentName, enrolledClassCount, dueSoonCount, overdueCount, gradedCount, todayScheduleItems, upcomingAssignments, grades }", "deps": [ "homework/types.StudentDashboardGradeProps" ], "usedBy": [ "student-dashboard-view.tsx" ] }, { "name": "TeacherDashboardData", "definition": "{ classes, schedule, assignments, submissions, teacherName, gradeTrends }", "deps": [ "homework/data-access.getTeacherGradeTrends", "classes/data-access.getTeacherClasses" ], "usedBy": [ "teacher-dashboard-view.tsx" ] }, { "name": "AdminDashboardData", "definition": "{ activeSessionsCount, userCount, userRoleCounts, classCount, textbookCount, chapterCount, questionCount, examCount, homeworkAssignmentCount, homeworkAssignmentPublishedCount, homeworkSubmissionCount, homeworkSubmissionToGradeCount, recentUsers, userGrowth, homeworkTrend }", "usedBy": [ "admin/dashboard/page.tsx" ] }, { "name": "AdminDashboardUserRoleCount", "type": "type", "definition": "管理员仪表盘用户角色计数", "usedBy": [ "admin-dashboard/AdminDashboardView" ] }, { "name": "AdminDashboardRecentUser", "type": "type", "definition": "管理员仪表盘最近用户", "usedBy": [ "admin-dashboard/AdminDashboardView" ] }, { "name": "StudentTodayScheduleItem", "type": "type", "definition": "学生今日课表项", "usedBy": [ "student-dashboard/StudentTodayScheduleCard" ] }, { "name": "TeacherTodayScheduleItem", "type": "type", "definition": "教师今日课表项", "usedBy": [ "teacher-dashboard/TeacherSchedule" ] }, { "name": "DashboardRole", "type": "type", "definition": "\"admin\" | \"teacher\" | \"student\" | \"parent\"", "purpose": "V4(P2-2)新增:仪表盘角色类型,用于配置驱动 Widget 渲染", "usedBy": [ "dashboard/config/widget-configs.getWidgetConfig", "dashboard/services/dashboard-service" ] }, { "name": "WidgetSkeletonVariant", "type": "type", "definition": "\"stats\" | \"card\" | \"chart\" | \"table\" | \"list\"", "purpose": "V4(P2-2)新增:Widget 骨架屏变体类型,与 DashboardSection 对齐", "usedBy": [ "dashboard/config/widget-configs", "dashboard/types.DashboardWidgetConfig" ] }, { "name": "DashboardWidgetConfig", "type": "interface", "definition": "{ id: string; slot: string; skeletonVariant: WidgetSkeletonVariant; layoutClassName: string; defaultVisible: boolean }", "purpose": "V4(P2-2)新增:Widget 配置项接口,通过配置决定每个角色仪表盘渲染哪些 Widget 及其布局", "usedBy": [ "dashboard/config/widget-configs", "dashboard/types.DashboardLayoutConfig" ] }, { "name": "DashboardLayoutConfig", "type": "interface", "definition": "{ role: DashboardRole; widgets: DashboardWidgetConfig[] }", "purpose": "V4(P2-2)新增:角色仪表盘布局配置接口", "usedBy": [ "dashboard/config/widget-configs.ADMIN_WIDGET_CONFIG", "dashboard/config/widget-configs.TEACHER_WIDGET_CONFIG", "dashboard/config/widget-configs.STUDENT_WIDGET_CONFIG", "dashboard/config/widget-configs.PARENT_WIDGET_CONFIG", "dashboard/config/widget-configs.getWidgetConfig" ] } ], "hooks": [ { "name": "useDashboardPreferences", "file": "hooks/use-dashboard-preferences", "signature": "(role: DashboardRole) => { widgets, toggleWidget, resetToDefault }", "purpose": "L4 新增:仪表盘自定义偏好 Hook,localStorage 持久化 Widget 显示/隐藏偏好,lazy initializer 避免 useEffect setState" }, { "name": "useDashboardRealtime", "file": "hooks/use-dashboard-realtime", "signature": "(url: string | null, eventName?: string) => { status, lastMessage, lastUpdatedAt }", "purpose": "L6 新增:SSE 实时更新 Hook,指数退避重连(最大 30s),组件卸载自动清理 EventSource" } ], "services": [ { "name": "DashboardService", "file": "services/dashboard-service.tsx", "type": "interface", "definition": "{ getAdminStreams(): Promise; getTeacherData(): Promise>; getStudentData(): Promise>; getParentData(): Promise> }", "purpose": "V4(P2-1)新增:仪表盘数据服务接口抽象,通过 React Context 依赖注入实现模块完全解耦,组件不直接 import 其他业务模块的 actions/data-access" }, { "name": "DashboardAnalytics", "file": "services/dashboard-service.tsx", "type": "interface", "definition": "{ trackWidgetClick(widgetId, role); trackEmptyState(widgetId, role); trackErrorRetry(widgetId, role); trackPageView(role, durationMs) }", "purpose": "V4(P2-3)新增:监控埋点接口抽象,预留关键操作埋点(Widget 点击/空状态/错误重试/页面停留)" }, { "name": "DashboardServiceProvider", "file": "services/dashboard-service.tsx", "type": "component", "props": "{ children, service, analytics }", "purpose": "V4(P2-1)新增:React Context Provider,注入 DashboardService 和 DashboardAnalytics 实现" }, { "name": "useDashboardService", "file": "services/dashboard-service.tsx", "type": "hook", "signature": "() => DashboardService", "purpose": "V4(P2-1)新增:消费 DashboardService Context 的 Hook" }, { "name": "useDashboardAnalytics", "file": "services/dashboard-service.tsx", "type": "hook", "signature": "() => DashboardAnalytics", "purpose": "V4(P2-3)新增:消费 DashboardAnalytics Context 的 Hook" } ], "config": [ { "name": "ADMIN_WIDGET_CONFIG", "file": "config/widget-configs.ts", "type": "const", "definition": "DashboardLayoutConfig", "purpose": "V4(P2-2)新增:管理员仪表盘 Widget 布局配置,配置驱动渲染哪些 Widget 及其布局" }, { "name": "TEACHER_WIDGET_CONFIG", "file": "config/widget-configs.ts", "type": "const", "definition": "DashboardLayoutConfig", "purpose": "V4(P2-2)新增:教师仪表盘 Widget 布局配置" }, { "name": "STUDENT_WIDGET_CONFIG", "file": "config/widget-configs.ts", "type": "const", "definition": "DashboardLayoutConfig", "purpose": "V4(P2-2)新增:学生仪表盘 Widget 布局配置" }, { "name": "PARENT_WIDGET_CONFIG", "file": "config/widget-configs.ts", "type": "const", "definition": "DashboardLayoutConfig", "purpose": "V4(P2-2)新增:家长仪表盘 Widget 布局配置" }, { "name": "getWidgetConfig", "file": "config/widget-configs.ts", "type": "function", "signature": "(role: DashboardRole) => DashboardLayoutConfig", "purpose": "V4(P2-2)新增:按角色获取 Widget 配置,新增角色只需修改配置不需动组件代码" } ] } }, "layout": { "path": "src/modules/layout", "description": "应用布局框架:侧边栏、顶栏、导航配置", "exports": { "components": [ { "name": "AppSidebar", "purpose": "根据权限渲染侧边栏导航;N3 新增多角色切换:从 SidebarContext 读取 currentRole(null 时自动检测 admin>student>parent>teacher),用户拥有多个角色时在侧边栏底部显示 Select 下拉切换", "internalDeps": [ "usePermission", "NAV_CONFIG", "useSidebar", "shared/components/ui/select" ] }, { "name": "SiteHeader", "purpose": "顶部导航栏(集成 GlobalSearch 全局搜索:Cmd/Ctrl+K 唤起、300ms 防抖、↑/↓ 导航、Enter 跳转;NotificationDropdown 通知下拉)", "internalDeps": [ "useSession", "signOut", "shared/components/global-search.GlobalSearch", "messaging/components/notification-dropdown.NotificationDropdown" ] }, { "name": "SidebarProvider", "props": "{ children, sidebar }", "purpose": "侧边栏上下文Provider(N3 新增 currentRole/setCurrentRole 状态,null 表示自动检测角色)" }, { "name": "useSidebar", "type": "hook", "purpose": "侧边栏状态Hook(返回 expanded/isMobile/toggleSidebar/currentRole/setCurrentRole)" } ], "types": [ { "name": "Role", "type": "type", "definition": "\"admin\" | \"teacher\" | \"student\" | \"parent\"", "usedBy": [ "NAV_CONFIG", "usePermission" ] }, { "name": "NavItem", "type": "type", "definition": "{ title, href, icon?, permission? }", "usedBy": [ "NAV_CONFIG", "AppSidebar" ] } ], "config": [ { "name": "NAV_CONFIG", "type": "Record", "note": "每个NavItem含permission字段用于权限过滤。admin角色菜单包含Audit Logs项(icon: ScrollText, href: /admin/audit-logs, permission: AUDIT_LOG_READ),含子项Operation Logs与Login Logs。admin角色菜单的School Management子菜单包含Import Users项(href: /admin/users/import, permission: USER_MANAGE)。admin角色菜单包含Scheduling项(icon: CalendarClock, href: /admin/scheduling/rules, permission: SCHEDULE_ADJUST),含子项Rules(/admin/scheduling/rules, permission: SCHEDULE_ADJUST)、Auto Schedule(/admin/scheduling/auto, permission: SCHEDULE_AUTO)、Change Requests(/admin/scheduling/changes, permission: SCHEDULE_ADJUST)。teacher角色菜单包含Grades项(icon: GraduationCap, permission: GRADE_RECORD_READ),含子项All Grades(/teacher/grades)、Batch Entry(/teacher/grades/entry, permission: GRADE_RECORD_MANAGE)、Statistics(/teacher/grades/stats)。teacher角色菜单包含Schedule Changes项(icon: CalendarClock, href: /teacher/schedule-changes, permission: SCHEDULE_ADJUST)。teacher角色菜单包含Diagnostic项(icon: Stethoscope, href: /teacher/diagnostic, permission: DIAGNOSTIC_READ)。student角色菜单包含My Grades项(icon: GraduationCap, href: /student/grades, permission: GRADE_RECORD_READ)。student角色菜单包含Diagnostic项(icon: Stethoscope, href: /student/diagnostic, permission: DIAGNOSTIC_READ)。parent角色菜单包含Dashboard项(icon: LayoutDashboard, href: /parent/dashboard,无permission字段仅需登录)、Grades项(icon: GraduationCap, href: /parent/grades, permission: GRADE_RECORD_READ)、Announcements项(icon: Megaphone, href: /announcements, permission: ANNOUNCEMENT_READ)" } ] } }, "settings": { "path": "src/modules/settings", "description": "系统设置 + AI Provider配置 + 用户偏好 + 密码安全(修改密码、强度校验)。系统设置页 /admin/settings 提供学校信息、安全策略、文件上传、通知配置等运行参数管理", "exports": { "actions": [ { "name": "getAiProviderSummaries", "permission": "AI_CHAT", "signature": "() => Promise>", "purpose": "获取当前用户可见的 AI Provider 列表(V3.1 增强:管理员返回全部,普通用户返回 public + own private;P1 已修复:DB 操作下沉到 data-access;v3 已修复:返回值统一为 ActionState)", "deps": [ "data-access.getAiProviderSummaries", "data-access.getAiProviderSummariesForUser" ] }, { "name": "upsertAiProviderAction", "permission": "AI_CHAT", "signature": "(data) => Promise>", "purpose": "创建/更新 AI Provider(V3.1 增强:visibility 字段,非管理员强制 private;管理员可创建 public;P1 已修复:DB 操作下沉到 data-access,并行查询优化)", "deps": [ "shared/lib/ai (encrypt/decrypt)", "data-access.countDefaultAiProviders", "data-access.getAiProviderForUpdate", "data-access.updateAiProvider", "data-access.createAiProvider" ] }, { "name": "testAiProviderAction", "permission": "AI_CHAT", "signature": "(data) => Promise>", "purpose": "测试AI Provider连通性", "deps": [ "shared/lib/ai.testAiProviderConfig" ] }, { "name": "deleteAiProviderAction", "permission": "AI_CHAT", "signature": "(input: { id: string }) => Promise>", "purpose": "删除 AI Provider(V3 新增:事务删除 + 若为默认则自动转移默认到最新记录;V3.1 增强:管理员可删除任意,普通用户仅可删除自己创建的)", "deps": [ "data-access.deleteAiProvider" ] }, { "name": "canConfigurePublicAiProvider", "permission": "-", "signature": "() => Promise>", "purpose": "检查当前用户是否拥有 AI_CONFIGURE 权限(V3.1 新增:供 UI 层决定是否显示 public 可见性选项)", "deps": [ "shared/lib/auth-guard.getAuthContext" ] }, { "name": "changePasswordAction", "file": "actions-password.ts", "permission": "USER_PROFILE_UPDATE", "signature": "(prevState: ActionState, formData: FormData) => Promise>", "purpose": "修改当前用户密码(Zod 校验 + 校验当前密码 + 新密码策略 validatePassword + checkBreachedPassword HIBP k-anonymity 检测(audit-P2-4 新增,fail-open)+ 速率限制 PASSWORD_CHANGE: 5次/分钟 + 并行查询优化)", "deps": [ "requirePermission", "validatePassword", "shared/lib/breached-password.checkBreachedPassword", "rateLimit", "bcryptjs (hash/compare)", "data-access.getUserPasswordHash", "data-access.getPasswordSecurityByUserId", "data-access.updateUserPassword", "data-access.upsertPasswordSecurityOnPasswordChange" ], "usedBy": [ "components/password-change-form.tsx" ] }, { "name": "updateUserAvatarAction", "file": "actions-avatar.ts", "permission": "USER_PROFILE_UPDATE", "signature": "(imageUrl: string) => Promise>", "purpose": "更新用户头像 URL(P2-8 新增;v2 已增强:更新成功后清理旧头像文件,包括磁盘文件和 DB 记录)", "deps": [ "requirePermission", "users/data-access.getUserProfile", "users/data-access.updateUserAvatar", "files/data-access.getFileByUrl", "files/data-access.deleteFileAttachment" ], "usedBy": [ "components/avatar-upload.tsx" ] }, { "name": "removeUserAvatarAction", "file": "actions-avatar.ts", "permission": "USER_PROFILE_UPDATE", "signature": "() => Promise>", "purpose": "移除用户头像(P2-8 新增;v2 已增强:同时清理旧头像文件)", "deps": [ "requirePermission", "users/data-access.getUserProfile", "users/data-access.updateUserAvatar", "files/data-access.getFileByUrl", "files/data-access.deleteFileAttachment" ], "usedBy": [ "components/avatar-upload.tsx" ] }, { "name": "sendTestNotificationAction", "file": "actions-notifications.ts", "permission": "USER_PROFILE_UPDATE", "signature": "(input: { channel: 'push' | 'email' | 'sms' }) => Promise>", "purpose": "发送测试通知(P2-10 新增;v2 已增强:接入 notifications/dispatcher.sendNotification 真实发送)", "deps": [ "requirePermission", "notifications/dispatcher.sendNotification" ], "usedBy": [ "components/notification-preferences-form.tsx" ] }, { "name": "getAdminSystemSettingsAction", "file": "actions-system-settings.ts", "permission": "SETTINGS_ADMIN", "signature": "() => Promise>", "purpose": "获取管理员系统设置(P0-3 新增:从 system_settings 表加载 4 个分类配置)", "deps": [ "requirePermission", "data-access-system-settings.getAllSystemSettings" ], "usedBy": [ "components/admin-settings-view.tsx" ] }, { "name": "saveAdminSystemSettingsAction", "file": "actions-system-settings.ts", "permission": "SETTINGS_ADMIN", "signature": "(values: AdminSettingsFormValues) => Promise>", "purpose": "保存管理员系统设置(P0-3 新增:4 分类 Zod 校验 + 批量 upsert)", "deps": [ "requirePermission", "data-access-system-settings.upsertSystemSettings" ], "usedBy": [ "components/admin-settings-view.tsx" ] }, { "name": "getBrandConfigAction", "file": "actions-brand.ts", "permission": "SCHOOL_MANAGE", "signature": "() => Promise>", "purpose": "获取品牌配置(audit-P2-6 新增:从 system_settings 表 brand 分类读取 4 个键 schoolName/logoUrl/testimonialQuote/testimonialAuthor,未配置项使用 DEFAULT_BRAND_CONFIG 默认值)", "deps": [ "requirePermission", "data-access-brand.getBrandConfig" ], "usedBy": [ "components/brand-config-card.tsx" ] }, { "name": "saveBrandConfigAction", "file": "actions-brand.ts", "permission": "SCHOOL_MANAGE", "signature": "(prevState: ActionState, formData: FormData) => Promise>", "purpose": "保存品牌配置(audit-P2-6 新增:Zod 校验 schoolName/logoUrl/testimonialQuote/testimonialAuthor + 4 键 upsert + revalidatePath 刷新 /(auth)/login / /(auth)/register / /admin/settings)", "deps": [ "requirePermission", "shared.lib.session.getSession", "data-access-brand.saveBrandConfig" ], "usedBy": [ "components/brand-config-card.tsx" ] }, { "name": "getSecurityCenterAction", "file": "actions-security.ts", "permission": "USER_PROFILE_UPDATE", "signature": "() => Promise>", "purpose": "获取安全中心数据(P2-9 新增:2FA 状态 + 最近 10 条登录历史;v2 已优化:使用 getSystemSettingsByCategory 一次查询替代 3 次单独查询)", "deps": [ "requirePermission", "data-access-system-settings.getSystemSettingsByCategory", "shared.db.schema.loginLogs" ], "usedBy": [ "components/security-center-card.tsx" ] }, { "name": "setupTwoFactorAction", "file": "actions-security.ts", "permission": "USER_PROFILE_UPDATE", "signature": "() => Promise>", "purpose": "2FA 启用流程第一步:生成 TOTP 密钥 + QR 码 Data URL(v3 新增:完整 TOTP 实现,替代 v2 的占位开关)", "deps": [ "requirePermission", "users/data-access.getUserProfile", "data-access-two-factor.getTwoFactorEnabled", "data-access-two-factor.setTotpSecret", "lib/totp.generateTotpSecret", "lib/totp.buildOtpAuthUrl", "lib/totp.generateQrCodeDataUrl" ], "usedBy": [ "components/security-center-card.tsx" ] }, { "name": "verifyTwoFactorAction", "file": "actions-security.ts", "permission": "USER_PROFILE_UPDATE", "signature": "(token: string) => Promise>", "purpose": "2FA 启用流程第二步:校验一次性码 + 生成 10 个备份码(bcrypt 哈希存储)+ 启用 2FA(v3 新增)", "deps": [ "requirePermission", "data-access-two-factor.getTotpSecret", "data-access-two-factor.setBackupCodesHashed", "data-access-two-factor.setTwoFactorEnabled", "data-access-two-factor.setTwoFactorEnabledAt", "lib/totp.verifyTotpCode", "lib/totp.generateBackupCodes", "lib/totp.hashBackupCodes" ], "usedBy": [ "components/security-center-card.tsx" ] }, { "name": "disableTwoFactorAction", "file": "actions-security.ts", "permission": "USER_PROFILE_UPDATE", "signature": "(token: string) => Promise>", "purpose": "关闭 2FA:需提供有效 TOTP 码或备份码确认身份,清除密钥和备份码(v3 新增)", "deps": [ "requirePermission", "data-access-two-factor.getTwoFactorEnabled", "data-access-two-factor.getTotpSecret", "data-access-two-factor.getBackupCodesHashed", "data-access-two-factor.setTwoFactorEnabled", "data-access-two-factor.setTwoFactorEnabledAt", "data-access-two-factor.deleteTotpSecret", "data-access-two-factor.deleteBackupCodes", "data-access-two-factor.setBackupCodesHashed", "lib/totp.verifyTotpCode", "lib/totp.verifyBackupCode", "lib/totp.consumeBackupCode" ], "usedBy": [ "components/security-center-card.tsx" ] }, { "name": "regenerateBackupCodesAction", "file": "actions-security.ts", "permission": "USER_PROFILE_UPDATE", "signature": "(token: string) => Promise>", "purpose": "重新生成备份码:需 TOTP 码确认身份,使旧备份码失效(v3 新增)", "deps": [ "requirePermission", "data-access-two-factor.getTwoFactorEnabled", "data-access-two-factor.getTotpSecret", "data-access-two-factor.setBackupCodesHashed", "lib/totp.verifyTotpCode", "lib/totp.generateBackupCodes", "lib/totp.hashBackupCodes" ], "usedBy": [ "components/security-center-card.tsx" ] }, { "name": "preflightTwoFactorAction", "file": "actions-security.ts", "permission": "(public, login 前预检)", "signature": "(email: string) => Promise<{ required: boolean }>", "purpose": "登录预检:根据邮箱查询用户是否启用 2FA,登录表单据此展示 2FA 输入框(v3 新增;不验证密码,防邮箱枚举)", "deps": [ "shared.db", "shared.db.schema.users", "data-access-two-factor.getTwoFactorEnabled" ], "usedBy": [ "modules/auth/components/login-form.tsx" ] }, { "name": "verifyTwoFactorForLogin", "file": "actions-security.ts", "permission": "(internal, 供 auth.ts 调用)", "signature": "(params: { userId: string; token?: string }) => Promise<{ required: boolean; valid: boolean }>", "purpose": "登录时 2FA 校验:检查用户是否启用 2FA 并校验 TOTP 码或备份码(消耗备份码);由 auth.ts authorize 回调调用(v3 新增)", "deps": [ "data-access-two-factor.getTwoFactorEnabled", "data-access-two-factor.getTotpSecret", "data-access-two-factor.getBackupCodesHashed", "data-access-two-factor.setBackupCodesHashed", "lib/totp.verifyTotpCode", "lib/totp.verifyBackupCode", "lib/totp.consumeBackupCode" ], "usedBy": [ "auth.ts" ] }, { "name": "revokeAllOtherSessionsAction", "file": "actions-security.ts", "permission": "USER_PROFILE_UPDATE", "signature": "() => Promise>", "purpose": "远程登出当前用户的所有其他会话(v2 新增:删除 sessions 表记录 + 记录安全处置日志;注意当前为 JWT 策略,sessions 表可能无活跃记录)", "deps": [ "requirePermission", "users/data-access.getUserProfile", "shared.db.schema.sessions", "shared.lib.login-logger.logLoginEvent" ], "usedBy": [ "components/security-center-card.tsx" ] }, { "name": "updateProfileAction", "file": "actions-service.ts", "permission": "USER_PROFILE_UPDATE", "signature": "(input: UpdateUserProfileInput) => Promise>", "purpose": "设置页个人资料更新 Server Action wrapper(v1.1 新增:委托给 users/actions.updateUserProfile,作为 Server Action 引用传递给 Client Component,避免 Server→Client 函数传递违规)", "deps": [ "users/actions.updateUserProfile" ], "usedBy": [ "app/(dashboard)/settings/page.tsx" ] }, { "name": "updateNotificationPreferencesAction", "file": "actions-service.ts", "permission": "MESSAGE_READ", "signature": "(input: UpdateNotificationPreferencesInput) => Promise>", "purpose": "设置页通知偏好更新 Server Action wrapper(v1.1 新增:直接调用 notifications/preferences.upsertNotificationPreferences,绕过 messaging/actions 的 FormData 签名,适配 SettingsService 接口)", "deps": [ "requirePermission", "notifications/preferences.upsertNotificationPreferences" ], "usedBy": [ "app/(dashboard)/settings/page.tsx" ] } ], "dataAccess": [ { "name": "getAiProviderSummaries", "signature": "() => Promise", "file": "data-access.ts", "purpose": "获取所有 AI Provider(管理员视图,返回 public + private 全部记录;P1 新增,从 actions 下沉)", "deps": [ "shared.db", "shared.db.schema.aiProviders" ] }, { "name": "getAiProviderSummariesForUser", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "获取当前用户可见的 AI Provider(V3.1 新增:用户视图,返回 public + own private)", "deps": [ "shared.db", "shared.db.schema.aiProviders" ] }, { "name": "countDefaultAiProviders", "signature": "() => Promise", "file": "data-access.ts", "purpose": "统计默认AI Provider数量(P1 新增)", "deps": [ "shared.db", "shared.db.schema.aiProviders" ] }, { "name": "getAiProviderForUpdate", "signature": "(id: string, userId?: string) => Promise", "file": "data-access.ts", "purpose": "获取 AI Provider 更新所需字段(P1 新增;V3.1 增强:可选 userId 做所有权校验,非创建者返回 null)", "deps": [ "shared.db", "shared.db.schema.aiProviders" ] }, { "name": "updateAiProvider", "signature": "(id: string, data: UpdateAiProviderInput, resetOtherDefaults: boolean) => Promise", "file": "data-access.ts", "purpose": "更新AI Provider(事务,P1 新增)", "deps": [ "shared.db", "shared.db.schema.aiProviders" ] }, { "name": "createAiProvider", "signature": "(data: CreateAiProviderInput, resetOtherDefaults: boolean) => Promise", "file": "data-access.ts", "purpose": "创建AI Provider(事务,P1 新增)", "deps": [ "shared.db", "shared.db.schema.aiProviders" ] }, { "name": "getUserPasswordHash", "signature": "(userId: string) => Promise<{ password: string | null } | null>", "file": "data-access.ts", "purpose": "获取用户密码哈希(P1 新增,从 actions-password 下沉)", "deps": [ "shared.db", "shared.db.schema.users" ] }, { "name": "getPasswordSecurityByUserId", "signature": "(userId: string) => Promise<{ id: string } | null>", "file": "data-access.ts", "purpose": "获取用户密码安全记录(P1 新增,从 actions-password 下沉)", "deps": [ "shared.db", "shared.db.schema.passwordSecurity" ] }, { "name": "updateUserPassword", "signature": "(userId: string, newHash: string, now: Date) => Promise", "file": "data-access.ts", "purpose": "更新用户密码(P1 新增,从 actions-password 下沉)", "deps": [ "shared.db", "shared.db.schema.users" ] }, { "name": "upsertPasswordSecurityOnPasswordChange", "signature": "(userId: string, now: Date, existing: { id: string } | null) => Promise", "file": "data-access.ts", "purpose": "密码修改后更新或创建密码安全记录(P1 新增,从 actions-password 下沉)", "deps": [ "shared.db", "shared.db.schema.passwordSecurity" ] }, { "name": "getSystemSettingsByCategory", "signature": "(category: SystemSettingCategory) => Promise", "file": "data-access-system-settings.ts", "purpose": "获取指定分类下所有设置项(P0-3 新增)", "deps": [ "shared.db", "shared.db.schema.systemSettings" ] }, { "name": "getAllSystemSettings", "signature": "() => Promise", "file": "data-access-system-settings.ts", "purpose": "获取所有系统设置项(P0-3 新增)", "deps": [ "shared.db", "shared.db.schema.systemSettings" ] }, { "name": "getSystemSetting", "signature": "(category: SystemSettingCategory, key: string) => Promise", "file": "data-access-system-settings.ts", "purpose": "获取单个设置项(P0-3 新增)", "deps": [ "shared.db", "shared.db.schema.systemSettings" ] }, { "name": "upsertSystemSetting", "signature": "(params: { category, key, value, valueType, updatedBy? }) => Promise", "file": "data-access-system-settings.ts", "purpose": "插入或更新设置项(P0-3 新增)", "deps": [ "shared.db", "shared.db.schema.systemSettings" ] }, { "name": "upsertSystemSettings", "signature": "(items: Array<{ category, key, value, valueType }>, updatedBy?: string) => Promise", "file": "data-access-system-settings.ts", "purpose": "批量 upsert 设置项(P0-3 新增)", "deps": [ "shared.db", "shared.db.schema.systemSettings" ] }, { "name": "getBrandConfig", "signature": "() => Promise", "file": "data-access-brand.ts", "purpose": "获取品牌配置(audit-P2-6 新增:server-only,从 system_settings 表 brand 分类并行读取 4 个键 schoolName/logoUrl/testimonialQuote/testimonialAuthor,未配置项使用 DEFAULT_BRAND_CONFIG 默认值;re-exports BrandConfig 类型 + DEFAULT_BRAND_CONFIG 默认值)", "deps": [ "data-access-system-settings.getSystemSetting", "brand-config.DEFAULT_BRAND_CONFIG" ] }, { "name": "saveBrandConfig", "signature": "(config: BrandConfig, updatedBy?: string) => Promise", "file": "data-access-brand.ts", "purpose": "保存品牌配置(audit-P2-6 新增:4 键并行 upsert 到 system_settings 表 brand 分类)", "deps": [ "data-access-system-settings.upsertSystemSetting" ] }, { "name": "getStudentProfileOverviewData", "signature": "(userId: string) => Promise", "file": "data-access-profile-overview.ts", "purpose": "获取学生概览数据(v3 新增:封装 classes/homework data-access 调用,避免组件层直接 import 其他业务模块;使用 Promise.all 并行查询)", "deps": [ "classes/data-access.getStudentClasses", "classes/data-access.getStudentSchedule", "homework/data-access.getStudentHomeworkAssignments", "homework/data-access.getStudentDashboardGrades" ] }, { "name": "getTeacherProfileOverviewData", "signature": "() => Promise", "file": "data-access-profile-overview.ts", "purpose": "获取教师概览数据(v3 新增:封装 classes data-access 调用,避免组件层直接 import 其他业务模块;使用 Promise.all 并行查询)", "deps": [ "classes/data-access.getTeacherClasses", "classes/data-access.getTeacherTeachingSubjects" ] } ], "types": [ { "name": "AiProviderSummary", "file": "types.ts", "type": "interface", "definition": "AI Provider摘要(P1 从 actions.ts 迁至 types.ts)", "usedBy": [ "getAiProviderSummaries", "settings/components", "exams/components" ] }, { "name": "AiProviderName", "file": "types.ts", "type": "type", "definition": "AI Provider名称联合类型(P1 新增)", "usedBy": [ "data-access", "types.AiProviderSummary" ] }, { "name": "AiProviderExisting", "file": "types.ts", "type": "interface", "definition": "AI Provider更新所需字段(P1 新增)", "usedBy": [ "data-access.getAiProviderForUpdate" ] }, { "name": "SettingsService", "file": "types.ts", "type": "interface", "definition": "设置模块统一服务接口(profile + notifications + trackEvent),通过 React Context 注入实现解耦", "usedBy": [ "components/settings-service-context.tsx", "app/(dashboard)/settings/page.tsx" ] }, { "name": "ProfileService", "file": "types.ts", "type": "interface", "definition": "个人资料服务接口(getProfile + updateProfile),解耦组件对 users/actions 的直接依赖", "usedBy": [ "types.SettingsService", "components/profile-settings-form.tsx" ] }, { "name": "NotificationPreferenceService", "file": "types.ts", "type": "interface", "definition": "通知偏好服务接口(getPreferences + updatePreferences),解耦组件对 messaging/actions 的直接依赖", "usedBy": [ "types.SettingsService", "components/notification-preferences-form.tsx" ] }, { "name": "BrandConfig", "file": "brand-config.ts", "type": "interface", "definition": "品牌配置(audit-P2-6 新增:schoolName: string / logoUrl: string|null / testimonialQuote: string / testimonialAuthor: string;纯类型无 server-only,可被 Server/Client Component 安全导入)", "usedBy": [ "data-access-brand.getBrandConfig", "data-access-brand.saveBrandConfig", "actions-brand.getBrandConfigAction", "actions-brand.saveBrandConfigAction", "components/brand-config-card.tsx", "auth/components/auth-layout.tsx" ] }, { "name": "DEFAULT_BRAND_CONFIG", "file": "brand-config.ts", "type": "const", "definition": "默认品牌配置(audit-P2-6 新增:schoolName='Next_Edu' / logoUrl=null / testimonialQuote 默认英文标语 / testimonialAuthor='Sofia Davis';数据库未配置 brand 分类时 AuthLayout 使用此默认值)", "usedBy": [ "data-access-brand.getBrandConfig", "components/brand-config-card.tsx", "auth/components/auth-layout.tsx" ] } ], "components": [ { "name": "AiProviderSettingsCard", "purpose": "AI Provider设置卡片(i18n:settings.ai.providers;v3 已重构:提取 AiProviderSelector + AiProviderDeleteDialog 子组件,从 529 行降至 ~443 行)", "deps": [ "components/ai-provider-selector", "components/ai-provider-delete-dialog" ] }, { "name": "AiProviderSelector", "file": "components/ai-provider-selector.tsx", "purpose": "AI 服务商选择器(v3 新增:从 AiProviderSettingsCard 拆分;服务商下拉 + 密钥状态 + 可见性 Badge)", "usedBy": [ "components/ai-provider-settings-card.tsx" ] }, { "name": "AiProviderDeleteDialog", "file": "components/ai-provider-delete-dialog.tsx", "purpose": "AI 服务商删除确认对话框(v3 新增:从 AiProviderSettingsCard 拆分;AlertDialog 确认)", "usedBy": [ "components/ai-provider-settings-card.tsx" ] }, { "name": "AdminSettingsView", "purpose": "系统设置视图(P0-3 已修复:从 mock 改为真实数据层;v3 已重构:拆分为 4 个子 Card 组件,主文件仅含表单状态 + 提交逻辑,从 444 行降至 176 行;✅ audit-P2-6 在主表单后挂载 BrandConfigCard,独立表单避免嵌套
;i18n:settings.admin)", "deps": [ "getAdminSystemSettingsAction", "saveAdminSystemSettingsAction", "components/admin-school-info-card", "components/admin-security-policy-card", "components/admin-file-upload-card", "components/admin-notification-config-card", "components/brand-config-card" ], "usedBy": [ "app/(dashboard)/admin/settings/page.tsx" ] }, { "name": "BrandConfigCard", "file": "components/brand-config-card.tsx", "purpose": "品牌配置卡片(audit-P2-6 新增:管理员配置学校品牌信息 schoolName/logoUrl/testimonialQuote/testimonialAuthor;独立表单 + 独立保存,通过 getBrandConfigAction/saveBrandConfigAction 调用;挂载在 AdminSettingsView 主表单之后以避免嵌套 ;i18n:settings.brand.*)", "deps": [ "getBrandConfigAction", "saveBrandConfigAction", "brand-config.DEFAULT_BRAND_CONFIG", "brand-config.BrandConfig" ], "usedBy": [ "components/admin-settings-view.tsx" ] }, { "name": "SchoolInfoCard", "file": "components/admin-school-info-card.tsx", "purpose": "学校信息 Card(v3 新增:从 AdminSettingsView 拆分;接收 values + onChange props;i18n:settings.admin.schoolInfo)", "usedBy": [ "components/admin-settings-view.tsx" ] }, { "name": "SecurityPolicyCard", "file": "components/admin-security-policy-card.tsx", "purpose": "安全策略 Card(v3 新增:从 AdminSettingsView 拆分;接收 values + onChange props;i18n:settings.admin.securityPolicy)", "usedBy": [ "components/admin-settings-view.tsx" ] }, { "name": "FileUploadCard", "file": "components/admin-file-upload-card.tsx", "purpose": "文件上传 Card(v3 新增:从 AdminSettingsView 拆分;接收 values + onChange props;i18n:settings.admin.fileUpload)", "usedBy": [ "components/admin-settings-view.tsx" ] }, { "name": "NotificationConfigCard", "file": "components/admin-notification-config-card.tsx", "purpose": "通知配置 Card(v3 新增:从 AdminSettingsView 拆分;接收 values + onChange props;i18n:settings.admin.notificationConfig)", "usedBy": [ "components/admin-settings-view.tsx" ] }, { "name": "AvatarUpload", "file": "components/avatar-upload.tsx", "purpose": "头像上传/预览/删除客户端组件(P2-8 新增;v2 已增强:文件名长度校验 ≤255 字符;文件通过 /api/upload 上传,调用 updateUserAvatarAction 更新 users.image;验证 JPEG/PNG/WebP/GIF + 2MB 上限;i18n:settings.profile.avatar)", "deps": [ "updateUserAvatarAction", "removeUserAvatarAction" ], "usedBy": [ "app/(dashboard)/profile/page.tsx" ] }, { "name": "SecurityCenterCard", "file": "components/security-center-card.tsx", "purpose": "安全中心卡片编排器(P2-9 新增;v3 已重构:从 645 行降至 ~115 行,仅负责加载数据 + 渲染 SecurityTwoFactorSection / SecurityRecentLoginsSection 两个子区块;i18n:settings.security.center)", "deps": [ "getSecurityCenterAction", "components/security-two-factor-section", "components/security-recent-logins-section" ], "usedBy": [ "components/settings-view.tsx" ] }, { "name": "SecurityTwoFactorSection", "file": "components/security-two-factor-section.tsx", "purpose": "2FA 区块(v3 新增:从 SecurityCenterCard 拆分;负责启用/关闭/重新生成备份码流程 + 3 个 Dialog;接收 twoFactor/loading/onStatusChange props)", "deps": [ "setupTwoFactorAction", "verifyTwoFactorAction", "disableTwoFactorAction", "regenerateBackupCodesAction" ], "usedBy": [ "components/security-center-card.tsx" ] }, { "name": "SecurityRecentLoginsSection", "file": "components/security-recent-logins-section.tsx", "purpose": "最近登录历史区块(v3 新增:从 SecurityCenterCard 拆分;展示登录记录 + 远程登出;接收 recentLogins/loading/currentDeviceLabel/onRevoked props)", "deps": [ "revokeAllOtherSessionsAction", "lib/security-utils.parseUserAgent", "lib/security-utils.formatRelativeTime" ], "usedBy": [ "components/security-center-card.tsx" ] }, { "name": "ProfileSettingsForm", "purpose": "个人资料设置表单(通过 useSettingsService().profile.updateProfile 调用,i18n:settings.profile)", "deps": [ "useSettingsService", "shared/components/form-fields/text-field", "shared/components/form-fields/select-field" ] }, { "name": "ThemePreferencesCard", "purpose": "主题偏好卡片(i18n:settings.appearance;P2-11 已增强:集成 LocaleSwitcher 语言切换到 Appearance 标签页)", "deps": [ "shared/components/locale-switcher" ] }, { "name": "SettingsView", "purpose": "统一设置页布局(5 标签页:General/Notifications/Appearance/Security/AI;角色差异通过 resolveRoleSettingsConfig 配置驱动 + generalExtra props 注入;Tab URL 持久化;每个 TabsContent 包裹 SettingsSectionErrorBoundary + Suspense 骨架屏;AI 标签页条件渲染需 AI_CONFIGURE 权限;登出 AlertDialog 二次确认;i18n:settings 命名空间)", "usedBy": [ "app/(dashboard)/settings/page.tsx" ] }, { "name": "SettingsServiceProvider", "file": "components/settings-service-context.tsx", "purpose": "SettingsService React Context Provider,页面层注入服务实现,组件层通过 useSettingsService() 消费", "usedBy": [ "app/(dashboard)/settings/page.tsx" ] }, { "name": "SettingsSectionErrorBoundary", "file": "components/settings-section-error-boundary.tsx", "purpose": "分区 Error Boundary,包裹每个 TabsContent 和 profile 角色概览区块,局部失败不影响整页", "usedBy": [ "components/settings-view.tsx", "app/(dashboard)/profile/page.tsx" ] }, { "name": "QuickLinksCard", "file": "components/quick-links-card.tsx", "purpose": "快捷链接卡片(客户端组件,i18n 键驱动:settings.quickLinks)", "usedBy": [ "config/role-settings-config.tsx" ] }, { "name": "ProfileStudentOverview", "file": "components/profile-student-overview.tsx", "purpose": "学生概览异步 Server Component,独立获取学生数据并渲染(StatsGrid + UpcomingAssignments + Grades + TodaySchedule),可被 Suspense + ErrorBoundary 包裹实现流式渲染(v3 已重构:通过 data-access-profile-overview 调用,不再直接 import classes/homework data-access)", "deps": [ "data-access-profile-overview.getStudentProfileOverviewData", "lib/student-overview-data.buildStudentOverviewData" ], "usedBy": [ "app/(dashboard)/profile/page.tsx" ] }, { "name": "ProfileTeacherOverview", "file": "components/profile-teacher-overview.tsx", "purpose": "教师概览异步 Server Component,独立获取教师数据并渲染(任教科目 + 任教班级),可被 Suspense + ErrorBoundary 包裹(v3 已重构:通过 data-access-profile-overview 调用,不再直接 import classes data-access)", "deps": [ "data-access-profile-overview.getTeacherProfileOverviewData" ], "usedBy": [ "app/(dashboard)/profile/page.tsx" ] }, { "name": "PasswordChangeForm", "purpose": "密码修改表单(当前密码/新密码/确认密码 + 强度指示器 + 需求提示;i18n:settings.security;a11y:aria-label)", "deps": [ "changePasswordAction", "getPasswordStrength", "PASSWORD_REQUIREMENT_HINTS" ] }, { "name": "NotificationPreferencesForm", "file": "components/notification-preferences-form.tsx", "purpose": "通知偏好设置表单(Switch 切换 email/sms/push 通道 + 5 个分类开关;通过 useSettingsService().notifications.updatePreferences 调用,i18n:settings.notifications;P2-10 已增强:每个已启用渠道旁显示测试按钮,调用 sendTestNotificationAction)", "deps": [ "useSettingsService", "shared/components/ui/switch", "shared/components/ui/card" ], "usedBy": [ "SettingsView" ] } ], "lib": [ { "name": "parseUserAgent", "file": "lib/security-utils.ts", "signature": "(ua: string | null) => { device: string; browser: string }", "purpose": "解析 User-Agent 字符串为设备类型 + 浏览器名称(v2 抽取的纯函数,便于单测)" }, { "name": "formatRelativeTime", "file": "lib/security-utils.ts", "signature": "(iso: string, locale: string) => string", "purpose": "将 ISO 时间戳格式化为相对时间(v2 抽取的纯函数)" }, { "name": "generateTotpSecret", "file": "lib/totp.ts", "signature": "() => string", "purpose": "生成 TOTP base32 密钥(v3 新增,基于 otplib v13)" }, { "name": "buildOtpAuthUrl", "file": "lib/totp.ts", "signature": "(params: { serviceName: string; accountName: string; secret: string }) => string", "purpose": "构建 otpauth:// URL 用于二维码扫描(v3 新增)" }, { "name": "generateQrCodeDataUrl", "file": "lib/totp.ts", "signature": "(otpAuthUrl: string) => Promise", "purpose": "将 otpauth URL 转换为 QR 码 Data URL(base64 PNG)(v3 新增)" }, { "name": "verifyTotpCode", "file": "lib/totp.ts", "signature": "(token: string, secret: string) => boolean", "purpose": "校验 TOTP 一次性码(±30s 窗口容差)(v3 新增)" }, { "name": "generateBackupCodes", "file": "lib/totp.ts", "signature": "() => string[]", "purpose": "生成 10 个 8 位备份码(去除易混淆字符)(v3 新增)" }, { "name": "hashBackupCodes", "file": "lib/totp.ts", "signature": "(codes: string[]) => Promise", "purpose": "将备份码列表 bcrypt 哈希为 JSON 数组字符串(v3 新增)" }, { "name": "verifyBackupCode", "file": "lib/totp.ts", "signature": "(input: string, storedHashedJson: string) => Promise", "purpose": "校验备份码,返回匹配索引或 -1(v3 新增)" }, { "name": "consumeBackupCode", "file": "lib/totp.ts", "signature": "(storedHashedJson: string, usedIndex: number) => Promise", "purpose": "从哈希列表中移除已使用的备份码(v3 新增)" }, { "name": "countRemainingBackupCodes", "file": "lib/totp.ts", "signature": "(storedHashedJson: string) => number", "purpose": "统计剩余备份码数量(v3 新增)" }, { "name": "toSettingItem", "file": "lib/system-settings-utils.ts", "signature": "(category: SystemSettingCategory, key: string, value: unknown, valueType: SystemSettingValueType) => SettingItem", "purpose": "将表单值序列化为数据库存储格式(v3 新增:从 actions-system-settings.ts 提取的纯函数,根据 valueType 转换为 string/number/boolean/json;便于单元测试)" } ] } }, "users": { "path": "src/modules/users", "description": "用户个人资料管理 + 用户批量导入/导出(Excel)", "exports": { "actions": [ { "name": "updateUserProfile", "signature": "(data: UpdateUserProfileInput) => Promise", "file": "actions.ts", "permission": "requireAuth()", "deps": [ "shared.db", "shared.db.schema.users", "shared.lib.auth-guard.requireAuth" ] }, { "name": "UpdateUserProfileInput", "type": "type", "file": "actions.ts", "definition": "{ name?, phone?, address?, gender?, age? }" }, { "name": "downloadUserTemplateAction", "signature": "() => Promise>", "file": "actions.ts", "permission": "USER_MANAGE", "purpose": "生成用户导入模板(返回 base64 编码的 Excel)", "deps": [ "requirePermission", "import-export.generateUserImportTemplate" ], "usedBy": [ "components/user-import-dialog.tsx" ] }, { "name": "importUsersAction", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "permission": "USER_MANAGE", "purpose": "导入用户:接收文件,解析+验证+批量创建(默认密码 123456)", "deps": [ "requirePermission", "shared.lib.excel.parseExcel", "import-export.parseUserImportData", "import-export.batchImportUsers" ], "usedBy": [ "components/user-import-dialog.tsx" ] }, { "name": "exportUsersAction", "signature": "(role?: string) => Promise>", "file": "actions.ts", "permission": "USER_MANAGE", "purpose": "导出用户列表(返回 base64 编码的 Excel)", "deps": [ "requirePermission", "import-export.exportUsersToExcel" ], "usedBy": [ "待扩展" ] }, { "name": "deleteUserAction", "signature": "(userId: string) => Promise>", "file": "actions.ts", "permission": "USER_MANAGE", "purpose": "删除用户(委托 data-access.deleteUserById,含最后管理员保护校验,revalidatePath /admin/users)。2026-06-24 审计修复:签名从 (prevState, formData) 改为 (userId),移除直接 DB 访问,改调 data-access", "deps": [ "requirePermission", "data-access.deleteUserById" ], "usedBy": [ "components/admin-users-view.tsx" ] } ], "dataAccess": [ { "name": "getUserProfile", "signature": "(userId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.users" ] }, { "name": "getUsersDashboardStats", "signature": "() => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.sessions", "shared.db.schema.usersToRoles", "shared.db.schema.roles" ], "usedBy": [ "dashboard/data-access.getAdminDashboardData" ] }, { "name": "getCurrentStudentUser", "signature": "() => Promise<{ id: string; name: string } | null>", "file": "data-access.ts", "purpose": "获取当前已认证的学生用户(id + name)。2026-06-24 审计修复:改用 shared/lib/session.getAuthContext() 统一认证入口(替代直接 auth()),通过 JOIN users/usersToRoles/roles 校验 student 角色", "deps": [ "shared.lib.session.getAuthContext", "data-access.getUserWithRole" ], "usedBy": [ "student/dashboard", "student/learning/assignments", "student/learning/assignments/[assignmentId]", "student/learning/courses", "student/learning/textbooks", "student/learning/textbooks/[id]", "student/schedule" ] }, { "name": "UserProfile", "type": "type", "file": "data-access.ts", "definition": "{ id, name, email, image, role, phone, address, gender, age, onboardedAt, createdAt, updatedAt }" }, { "name": "UsersDashboardStats", "type": "type", "file": "data-access.ts", "definition": "{ userCount, activeSessionsCount, userRoleCounts, recentUsers }" }, { "name": "getAdminUsers", "signature": "(params: { page?, pageSize?, search?, role? }) => Promise", "file": "data-access.ts", "purpose": "管理员用户列表分页查询(支持按姓名/邮箱搜索,按 createdAt 倒序,关联 usersToRoles/roles 聚合角色名)", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.usersToRoles", "shared.db.schema.roles" ], "usedBy": [ "app/(dashboard)/admin/users/page.tsx" ] }, { "name": "getAdminUserRoles", "signature": "() => Promise", "file": "data-access.ts", "purpose": "返回所有角色名列表(用于用户管理页角色筛选下拉框)", "deps": [ "shared.db", "shared.db.schema.roles" ], "usedBy": [ "app/(dashboard)/admin/users/page.tsx" ] }, { "name": "AdminUserListItem", "type": "type", "file": "data-access.ts", "definition": "{ id, name, email, roles: string[], phone, createdAt }" }, { "name": "AdminUserListResult", "type": "type", "file": "data-access.ts", "definition": "{ items: AdminUserListItem[], total, page, pageSize, totalPages }" }, { "name": "deleteUserById", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "2026-06-24 审计修复新增:按 userId 删除用户。包含最后管理员保护逻辑(删除前校验仍至少保留一个 admin 角色用户),物理删除 users 表记录(users_to_roles 外键 cascade)", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.usersToRoles", "shared.db.schema.roles" ], "usedBy": [ "actions.deleteUserAction" ] } ], "importExport": [ { "name": "generateUserImportTemplate", "signature": "() => Promise", "file": "import-export.ts", "purpose": "生成用户导入模板(列:姓名/邮箱/角色/手机/班级邀请码,含示例行)", "deps": [ "shared.lib.excel.generateTemplate" ], "usedBy": [ "actions.downloadUserTemplateAction" ] }, { "name": "parseUserImportData", "signature": "(rows: Record[]) => UserImportValidation", "file": "import-export.ts", "purpose": "解析并验证导入行(校验姓名/邮箱格式/角色枚举/邀请码仅 student)", "deps": [], "usedBy": [ "actions.importUsersAction" ] }, { "name": "exportUsersToExcel", "signature": "(params: { scope: DataScope; role?: string }) => Promise", "file": "import-export.ts", "purpose": "导出用户列表到 Excel(含姓名/邮箱/手机/性别/年龄/角色/创建时间)", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.roles", "shared.db.schema.usersToRoles", "shared.lib.excel.exportToExcel" ], "usedBy": [ "actions.exportUsersAction", "app/api/export/route.ts" ] } ], "userService": [ { "name": "batchImportUsers", "signature": "(records: UserImportRecord[]) => Promise", "file": "user-service.ts", "purpose": "批量创建用户(默认密码 123456 bcrypt 哈希,自动创建 usersToRoles,student 通过邀请码自动加入班级——委托 class-registration)", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.roles", "shared.db.schema.usersToRoles", "bcryptjs", "@paralleldrive/cuid2", "class-registration.registerStudentByInvitationCode" ], "usedBy": [ "actions.importUsersAction", "import-export.ts (re-export 向后兼容)" ] } ], "classRegistration": [ { "name": "registerStudentByInvitationCode", "signature": "(studentId: string, invitationCode: string) => Promise", "file": "class-registration.ts", "purpose": "通过邀请码将学生注册到班级,委托 classes/data-access.enrollStudentByInvitationCode,返回结构化结果(不抛异常)", "deps": [ "classes/data-access.enrollStudentByInvitationCode" ], "usedBy": [ "user-service.batchImportUsers" ] }, { "name": "ClassRegistrationResult", "type": "type", "file": "class-registration.ts", "definition": "{ success: boolean; error?: string }" } ], "types": [ { "name": "UserImportRecord", "type": "type", "file": "import-export.ts", "definition": "{ name, email, role, phone?, invitationCode? }", "usedBy": [ "parseUserImportData", "batchImportUsers" ] }, { "name": "UserImportValidation", "type": "type", "file": "import-export.ts", "definition": "{ valid: UserImportRecord[], invalid: Array<{ row, record, errors }> }", "usedBy": [ "parseUserImportData" ] }, { "name": "UserImportResult", "type": "type", "file": "user-service.ts", "definition": "{ successCount, failedCount, errors: Array<{ row, email, error }> }", "usedBy": [ "batchImportUsers", "importUsersAction", "import-export.ts (re-export 向后兼容)" ] } ], "components": [ { "name": "UserImportDialog", "file": "components/user-import-dialog.tsx", "purpose": "用户批量导入对话框(4 状态:idle/preview/importing/done;下载模板→上传预览→确认导入→结果展示)", "usedBy": [ "app/(dashboard)/admin/users/import/page.tsx" ] }, { "name": "AdminUsersView", "file": "components/admin-users-view.tsx", "purpose": "管理员用户列表客户端组件(搜索+角色筛选+表格+分页+删除确认对话框,通过 URL searchParams 驱动筛选状态)", "usedBy": [ "app/(dashboard)/admin/users/page.tsx" ] } ] } }, "rbac": { "path": "src/modules/rbac", "description": "角色与权限管理(RBAC):角色 CRUD、角色权限分配、用户角色分配、权限目录展示。2026-06-24 审计修复后新增模块记录", "exports": { "actions": [ { "name": "createRoleAction", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "file": "actions.ts", "permission": "ROLE_CREATE", "purpose": "创建自定义角色(requirePermission + Zod 校验 + createRole + logAudit + logDataChange)", "deps": [ "requirePermission", "data-access.createRole", "shared.lib.audit-logger.logAudit", "shared.lib.change-logger.logDataChange" ], "usedBy": [ "components/role-form-dialog.tsx" ] }, { "name": "updateRoleAction", "signature": "(roleId: string, prevState: ActionState | undefined, formData: FormData) => Promise>", "file": "actions.ts", "permission": "ROLE_UPDATE", "purpose": "更新角色名称/描述(admin 角色名不可改)", "deps": [ "requirePermission", "data-access.getRoleById", "data-access.updateRole" ], "usedBy": [ "components/role-form-dialog.tsx" ] }, { "name": "deleteRoleAction", "signature": "(roleId: string) => Promise>", "file": "actions.ts", "permission": "ROLE_DELETE", "purpose": "删除自定义角色(系统角色不可删)", "deps": [ "requirePermission", "data-access.getRoleById", "data-access.deleteRole" ], "usedBy": [ "components/role-list.tsx" ] }, { "name": "toggleRoleEnabledAction", "signature": "(roleId: string, enabled: boolean) => Promise>", "file": "actions.ts", "permission": "ROLE_UPDATE", "purpose": "启用/禁用角色(admin 角色不可禁用)", "deps": [ "requirePermission", "data-access.setRoleEnabled" ], "usedBy": [ "components/role-list.tsx" ] }, { "name": "setRolePermissionsAction", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "file": "actions.ts", "permission": "ROLE_UPDATE", "purpose": "替换角色的完整权限列表(admin 角色权限不可改)。P2-7 新增 trackPermissionChange 埋点", "deps": [ "requirePermission", "data-access.getRolePermissions", "data-access.setRolePermissions", "lib/track.trackPermissionChange" ], "usedBy": [ "components/role-permission-matrix.tsx" ] }, { "name": "assignUserRolesAction", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "file": "actions.ts", "permission": "ROLE_ASSIGN", "purpose": "为用户分配角色(全量替换)。P2-7 新增 trackPermissionChange 埋点", "deps": [ "requirePermission", "data-access-assignments.getUserRoleNames", "data-access-assignments.assignRolesToUser", "lib/track.trackPermissionChange" ], "usedBy": [ "components/user-role-assign-dialog.tsx" ] }, { "name": "getRoleDetailAction", "signature": "(roleId: string) => Promise>", "file": "actions.ts", "permission": "ROLE_READ", "purpose": "获取角色详情(含权限列表),供客户端组件调用", "deps": [ "requirePermission", "data-access.getRoleById" ], "usedBy": [ "components/role-detail-edit-button.tsx" ] }, { "name": "isAdminRole", "signature": "(roleName: string) => Promise", "file": "actions.ts", "permission": "无(公开 Server Action,仅判断角色名是否为 admin)", "purpose": "判断角色名是否为锁定 admin 角色,供客户端组件调用避免直接 import data-access", "deps": [ "data-access.ADMIN_ROLE_NAME" ], "usedBy": [ "components/role-list.tsx", "components/role-permission-matrix.tsx" ] } ], "dataAccess": [ { "name": "getRoles", "signature": "() => Promise", "file": "data-access.ts", "purpose": "列出所有角色(含权限数和用户数),系统角色优先", "deps": [ "shared.db", "shared.db.schema.roles", "shared.db.schema.rolePermissions", "shared.db.schema.usersToRoles" ], "usedBy": [ "app/(dashboard)/admin/roles/page.tsx" ] }, { "name": "getRoleById", "signature": "(id: string) => Promise", "file": "data-access.ts", "purpose": "按 id 获取单个角色详情(含完整权限列表和用户数),使用 isPermission 类型守卫过滤无效权限。P2-3 新增 userCount 字段用于影响提示", "deps": [ "shared.db", "shared.db.schema.roles", "shared.db.schema.rolePermissions", "shared.db.schema.usersToRoles", "shared.lib.type-guards.isPermission" ], "usedBy": [ "actions.updateRoleAction", "actions.deleteRoleAction", "actions.getRoleDetailAction", "app/(dashboard)/admin/roles/[id]/page.tsx" ] }, { "name": "getRoleByName", "signature": "(name: string) => Promise", "file": "data-access.ts", "purpose": "按 name 获取角色", "deps": [ "shared.db", "shared.db.schema.roles" ] }, { "name": "createRole", "signature": "(input: CreateRoleInput) => Promise", "file": "data-access.ts", "purpose": "创建自定义角色(重名校验)", "deps": [ "shared.db", "shared.db.schema.roles" ] }, { "name": "updateRole", "signature": "(id: string, input: UpdateRoleInput) => Promise", "file": "data-access.ts", "purpose": "更新角色名/描述(admin 角色名锁定)", "deps": [ "shared.db", "shared.db.schema.roles" ] }, { "name": "deleteRole", "signature": "(id: string) => Promise", "file": "data-access.ts", "purpose": "删除角色(系统角色不可删,FK cascade)", "deps": [ "shared.db", "shared.db.schema.roles" ] }, { "name": "setRoleEnabled", "signature": "(id: string, enabled: boolean) => Promise", "file": "data-access.ts", "purpose": "启用/禁用角色(admin 不可禁用)", "deps": [ "shared.db", "shared.db.schema.roles" ] }, { "name": "getRolePermissions", "signature": "(roleId: string) => Promise", "file": "data-access.ts", "purpose": "获取角色权限列表,使用 isPermission 类型守卫过滤无效权限", "deps": [ "shared.db", "shared.db.schema.rolePermissions", "shared.lib.type-guards.isPermission" ] }, { "name": "setRolePermissions", "signature": "(roleId: string, permissions: Permission[]) => Promise", "file": "data-access.ts", "purpose": "替换角色完整权限列表(事务:删后插,admin 锁定)", "deps": [ "shared.db", "shared.db.schema.roles", "shared.db.schema.rolePermissions" ] }, { "name": "ADMIN_ROLE_NAME", "type": "const", "file": "data-access.ts", "definition": "\"admin\"" } ], "dataAccessAssignments": [ { "name": "getUserRoleNames", "signature": "(userId: string) => Promise", "file": "data-access-assignments.ts", "purpose": "获取用户角色名列表", "deps": [ "shared.db", "shared.db.schema.usersToRoles", "shared.db.schema.roles" ] }, { "name": "assignRolesToUser", "signature": "(userId: string, roleNames: string[]) => Promise", "file": "data-access-assignments.ts", "purpose": "全量替换用户角色(事务:删后插,校验角色名存在)", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.roles", "shared.db.schema.usersToRoles" ] }, { "name": "getUserRoleAssignments", "signature": "(params?: { page?, pageSize?, search?, role? }) => Promise>", "file": "data-access-assignments.ts", "purpose": "分页查询用户角色分配列表(支持搜索和角色筛选)", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.usersToRoles", "shared.db.schema.roles" ] } ], "dataAccessPermissions": [ { "name": "getPermissionRoleCounts", "signature": "() => Promise>", "file": "data-access-permissions.ts", "purpose": "2026-06-24 审计修复新增:统计每个权限被多少启用角色拥有。使用 isPermission 类型守卫过滤无效权限。替代 admin/permissions/page.tsx 原直接 DB 查询", "deps": [ "shared.db", "shared.db.schema.rolePermissions", "shared.db.schema.roles", "shared.lib.type-guards.isPermission", "lib/permission-catalog.getAllPermissionMetas" ], "usedBy": [ "app/(dashboard)/admin/permissions/page.tsx" ] } ], "schema": [ { "name": "CreateRoleSchema", "type": "zod", "file": "schema.ts", "definition": "{ name: 2-50 字符 lowercase+digits+underscore, description?: 255 字符 }" }, { "name": "UpdateRoleSchema", "type": "zod", "file": "schema.ts", "definition": "{ name?: 同上, description?: 255 字符 }" }, { "name": "SetRolePermissionsSchema", "type": "zod", "file": "schema.ts", "definition": "{ roleId: string, permissions: string[] (refine 校验为有效权限值) }" }, { "name": "AssignUserRolesSchema", "type": "zod", "file": "schema.ts", "definition": "{ userId: string, roleNames: string[] }" } ], "types": [ { "name": "RoleRecord", "type": "type", "file": "types.ts", "definition": "{ id, name, description, isSystem, isEnabled, createdAt, updatedAt }" }, { "name": "RoleWithStats", "type": "type", "file": "types.ts", "definition": "RoleRecord & { permissionCount, userCount }" }, { "name": "RoleDetail", "type": "type", "file": "types.ts", "definition": "RoleRecord & { permissions: Permission[], userCount: number }" }, { "name": "CreateRoleInput", "type": "type", "file": "types.ts", "definition": "{ name, description? }" }, { "name": "UpdateRoleInput", "type": "type", "file": "types.ts", "definition": "{ name?, description? }" }, { "name": "UserRoleAssignment", "type": "type", "file": "types.ts", "definition": "{ userId, userName, userEmail, roleNames: string[], createdAt }" }, { "name": "PaginatedResult", "type": "type", "file": "types.ts", "definition": "{ items: T[], page, pageSize, total, totalPages }" } ], "lib": [ { "name": "getAllPermissionMetas", "signature": "() => Array<{ value: string; label: string; group: string; description?: string }>", "file": "lib/permission-catalog.ts", "purpose": "返回所有权限点的元数据(值/标签/分组/描述),用于权限目录展示和校验", "deps": [ "shared.types.permissions.Permissions" ], "usedBy": [ "data-access-permissions.getPermissionRoleCounts", "components/permission-catalog-view.tsx", "components/role-permission-matrix.tsx" ] }, { "name": "trackPermissionChange", "signature": "(params: TrackPermissionChangeParams) => void", "file": "lib/track.ts", "purpose": "P2-7 新增:RBAC 关键操作埋点接口(no-op 实现,预留未来 analytics 接入)。定义 TrackPermissionChangeParams 接口(action/roleId/userId/before/after)", "deps": [], "usedBy": [ "actions.setRolePermissionsAction", "actions.assignUserRolesAction" ] }, { "name": "DATA_SCOPE_RULES", "type": "const", "file": "lib/data-scope-resolver.ts", "definition": "DataScopeRule[]", "purpose": "P1-5/P1-6 审计修复新增:配置驱动的 DataScope 规则数组。按优先级顺序定义 admin/grade_head+teaching_head/teacher/student/parent 五种角色的 DataScope 解析策略。新增角色只需修改此数组", "deps": [ "modules/school/data-access.getGradesForStaff", "modules/classes/data-access.getTeacherScopeData", "modules/classes/data-access.getStudentScopeData", "modules/classes/data-access.getGradeIdsForStudentIds", "modules/parent/data-access.getChildren" ], "usedBy": [ "lib/data-scope-resolver.resolveDataScopeFromConfig" ] }, { "name": "resolveDataScopeFromConfig", "signature": "(userId: string, roleNames: Role[]) => Promise", "file": "lib/data-scope-resolver.ts", "purpose": "P1-5/P1-6 审计修复新增:配置驱动的 DataScope 解析器。遍历 DATA_SCOPE_RULES 数组,首个匹配且返回非 null 的规则决定用户 DataScope,无匹配则回退到 owned。替代 shared/lib/auth-guard.ts 中原硬编码 roleNames.includes('xxx') 链", "deps": [ "lib/data-scope-config.DataScopeRule", "lib/data-scope-resolver.DATA_SCOPE_RULES" ], "usedBy": [ "shared/lib/auth-guard.resolveDataScope (via dynamic import)" ] }, { "name": "DataScopeRule", "type": "type", "file": "lib/data-scope-config.ts", "definition": "{ roles: Role[]; resolve: (userId: string) => Promise }", "purpose": "P1-6 审计修复新增:DataScope 规则接口定义。roles 触发规则,resolve 返回 DataScope 或 null(null 表示 fall-through 到下一规则)" } ], "components": [ { "name": "RoleManagementView", "file": "components/role-management-view.tsx", "purpose": "角色管理主视图(角色列表 + 创建对话框)", "usedBy": [ "app/(dashboard)/admin/roles/page.tsx" ] }, { "name": "RoleList", "file": "components/role-list.tsx", "purpose": "角色列表组件(启用/禁用切换、删除、权限数显示)", "usedBy": [ "components/role-management-view.tsx" ] }, { "name": "RoleFormDialog", "file": "components/role-form-dialog.tsx", "purpose": "角色创建/编辑表单对话框", "usedBy": [ "components/role-management-view.tsx", "components/role-detail-edit-button.tsx" ] }, { "name": "RolePermissionMatrix", "file": "components/role-permission-matrix.tsx", "purpose": "角色权限矩阵(勾选/取消权限,调用 setRolePermissionsAction)。P2-1 新增搜索框+折叠分组;P2-3 新增 userCount 影响提示(Alert);P2-5 新增 fieldset/legend 语义化标签", "usedBy": [ "app/(dashboard)/admin/roles/[id]/page.tsx" ] }, { "name": "PermissionCatalogView", "file": "components/permission-catalog-view.tsx", "purpose": "权限目录展示(按分组列出所有权限点 + 角色计数)", "usedBy": [ "app/(dashboard)/admin/permissions/page.tsx" ] }, { "name": "UserRoleAssignDialog", "file": "components/user-role-assign-dialog.tsx", "purpose": "用户角色分配对话框(调用 assignUserRolesAction)", "usedBy": [ "app/(dashboard)/admin/users/page.tsx" ] }, { "name": "RoleDetailEditButton", "file": "components/role-detail-edit-button.tsx", "purpose": "角色详情编辑按钮(调用 getRoleDetailAction + RoleFormDialog)", "usedBy": [ "app/(dashboard)/admin/roles/[id]/page.tsx" ] }, { "name": "ErrorBoundary", "file": "components/error-boundary.tsx", "purpose": "2026-06-24 审计修复新增:RBAC 专用错误边界组件,包裹独立数据区块防止单点错误导致整页崩溃", "usedBy": [ "app/(dashboard)/admin/roles/page.tsx", "app/(dashboard)/admin/permissions/page.tsx", "app/(dashboard)/admin/users/page.tsx" ] } ] }, "dependencies": { "dependsOn": [ "shared/db", "shared/db/schema (roles, rolePermissions, usersToRoles, users)", "shared/lib/auth-guard (requirePermission)", "shared/lib/audit-logger (logAudit)", "shared/lib/change-logger (logDataChange)", "shared/lib/type-guards (isPermission)", "shared/lib/session (getAuthContext - 间接)", "shared/types/permissions (Permissions, Permission)", "shared/types/action-state (ActionState)" ], "dependedBy": [ "app/(dashboard)/admin/roles (页面调用 actions + data-access)", "app/(dashboard)/admin/permissions (页面调用 data-access-permissions)", "app/(dashboard)/admin/users (页面调用 user-role-assign-dialog)", "users (data-access-assignments 查询 users 表)" ] }, "dbTables": [ "roles", "role_permissions", "users_to_roles" ] }, "audit": { "path": "src/modules/audit", "description": "操作日志、登录日志与数据变更日志查询,支持 Excel 导出", "exports": { "dataAccess": [ { "name": "getAuditLogs", "signature": "(params?: AuditLogQueryParams) => Promise>", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.auditLogs" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/page.tsx" ] }, { "name": "getLoginLogs", "signature": "(params?: LoginLogQueryParams) => Promise>", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.loginLogs" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/login-logs/page.tsx" ] }, { "name": "getAuditModuleOptions", "signature": "() => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.auditLogs" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/page.tsx" ] }, { "name": "getDataChangeLogs", "signature": "(params?: DataChangeLogQueryParams) => Promise>", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.dataChangeLogs" ], "usedBy": [ "getDataChangeLogsAction" ] }, { "name": "getDataChangeStats", "signature": "() => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.dataChangeLogs" ], "usedBy": [ "getDataChangeLogsAction" ] }, { "name": "getDataChangeTableOptions", "signature": "() => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.dataChangeLogs" ], "usedBy": [ "getDataChangeLogsAction" ] }, { "name": "getDataChangeLogsForExport", "signature": "(params?: DataChangeLogQueryParams) => Promise", "file": "data-access.ts", "deps": [ "getDataChangeLogs" ], "usedBy": [ "exportDataChangeLogsAction" ] }, { "name": "getAuditLogsForExport", "signature": "(params?: AuditLogQueryParams) => Promise", "file": "data-access.ts", "deps": [ "getAuditLogs" ], "usedBy": [ "exportAuditLogsAction" ] }, { "name": "getLoginLogsForExport", "signature": "(params?: LoginLogQueryParams) => Promise", "file": "data-access.ts", "deps": [ "getLoginLogs" ], "usedBy": [ "exportLoginLogsAction" ] }, { "name": "getAuditOverviewStats", "signature": "() => Promise", "file": "data-access.ts", "purpose": "获取审计概览统计(今日审计事件/失败登录/数据变更 + 总数,✅ P2-4 新增)", "deps": [ "shared.db", "shared.db.schema.auditLogs", "shared.db.schema.loginLogs", "shared.db.schema.dataChangeLogs" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/overview/page.tsx" ] }, { "name": "getAuditTrend", "signature": "(days?: number) => Promise", "file": "data-access.ts", "purpose": "获取审计活动趋势(最近 N 天每日计数,MySQL DATE() 分组聚合,✅ P2-4 新增)", "deps": [ "shared.db", "shared.db.schema.auditLogs", "shared.db.schema.loginLogs", "shared.db.schema.dataChangeLogs" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/overview/page.tsx" ] }, { "name": "getDataChangeActionStats", "signature": "() => Promise", "file": "data-access.ts", "purpose": "获取数据变更按动作统计(create/update/delete 分布,✅ P2-4 新增)", "deps": [ "shared.db", "shared.db.schema.dataChangeLogs" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/overview/page.tsx" ] } ], "actions": [ { "name": "getDataChangeLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(params?: DataChangeLogQueryParams) => Promise>", "file": "actions.ts", "purpose": "获取数据变更日志(分页结果 + tableOptions + stats 三者并行加载)", "deps": [ "requirePermission", "data-access.getDataChangeLogs", "data-access.getDataChangeTableOptions", "data-access.getDataChangeStats" ], "usedBy": [ "待扩展" ] }, { "name": "exportAuditLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(params?: AuditLogQueryParams) => Promise>", "file": "actions.ts", "purpose": "导出操作日志为 Excel(✅ 审计重构:调用 export.buildAuditLogExport 构建列+行,trackEvent 埋点 audit.exported)", "deps": [ "requirePermission", "data-access.getAuditLogsForExport", "export.buildAuditLogExport", "shared.lib.excel.exportToExcel", "shared.lib.track-event.trackEvent" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/page.tsx" ] }, { "name": "exportLoginLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(params?: LoginLogQueryParams) => Promise>", "file": "actions.ts", "purpose": "导出登录日志为 Excel(✅ 审计重构:调用 export.buildLoginLogExport,trackEvent 埋点 audit.exported)", "deps": [ "requirePermission", "data-access.getLoginLogsForExport", "export.buildLoginLogExport", "shared.lib.excel.exportToExcel", "shared.lib.track-event.trackEvent" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/login-logs/page.tsx" ] }, { "name": "exportDataChangeLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(params?: DataChangeLogQueryParams) => Promise>", "file": "actions.ts", "purpose": "导出数据变更日志为 Excel(✅ 审计重构:调用 export.buildDataChangeLogExport,trackEvent 埋点 audit.exported)", "deps": [ "requirePermission", "data-access.getDataChangeLogsForExport", "export.buildDataChangeLogExport", "shared.lib.excel.exportToExcel", "shared.lib.track-event.trackEvent" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/data-changes/page.tsx" ] }, { "name": "getAuditRetentionConfigAction", "permission": "AUDIT_LOG_READ", "signature": "() => Promise>", "file": "actions.ts", "purpose": "获取审计日志保留策略配置(✅ P2-5 新增)", "deps": [ "requirePermission", "retention.getAuditRetentionConfig" ], "usedBy": [ "audit/services/admin-audit-service.ts" ] }, { "name": "saveAuditRetentionConfigAction", "permission": "AUDIT_LOG_READ", "signature": "(config: AuditRetentionConfig) => Promise>", "file": "actions.ts", "purpose": "保存审计日志保留策略配置(✅ P2-5 新增,trackEvent 埋点)", "deps": [ "requirePermission", "retention.saveAuditRetentionConfig", "shared.lib.track-event.trackEvent" ], "usedBy": [ "audit/components/audit-retention-settings.tsx" ] }, { "name": "purgeAuditLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(retentionDays?: number, loginLogRetentionDays?: number) => Promise>", "file": "actions.ts", "purpose": "手动清理过期审计日志(✅ P2-5 新增,trackEvent 埋点;✅ audit-P2-5 新增 loginLogRetentionDays 参数支持 login_logs 独立保留期)", "deps": [ "requirePermission", "retention.getAuditRetentionConfig", "retention.purgeExpiredAuditLogs", "shared.lib.track-event.trackEvent" ], "usedBy": [ "audit/components/audit-retention-settings.tsx" ] } ], "types": [ { "name": "AuditLog", "type": "interface", "file": "types.ts", "definition": "{ id, userId, userName, action, module, targetId, targetType, detail, ipAddress, userAgent, status, createdAt }" }, { "name": "LoginLog", "type": "interface", "file": "types.ts", "definition": "{ id, userId, userEmail, action, status, ipAddress, userAgent, errorMessage, createdAt }" }, { "name": "AuditLogQueryParams", "type": "type", "file": "types.ts", "definition": "{ userId?, module?, action?, status?, page?, pageSize?, startDate?, endDate? }" }, { "name": "LoginLogQueryParams", "type": "type", "file": "types.ts", "definition": "{ userId?, action?, status?, page?, pageSize?, startDate?, endDate? }" }, { "name": "PaginatedResult", "type": "interface", "file": "types.ts", "definition": "{ items: T[], total, page, pageSize, totalPages }" }, { "name": "DataChangeAction", "type": "type", "file": "types.ts", "definition": "'create' | 'update' | 'delete'", "usedBy": [ "data-access", "shared/lib/change-logger", "DataChangeLog" ] }, { "name": "DataChangeLog", "type": "interface", "file": "types.ts", "definition": "{ id, tableName, recordId, action, oldValue, newValue, changedBy, changedByName, ipAddress, createdAt }", "usedBy": [ "audit/data-access", "audit/actions" ] }, { "name": "DataChangeStat", "type": "interface", "file": "types.ts", "definition": "{ tableName: string, count: number }", "usedBy": [ "getDataChangeStats", "getDataChangeLogsAction" ] }, { "name": "DataChangeLogQueryParams", "type": "type", "file": "types.ts", "definition": "{ tableName?, recordId?, action?, userId?, page?, pageSize?, startDate?, endDate? }", "usedBy": [ "getDataChangeLogs", "getDataChangeLogsForExport", "getDataChangeLogsAction", "exportDataChangeLogsAction" ] } ], "lib": [ { "name": "buildAuditLogExport", "signature": "(logs: AuditLog[]) => { columns: ExportColumn[]; rows: Record[] }", "file": "export.ts", "purpose": "构建操作日志 Excel 导出数据(列定义 + 行映射纯函数,✅ 审计重构新增,从 actions 抽取)", "deps": [ "audit/types.AuditLog" ], "usedBy": [ "audit/actions.exportAuditLogsAction" ] }, { "name": "buildLoginLogExport", "signature": "(logs: LoginLog[]) => { columns: ExportColumn[]; rows: Record[] }", "file": "export.ts", "purpose": "构建登录日志 Excel 导出数据(✅ 审计重构新增)", "deps": [ "audit/types.LoginLog" ], "usedBy": [ "audit/actions.exportLoginLogsAction" ] }, { "name": "buildDataChangeLogExport", "signature": "(logs: DataChangeLog[]) => { columns: ExportColumn[]; rows: Record[] }", "file": "export.ts", "purpose": "构建数据变更日志 Excel 导出数据(✅ 审计重构新增)", "deps": [ "audit/types.DataChangeLog" ], "usedBy": [ "audit/actions.exportDataChangeLogsAction" ] } ], "hooks": [ { "name": "useLogPagination", "signature": "() => (page: number) => void", "file": "hooks/use-log-pagination.ts", "purpose": "分页 URL 状态管理 hook(useRouter + useSearchParams,三处重复逻辑抽取,✅ 审计重构新增)", "deps": [ "next/navigation.useRouter", "next/navigation.useSearchParams" ], "usedBy": [ "audit/components/audit-log-table.tsx", "audit/components/login-log-table.tsx", "audit/components/data-change-log-table.tsx" ] } ], "components": [ { "name": "AuditErrorBoundary", "file": "components/audit-error-boundary.tsx", "purpose": "审计模块错误边界(class 组件,捕获子树渲染错误展示 EmptyState + 重试按钮;✅ 审计重构新增;✅ v2 P1-2 namespace 从 common 改为 audit)", "config": { "namespace": "audit" }, "usedBy": [ "app/(dashboard)/admin/audit-logs/page.tsx", "app/(dashboard)/admin/audit-logs/login-logs/page.tsx", "app/(dashboard)/admin/audit-logs/data-changes/page.tsx", "app/(dashboard)/admin/audit-logs/overview/page.tsx" ] }, { "name": "AuditLogTableSkeleton", "file": "components/audit-log-table-skeleton.tsx", "purpose": "审计日志表格骨架屏(✅ 审计重构新增;✅ v2 P1-7 添加 aria-hidden=true role=presentation,避免屏幕阅读器朗读骨架)", "a11y": { "aria-hidden": true, "role": "presentation" }, "usedBy": [ "app/(dashboard)/admin/audit-logs/loading.tsx", "app/(dashboard)/admin/audit-logs/login-logs/loading.tsx", "app/(dashboard)/admin/audit-logs/data-changes/loading.tsx" ] }, { "name": "AuditLogTable", "file": "components/audit-log-table.tsx", "purpose": "操作日志表格(分页,✅ 审计重构:接入 useTranslations + useLocale + a11y aria-label)", "usedBy": [ "audit/components/audit-log-view.tsx" ] }, { "name": "AuditLogFilters", "file": "components/audit-log-filters.tsx", "purpose": "操作日志筛选器(模块/操作/状态/日期,✅ 审计重构:接入 i18n + aria-expanded;✅ v2 P1-5 日期 aria-label 区分 startDate/endDate)", "usedBy": [ "audit/components/audit-log-view.tsx" ] }, { "name": "AuditLogView", "file": "components/audit-log-view.tsx", "purpose": "操作日志视图(筛选+表格+分页)", "usedBy": [ "app/(dashboard)/admin/audit-logs/page.tsx" ] }, { "name": "AuditLogExportButton", "file": "components/audit-log-export-button.tsx", "purpose": "导出按钮(✅ 审计重构:接入 i18n;✅ v2 P1-3 接入 useAuditAnalytics().trackExport 埋点导出操作)", "usedBy": [ "app/(dashboard)/admin/audit-logs/page.tsx", "app/(dashboard)/admin/audit-logs/login-logs/page.tsx", "app/(dashboard)/admin/audit-logs/data-changes/page.tsx" ] }, { "name": "LoginLogTable", "file": "components/login-log-table.tsx", "purpose": "登录日志表格(分页,✅ 审计重构:接入 i18n + a11y)", "usedBy": [ "audit/components/login-log-view.tsx" ] }, { "name": "LoginLogFilters", "file": "components/login-log-filters.tsx", "purpose": "登录日志筛选器(操作/状态/日期,✅ 审计重构:接入 i18n + a11y;✅ v2 P1-5 日期 aria-label 区分 startDate/endDate)", "usedBy": [ "audit/components/login-log-view.tsx" ] }, { "name": "LoginLogView", "file": "components/login-log-view.tsx", "purpose": "登录日志视图(筛选+表格+分页)", "usedBy": [ "app/(dashboard)/admin/audit-logs/login-logs/page.tsx" ] }, { "name": "DataChangeLogTable", "file": "components/data-change-log-table.tsx", "purpose": "数据变更日志表格(含展开行,✅ 审计重构:从 281 行简化为 146 行,拆出筛选器)", "usedBy": [ "audit/components/data-change-log-view.tsx" ] }, { "name": "DataChangeLogFilters", "file": "components/data-change-log-filters.tsx", "purpose": "数据变更日志筛选器(表名/操作/日期,✅ 审计重构新增,从 data-change-log-table 拆出;✅ v2 P1-5 日期 aria-label 区分 startDate/endDate)", "usedBy": [ "audit/components/data-change-log-view.tsx" ] }, { "name": "DataChangeLogView", "file": "components/data-change-log-view.tsx", "purpose": "数据变更日志视图(筛选+表格+分页,✅ 审计重构新增)", "usedBy": [ "app/(dashboard)/admin/audit-logs/data-changes/page.tsx" ] }, { "name": "AuditLogDetailDialog", "file": "components/audit-log-detail-dialog.tsx", "purpose": "日志详情对话框(discriminated union 模式,支持 audit/login/dataChange 三种类型,✅ P2-2 新增)", "usedBy": [ "audit/components/audit-log-table.tsx", "audit/components/login-log-table.tsx", "audit/components/data-change-log-table.tsx" ] }, { "name": "AuditOverviewStatsBar", "file": "components/audit-overview-stats-bar.tsx", "purpose": "概览统计卡片栏(4 张 StatCard:今日审计事件/失败登录/数据变更/总数,✅ P2-4 新增)", "usedBy": [ "audit/components/audit-overview-view.tsx" ] }, { "name": "AuditActivityTrendChart", "file": "components/audit-activity-trend-chart.tsx", "purpose": "活动趋势折线图(近 7 天审计/登录/数据变更每日计数,ChartCardShell + TrendLineChart,✅ P2-4 新增)", "usedBy": [ "audit/components/audit-overview-view.tsx" ] }, { "name": "DataChangeDistributionChart", "file": "components/data-change-distribution-chart.tsx", "purpose": "数据变更分布柱状图(create/update/delete 占比,ChartCardShell + SimpleBarChart,✅ P2-4 新增)", "usedBy": [ "audit/components/audit-overview-view.tsx" ] }, { "name": "AuditOverviewView", "file": "components/audit-overview-view.tsx", "purpose": "概览页视图(统计卡片+趋势图+分布图+快捷入口+保留策略配置,✅ P2-4 新增)", "usedBy": [ "app/(dashboard)/admin/audit-logs/overview/page.tsx" ] }, { "name": "AuditRetentionSettings", "file": "components/audit-retention-settings.tsx", "purpose": "保留策略配置组件(保留天数 Input + 自动清理 Switch + 保存/清理按钮,✅ P2-5 新增;✅ v2 P0-1 + P1-3 移除直接 import Server Action,改用 useAuditService() + useAuditAnalytics() hook 触发埋点)", "usedBy": [ "audit/components/audit-overview-view.tsx" ] } ], "retention": [ { "name": "getAuditRetentionConfig", "signature": "() => Promise", "file": "retention.ts", "purpose": "获取审计日志保留策略配置(从 system_settings 表 audit_retention 分类读取,✅ P2-5 新增;✅ audit-P2-5 新增读取 loginLogRetentionDays 配置项,默认 365 天)", "deps": [ "settings.data-access-system-settings.getSystemSetting" ], "usedBy": [ "actions.getAuditRetentionConfigAction", "actions.purgeAuditLogsAction", "app/api/cron/audit-cleanup/route.ts" ] }, { "name": "saveAuditRetentionConfig", "signature": "(config: AuditRetentionConfig, updatedBy?: string) => Promise", "file": "retention.ts", "purpose": "保存审计日志保留策略配置(✅ P2-5 新增;✅ audit-P2-5 新增持久化 loginLogRetentionDays 配置项)", "deps": [ "settings.data-access-system-settings.upsertSystemSetting" ], "usedBy": [ "actions.saveAuditRetentionConfigAction" ] }, { "name": "purgeExpiredAuditLogs", "signature": "(retentionDays: number, loginLogRetentionDays?: number) => Promise", "file": "retention.ts", "purpose": "清理过期审计日志(删除三张表中早于保留阈值的记录,✅ P2-5 新增;✅ audit-P2-5 login_logs 可使用独立保留期 loginLogRetentionDays,默认与 retentionDays 一致,传入时使用独立阈值)", "deps": [ "shared.db", "shared.db.schema.auditLogs", "shared.db.schema.loginLogs", "shared.db.schema.dataChangeLogs" ], "usedBy": [ "actions.purgeAuditLogsAction", "app/api/cron/audit-cleanup/route.ts" ] }, { "name": "calculateRetentionThreshold", "signature": "(retentionDays: number, now?: Date) => Date", "file": "retention.ts", "purpose": "计算保留阈值日期(纯函数,便于单测,✅ P2-5 新增)", "deps": [], "usedBy": [ "retention.purgeExpiredAuditLogs" ] }, { "name": "isValidRetentionDays", "signature": "(days: number) => boolean", "file": "retention.ts", "purpose": "校验保留天数是否在合法范围内(30-3650 整数,纯函数,✅ P2-5 新增)", "deps": [], "usedBy": [ "retention.getAuditRetentionConfig", "retention.saveAuditRetentionConfig", "retention.purgeExpiredAuditLogs" ] } ], "services": [ { "name": "AuditService", "type": "interface", "file": "services/audit-service.tsx", "purpose": "审计数据服务接口(抽象所有数据获取与操作契约,✅ P2-8 新增)", "deps": [ "audit/types" ] }, { "name": "AuditAnalytics", "type": "interface", "file": "services/audit-service.tsx", "purpose": "审计监控埋点接口(预留关键操作埋点,✅ P2-8 新增)", "deps": [] }, { "name": "AuditServiceProvider", "type": "component", "file": "services/audit-service.tsx", "purpose": "审计服务 Provider(Context 注入角色特定实现,✅ P2-8 新增;✅ v2 P0-1 接线完成,overview page 注入 adminAuditService)", "deps": [ "react.createContext" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/overview/page.tsx" ] }, { "name": "useAuditService", "type": "hook", "file": "services/audit-service.tsx", "purpose": "获取当前注入的审计数据服务(✅ P2-8 新增;✅ v2 P0-1 接线完成)", "deps": [ "react.useContext" ], "usedBy": [ "audit/components/audit-retention-settings.tsx" ] }, { "name": "useAuditAnalytics", "type": "hook", "file": "services/audit-service.tsx", "purpose": "获取当前注入的审计监控埋点接口(✅ P2-8 新增;✅ v2 P1-3 接线完成)", "deps": [ "react.useContext" ], "usedBy": [ "audit/components/audit-retention-settings.tsx", "audit/components/audit-log-export-button.tsx" ] }, { "name": "adminAuditService", "type": "const", "file": "services/admin-audit-service.ts", "purpose": "管理员审计服务默认实现(✅ P2-8 新增;✅ v2 P0-1 改造为 Client-safe:移除 server-only 与 data-access 依赖;查询方法抛错强制走 RSC props 传递;写操作委托 Server Action)", "deps": [ "audit/actions.getAuditRetentionConfigAction", "audit/actions.saveAuditRetentionConfigAction", "audit/actions.purgeAuditLogsAction" ], "usedBy": [ "app/(dashboard)/admin/audit-logs/overview/page.tsx" ] }, { "name": "mockAuditService", "type": "const", "file": "services/mock-audit-service.ts", "purpose": "测试用 Mock 实现(所有方法返回空数据,✅ P2-8 新增;✅ v2 P0-2 消除 9 处 as 类型断言违规,为所有方法添加显式返回类型标注)", "deps": [ "audit/types" ] } ] } }, "announcements": { "path": "src/modules/announcements", "description": "通知公告系统:创建、编辑、发布、归档、删除公告,所有登录用户可查看已发布公告", "exports": { "actions": [ { "name": "createAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建公告(草稿/已发布);若直接发布则触发通知模块 sendBatchNotifications;V2-P0-2 新增通知标题 i18n 化,通过 getTranslations('announcements') 生成 notification.publishedTitle / publishedContent", "deps": [ "requirePermission", "data-access.insertAnnouncement", "data-access.getAnnouncementById", "notifications.sendBatchNotifications", "users.data-access.getAllUserIds", "users.data-access.getUserIdsByGradeId", "classes.data-access.getStudentIdsByClassId", "classes.data-access.getTeacherIdsByClassIds", "next-intl/server.getTranslations" ], "usedBy": [ "announcement-form.tsx" ] }, { "name": "updateAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新公告;若状态从非发布变为发布则触发通知模块 sendBatchNotifications;V2-P0-2 新增通知标题 i18n 化(同 createAnnouncementAction)", "deps": [ "requirePermission", "data-access.updateAnnouncementById", "data-access.getAnnouncementById", "notifications.sendBatchNotifications", "users.data-access.getAllUserIds", "users.data-access.getUserIdsByGradeId", "classes.data-access.getStudentIdsByClassId", "classes.data-access.getTeacherIdsByClassIds", "next-intl/server.getTranslations" ], "usedBy": [ "announcement-form.tsx" ] }, { "name": "deleteAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string) => Promise>", "purpose": "删除公告", "deps": [ "requirePermission", "data-access.deleteAnnouncementById" ], "usedBy": [ "announcement-detail.tsx" ] }, { "name": "publishAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string) => Promise>", "purpose": "发布公告(发布成功后触发通知模块 sendBatchNotifications);V2-P0-2 新增通知标题 i18n 化(同 createAnnouncementAction)", "deps": [ "requirePermission", "data-access.publishAnnouncementById", "data-access.getAnnouncementById", "notifications.sendBatchNotifications", "users.data-access.getAllUserIds", "users.data-access.getUserIdsByGradeId", "classes.data-access.getStudentIdsByClassId", "classes.data-access.getTeacherIdsByClassIds", "next-intl/server.getTranslations" ], "usedBy": [ "announcement-detail.tsx" ] }, { "name": "archiveAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string) => Promise>", "purpose": "归档公告", "deps": [ "requirePermission", "data-access.archiveAnnouncementById" ], "usedBy": [ "announcement-detail.tsx" ] }, { "name": "getAnnouncementsAction", "permission": "ANNOUNCEMENT_READ", "signature": "(params?: GetAnnouncementsParams) => Promise>", "purpose": "获取公告列表(所有登录用户可读)", "deps": [ "requirePermission", "data-access.getAnnouncements" ], "usedBy": [ "待扩展" ] }, { "name": "toggleAnnouncementPinAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string) => Promise>", "purpose": "V2-P2-13d 新增:切换公告置顶状态(置顶公告在列表中优先显示)", "deps": [ "requirePermission", "data-access.toggleAnnouncementPin" ], "usedBy": [ "待扩展" ] }, { "name": "markAnnouncementAsReadAction", "permission": "ANNOUNCEMENT_READ", "signature": "(announcementId: string) => Promise>", "purpose": "V2-P2-13d 新增:标记公告为已读(当前用户维度,幂等)", "deps": [ "requirePermission", "data-access.markAnnouncementAsRead" ], "usedBy": [ "待扩展" ] }, { "name": "getAnnouncementReadStatusAction", "permission": "ANNOUNCEMENT_READ", "signature": "(announcementIds: string[]) => Promise>>", "purpose": "V2-P2-13d 新增:批量获取当前用户对多个公告的已读状态(用于列表页标记已读/未读)", "deps": [ "requirePermission", "data-access.getAnnouncementReadStatusForUser" ], "usedBy": [ "待扩展" ] } ], "dataAccess": [ { "name": "getAnnouncements", "signature": "(params?: { status?, type?, page?, pageSize?, audience?: { gradeIds: string[]; classIds: string[] } }) => Promise", "file": "data-access.ts", "purpose": "P0-2 修复:audience 字段从单值 { gradeId?, classId? } 升级为数组 { gradeIds, classIds },解决家长多孩子/教师多班级时仅能匹配首个受众的截断 Bug;过滤逻辑改用 inArray", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "admin/announcements/page.tsx", "announcements/page.tsx" ] }, { "name": "getAnnouncementById", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "admin/announcements/[id]/edit/page.tsx", "publishAnnouncementAction", "updateAnnouncementAction", "deleteAnnouncementAction", "archiveAnnouncementAction" ] }, { "name": "getAnnouncementByIdForUser", "signature": "(id: string, userId: string, dataScope: { gradeIds: string[]; classIds: string[] }) => Promise", "file": "data-access.ts", "purpose": "P0-1 新增:用户端详情页专用查询。结合 status=published + 受众过滤 + 已读状态 + readCount 一次返回,防止越权访问草稿/他人班级公告。Audience 判断通过 isAnnouncementVisibleToAudience 纯函数完成", "deps": [ "shared.db", "shared.db.schema.announcements", "shared.db.schema.announcementReads", "data-access.isAnnouncementVisibleToAudience" ], "usedBy": [ "announcements/[id]/page.tsx", "markAnnouncementAsReadAction(资源可见性二次校验)" ] }, { "name": "isAnnouncementVisibleToAudience", "signature": "(announcement: { type: AnnouncementType; targetGradeId: string | null; targetClassId: string | null }, audience: { gradeIds: string[]; classIds: string[] }) => boolean", "file": "data-access.ts", "purpose": "P0-1 新增:纯函数,判断公告对受众是否可见。school 类型始终可见;grade 类型需 targetGradeId ∈ audience.gradeIds;class 类型需 targetClassId ∈ audience.classIds。可独立单测", "deps": [], "usedBy": [ "data-access.getAnnouncementByIdForUser" ] }, { "name": "resolveUserAudience", "signature": "(dataScope: { gradeIds: string[]; classIds: string[] }) => UserAudience | null", "file": "data-access.ts", "purpose": "P0-2 新增:从 dataScope 解析受众(合并所有 gradeIds/classIds 而非仅首个)。若 dataScope 为空返回 null 表示无可见受众", "deps": [], "usedBy": [ "data-access.getUserAnnouncementsPageData", "data-access.getAnnouncementByIdForUser" ] }, { "name": "resolveAnnouncementTargetUserIds", "signature": "(announcement: { type: AnnouncementType; targetGradeId: string | null; targetClassId: string | null }) => Promise", "file": "data-access.ts", "purpose": "P1-5 新增(从 actions.ts 下沉):根据公告 type 解析目标用户 ID 列表。school 类型调用 users.getAllUserIds;grade 类型调用 users.getUserIdsByGradeId;class 类型调用 classes.getStudentIdsByClassId + classes.getTeacherIdsByClassIds", "deps": [ "users.data-access.getAllUserIds", "users.data-access.getUserIdsByGradeId", "classes.data-access.getStudentIdsByClassId", "classes.data-access.getTeacherIdsByClassIds" ], "usedBy": [ "createAnnouncementAction", "updateAnnouncementAction", "publishAnnouncementAction" ] }, { "name": "insertAnnouncement", "signature": "(input: { title, content, type?, status?, targetGradeId?, targetClassId?, publishedAt?, authorId }) => Promise<{ announcementId: string }>", "file": "data-access.ts", "purpose": "插入公告(P1-2 新增,从 actions 下沉)", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "createAnnouncementAction" ] }, { "name": "updateAnnouncementById", "signature": "(id: string, input: Partial<{ title, content, type?, status?, targetGradeId?, targetClassId?, publishedAt? }>) => Promise", "file": "data-access.ts", "purpose": "按ID更新公告(P1-2 新增,从 actions 下沉)", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "updateAnnouncementAction" ] }, { "name": "deleteAnnouncementById", "signature": "(id: string) => Promise", "file": "data-access.ts", "purpose": "按ID删除公告(P1-2 新增,从 actions 下沉)", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "deleteAnnouncementAction" ] }, { "name": "publishAnnouncementById", "signature": "(id: string) => Promise", "file": "data-access.ts", "purpose": "发布公告(P1-2 新增,从 actions 下沉)", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "publishAnnouncementAction" ] }, { "name": "archiveAnnouncementById", "signature": "(id: string) => Promise", "file": "data-access.ts", "purpose": "归档公告(P1-2 新增,从 actions 下沉)", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "archiveAnnouncementAction" ] }, { "name": "toggleAnnouncementPin", "signature": "(id: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13d 新增;P1-6 优化:改用原子化 UPDATE ... SET isPinned = NOT isPinned(sql`${announcements.isPinned} = NOT ${announcements.isPinned}`),消除原 read-then-write 竞态条件", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "toggleAnnouncementPinAction" ] }, { "name": "markAnnouncementAsRead", "signature": "(announcementId: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13d 新增;P1-6 优化:改用 Drizzle onDuplicateKeyUpdate 实现幂等 upsert,替代原 select-then-insert 双查询", "deps": [ "shared.db", "shared.db.schema.announcementReads" ], "usedBy": [ "markAnnouncementAsReadAction" ] }, { "name": "isAnnouncementReadByUser", "signature": "(announcementId: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13d 新增:检查用户是否已读指定公告", "deps": [ "shared.db", "shared.db.schema.announcementReads" ], "usedBy": [ "待扩展" ] }, { "name": "getAnnouncementReadCount", "signature": "(announcementId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13d 新增:获取公告的已读用户数(管理端统计)", "deps": [ "shared.db", "shared.db.schema.announcementReads" ], "usedBy": [ "待扩展" ] }, { "name": "getAnnouncementReadStatusForUser", "signature": "(announcementIds: string[], userId: string) => Promise>", "file": "data-access.ts", "purpose": "V2-P2-13d 新增:批量获取用户对多个公告的已读状态(列表页标记已读/未读)", "deps": [ "shared.db", "shared.db.schema.announcementReads" ], "usedBy": [ "getAnnouncementReadStatusAction" ] }, { "name": "getAdminAnnouncementDetailPageData", "signature": "(id: string) => Promise<{ announcement: Announcement & { readCount: number }; classes: ClassOption[] } | null>", "file": "data-access.ts", "purpose": "P1-2 新增:管理端详情页编排函数。并行调用 getAnnouncementById + getAnnouncementReadCount + classes.getClassOptions,页面层仅调用单一函数。管理端 canManage=true,不做受众过滤", "deps": [ "data-access.getAnnouncementById", "data-access.getAnnouncementReadCount", "classes.data-access.getClassOptions" ], "usedBy": [ "admin/announcements/[id]/page.tsx" ] }, { "name": "countAnnouncements", "signature": "(params?: Omit) => Promise", "file": "data-access.ts", "purpose": "P2-6 新增:统计符合条件的公告总数(用于分页)。与 getAnnouncements 使用相同的过滤条件,但不应用 limit/offset。仅用户端 getUserAnnouncementsPageData 调用;管理端列表暂不分页", "deps": [ "shared.db", "shared.db.schema.announcements" ], "usedBy": [ "data-access.getUserAnnouncementsPageData" ] } ], "schemas": [ { "name": "CreateAnnouncementSchema", "type": "zod", "file": "schema.ts", "definition": "{ title, content, type?, status?, targetGradeId?, targetClassId?, publishedAt? }", "usedBy": [ "createAnnouncementAction" ] }, { "name": "UpdateAnnouncementSchema", "type": "zod", "file": "schema.ts", "definition": "{ title, content, type?, status?, targetGradeId?, targetClassId?, publishedAt? }", "usedBy": [ "updateAnnouncementAction" ] } ], "types": [ { "name": "Announcement", "type": "interface", "file": "types.ts", "definition": "{ id, title, content, type, status, targetGradeId, targetClassId, authorId, authorName, publishedAt, isPinned, isReadByCurrentUser?, readCount?, createdAt, updatedAt }", "usedBy": [ "announcements/components", "页面" ] }, { "name": "AnnouncementListItem", "type": "type", "file": "types.ts", "definition": "= Announcement", "usedBy": [ "列表页" ] }, { "name": "AnnouncementStatus", "type": "type", "file": "types.ts", "definition": "\"draft\" | \"published\" | \"archived\"", "usedBy": [ "data-access", "components" ] }, { "name": "AnnouncementType", "type": "type", "file": "types.ts", "definition": "\"school\" | \"grade\" | \"class\"", "usedBy": [ "data-access", "components" ] }, { "name": "GetAnnouncementsParams", "type": "interface", "file": "types.ts", "definition": "{ status?, type?, page?, pageSize?, audience?: { gradeIds: string[]; classIds: string[] } }", "purpose": "P0-2 修复:audience 字段从单值 { gradeId?, classId? } 升级为数组 { gradeIds, classIds },解决家长多孩子/教师多班级时仅匹配首个受众的截断 Bug", "usedBy": [ "getAnnouncements", "getAnnouncementsAction" ] }, { "name": "UserAudience", "type": "interface", "file": "types.ts", "definition": "{ gradeIds: string[]; classIds: string[] }", "purpose": "P0-2 新增:用户受众信息,由 data-access.resolveUserAudience 解析得到", "usedBy": [ "data-access.resolveUserAudience", "data-access.getAnnouncementByIdForUser", "data-access.getUserAnnouncementsPageData" ] }, { "name": "AnnouncementRead", "type": "interface", "file": "types.ts", "definition": "{ id, announcementId, userId, readAt }", "usedBy": [ "data-access.announcement_reads 相关函数" ] }, { "name": "AnnouncementsService", "type": "interface", "file": "types.ts", "definition": "{ create, update, delete, publish, archive, togglePin, markRead, getReadStatus }", "purpose": "P1-3 新增:公告服务接口(依赖注入抽象)。组件层通过 useAnnouncementsService() 消费此接口而非直接 import actions,实现完全解耦:默认实现调用真实 Server Actions(default-announcements-service.ts),测试时可注入 mock 实现,未来不同角色可注入不同实现(家长只读、教师可编辑班级公告等)。所有方法返回 ActionState,与 Server Action 签名保持一致便于无缝替换", "usedBy": [ "components/announcements-service-context.tsx", "components/default-announcements-service.ts", "components/announcement-card.tsx", "components/announcement-detail.tsx", "components/announcement-form.tsx", "components/announcement-list.tsx" ] }, { "name": "PaginatedAnnouncements", "type": "interface", "file": "types.ts", "definition": "{ items: Announcement[]; total: number; page: number; pageSize: number }", "purpose": "P2-6 新增:分页查询结果。由 getUserAnnouncementsPageData 返回,供 AnnouncementPagination 组件计算页码", "usedBy": [ "data-access.getUserAnnouncementsPageData", "app/(dashboard)/announcements/page.tsx", "components/announcement-list.tsx (pagination prop)" ] } ], "components": [ { "name": "AnnouncementsServiceProvider", "file": "components/announcements-service-context.tsx", "purpose": "P1-3 新增:React Context Provider,向子组件注入 AnnouncementsService 实现。仿照 settings-service-context.tsx 模式。Client Component(\"use client\")。未包裹 Provider 时 useAnnouncementsService 抛错(强约束)", "props": "{ service: AnnouncementsService; children: ReactNode }" }, { "name": "useAnnouncementsService", "file": "components/announcements-service-context.tsx", "purpose": "P1-3 新增:消费 AnnouncementsService 的 Hook。返回当前 Context 中的 service 实例。所有交互型组件(Card/Detail/Form/List)通过此 Hook 获取 service,不直接 import actions" }, { "name": "DefaultAnnouncementsService", "file": "components/default-announcements-service.ts", "purpose": "P1-3 新增:AnnouncementsService 接口的默认实现,代理到真实 Server Actions(createAnnouncementAction 等)。生产环境使用;测试时可被 mock 实现替换" }, { "name": "AnnouncementListSkeleton", "file": "components/announcement-list-skeleton.tsx", "purpose": "P2-5 新增:抽取共享骨架屏组件,被 /announcements/loading.tsx 与 /admin/announcements/loading.tsx 共用,消除两处重复骨架屏 JSX。支持 showCreateButton prop 控制是否显示新建按钮骨架(仅管理端为 true)", "props": "{ showCreateButton?: boolean }" }, { "name": "AnnouncementPagination", "file": "components/announcement-pagination.tsx", "purpose": "P2-6 新增:公告列表分页组件。通过 URL ?page=N 持久化当前页码(RSC 重新渲染模式,与 ?status= 过滤机制一致),不引入客户端路由状态。设计要点:服务端渲染友好(仅渲染 a 链接,无 onClick 路由副作用);a11y(上一页/下一页 aria-label,当前页 aria-current=page);边界处理(total=0 不渲染、首页禁用上一页、末页禁用下一页);窗口策略(总页数 > 7 时显示首末页 + 当前页 ±1 + 省略号)", "props": "{ page: number; pageSize: number; total: number; buildHref: (page: number) => string; className?: string }" }, { "name": "AnnouncementList", "file": "components/announcement-list.tsx", "purpose": "公告列表(支持状态筛选);V2-P1-1:纯服务端过滤,Select 切换更新 URL ?status= 触发 RSC 重新渲染;V3:新增 detailHrefPrefix prop 替代 detailHrefBuilder 函数 prop,解决 Next.js 16 Server→Client 序列化限制;P1-2:用户端通过 useAnnouncementsService().getReadStatus 批量获取已读状态并传入卡片;P2-4:删除死 prop detailHrefBuilder;P2-6:新增 pagination prop(含 page/pageSize/total/buildPageHref),传入时在列表底部渲染 AnnouncementPagination" }, { "name": "AnnouncementCard", "file": "components/announcement-card.tsx", "purpose": "单条公告卡片;P1-3:通过 useAnnouncementsService() 消费 togglePin,不直接 import actions;P2-1 a11y:置顶按钮改为绝对定位 + onKeyDown 处理 Enter/Space,脱离外层 Link 语义避免嵌套交互元素;P1-2:新增 isRead prop 做已读/未读视觉区分(ring-2 ring-primary/40)" }, { "name": "AnnouncementForm", "file": "components/announcement-form.tsx", "purpose": "创建/编辑表单;V2-P1-4:fieldErrors + aria-invalid 字段级服务端校验错误展示;P1-3:通过 useAnnouncementsService() 消费 create/update;P1-4/P1-8:新增 onSuccess / onCancel 回调优先于 successHref / cancelHref 兜底,便于在 Dialog 中复用并解耦路由跳转行为" }, { "name": "AnnouncementDetail", "file": "components/announcement-detail.tsx", "purpose": "详情查看(含发布/归档/删除操作);P1-3:所有 Action 调用改用 useAnnouncementsService()(service.publish/archive/delete/togglePin/markRead)" }, { "name": "AdminAnnouncementsView", "file": "components/admin-announcements-view.tsx", "purpose": "管理端公告视图(列表+创建对话框);P1-3:包裹 AnnouncementsServiceProvider;P1-8:Dialog 内表单通过 onSuccess={handleFormSuccess} 关闭弹窗而非路由跳转" } ] } }, "files": { "path": "src/modules/files", "description": "文件上传与管理:通过 actions.ts 编排层处理文件上传(storageProvider 抽象保存到 public/uploads/YYYY-MM/),记录文件元数据到 DB,支持按关联资源(exam/textbook/question/announcement/homework/user_avatar)多态查询、下载、预览与删除。2026-06-25 P0-P2 审计重构:新增 actions.ts 编排层(权限+Zod+trackEvent+logAudit)、schema.ts Zod 校验、hooks/ 目录抽取 3 个自定义 hooks,API 路由全部委托 actions", "exports": { "dataAccess": [ { "name": "createFileAttachment", "signature": "(data: CreateFileAttachmentInput) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.fileAttachments" ], "usedBy": [ "files/actions.uploadFileAction" ] }, { "name": "getFileAttachment", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.fileAttachments" ], "usedBy": [ "files/actions.getFileAction" ] }, { "name": "getFileAttachmentsByTarget", "signature": "(targetType: string, targetId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.fileAttachments" ], "usedBy": [ "按关联资源查询文件列表" ] }, { "name": "getFileAttachmentsByUploader", "signature": "(uploaderId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.fileAttachments" ], "usedBy": [ "按上传者查询文件列表" ] }, { "name": "getAllFileAttachments", "signature": "(limit?: number) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.fileAttachments" ], "usedBy": [ "files/actions.getFileListAction" ] }, { "name": "deleteFileAttachment", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.fileAttachments" ], "usedBy": [ "files/actions.deleteFileAction", "settings/actions-avatar.cleanupOldAvatarFile" ] }, { "name": "deleteFileAttachments", "signature": "(ids: string[]) => Promise", "file": "data-access.ts", "purpose": "批量删除文件附件记录(仅删 DB 行,磁盘文件由调用方处理;失败时回退到逐条删除)", "deps": [ "shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.inArray" ], "usedBy": [ "files/actions.batchDeleteFilesAction" ] }, { "name": "getFileAttachmentsWithFilters", "signature": "(params: FileAttachmentQueryParams) => Promise", "file": "data-access.ts", "purpose": "按 mimeType(精确或前缀匹配)与 search(originalName/filename 模糊匹配)筛选文件列表,支持 limit/offset 分页(v3 修复:conditions 显式标注 SQL[] 类型,消除隐式 any[])", "deps": [ "shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.like", "drizzle-orm.or", "drizzle-orm.and", "drizzle-orm.SQL" ], "usedBy": [ "files/actions.getFileListAction" ] }, { "name": "getFileStats", "signature": "() => Promise", "file": "data-access.ts", "purpose": "获取文件统计(总数、总大小、按 mimeType 分组的 count/size)", "deps": [ "shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.count", "drizzle-orm.sql" ], "usedBy": [ "files/actions.getFileStatsAction" ] }, { "name": "getFileAttachmentsByIds", "signature": "(ids: string[]) => Promise", "file": "data-access.ts", "purpose": "按 ID 列表批量查询文件(用于批量删除前获取磁盘路径)", "deps": [ "shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.inArray" ], "usedBy": [ "files/actions.batchDeleteFilesAction" ] }, { "name": "getFileByUrl", "signature": "(url: string) => Promise", "file": "data-access.ts", "purpose": "按 url 字段查询文件附件记录(2026-06-25 新增,用于按 URL 反查文件元数据,例如头像删除场景)", "deps": [ "shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.eq" ], "usedBy": [ "settings/actions-avatar.cleanupOldAvatarFile" ] } ], "types": [ { "name": "FileAttachment", "type": "interface", "file": "types.ts", "definition": "{ id, filename, originalName, mimeType, size, storagePath, url, uploaderId, targetType, targetId, createdAt }", "usedBy": [ "files/components", "data-access", "API 路由" ] }, { "name": "FileUploadResult", "type": "interface", "file": "types.ts", "definition": "{ id, url, filename, originalName, size, mimeType }", "usedBy": [ "app/api/upload/route.ts 响应", "file-upload.tsx 回调" ] }, { "name": "FileTargetType", "type": "type", "file": "types.ts", "definition": "\"exam\" | \"textbook\" | \"question\" | \"announcement\" | \"homework\" | \"user_avatar\"", "purpose": "文件附件关联资源类型枚举。2026-06-24 新增 \"homework\" 枚举值,用于学生答题扫描图附件(targetId = homeworkSubmissions.id);2026-06-25 新增 \"user_avatar\" 枚举值,用于用户头像上传场景(targetId = users.id)", "usedBy": [ "types.FileAttachment.targetType", "file-upload.tsx", "app/api/upload/route.ts (VALID_TARGET_TYPES)", "homework/actions.getScansAction", "homework/actions.deleteScanAction", "homework/components/scan-uploader.tsx", "settings/components/avatar-upload.tsx" ] }, { "name": "CreateFileAttachmentInput", "type": "interface", "file": "types.ts", "definition": "{ id, filename, originalName, mimeType, size, storagePath, url, uploaderId, targetType?, targetId? }", "usedBy": [ "createFileAttachment" ] }, { "name": "FileAttachmentQueryParams", "type": "interface", "file": "types.ts", "definition": "{ mimeType?, search?, limit?, offset? }", "usedBy": [ "getFileAttachmentsWithFilters" ] }, { "name": "FileStats", "type": "interface", "file": "types.ts", "definition": "{ totalCount, totalSize, byType: Array<{ mimeType, count, size }> }", "usedBy": [ "getFileStats" ] }, { "name": "BatchDeleteResult", "type": "interface", "file": "types.ts", "definition": "{ success, deletedCount, failedIds: string[] }", "usedBy": [ "deleteFileAttachments", "app/api/files/batch-delete/route.ts" ] } ], "components": [ { "name": "FileUpload", "file": "components/file-upload.tsx", "purpose": "文件上传组件(拖拽+点击上传,进度条,文件类型校验,接入 useFileUpload hook 调用 /api/upload;2026-06-25 接入 i18n + ARIA 标签)" }, { "name": "FilePreview", "file": "components/file-preview.tsx", "purpose": "文件预览(图片直接预览 + useImageZoom 缩放,PDF iframe,其他下载;2026-06-25 接入 useFilePreview hook + i18n)" }, { "name": "FilePreviewDialog", "file": "components/file-preview-dialog.tsx", "purpose": "文件预览弹窗(Dialog 包裹 FilePreview,支持自定义触发器;2026-06-25 补录 + i18n)" }, { "name": "FileIcon", "file": "components/file-icon.tsx", "purpose": "根据 MIME 类型显示不同图标与颜色" }, { "name": "AdminFilesView", "file": "components/admin-files-view.tsx", "purpose": "管理端文件视图(统计卡片+上传+筛选+列表+批量删除;2026-06-25 接入 useFileBatchOperations hook + 三层 WidgetBoundary 分区 + i18n + renderTypeLabel 类型安全)" } ], "actions": [ { "name": "uploadFileAction", "signature": "(file: File, rawMetadata: UploadMetadataInput) => Promise>", "file": "actions.ts", "purpose": "上传文件编排:requirePermission(FILE_UPLOAD) + Zod 校验 + storageProvider.save + createFileAttachment + trackEvent(file.uploaded/upload_failed) + logAudit", "deps": [ "shared.lib.auth-guard.requirePermission", "shared.lib.storage-provider.storageProvider", "shared.lib.file-storage", "shared.lib.track-event.trackEvent", "shared.lib.audit-logger.logAudit", "files.schema.UploadMetadataSchema", "files.data-access.createFileAttachment" ], "usedBy": [ "app/api/upload/route.ts" ] }, { "name": "getFileAction", "signature": "(id: string) => Promise>", "file": "actions.ts", "purpose": "获取文件编排:requirePermission(FILE_READ) + ownership 检查(非 FILE_DELETE 持有者只能读自己上传的)+ trackEvent(file.viewed)", "deps": [ "shared.lib.auth-guard.requirePermission", "shared.lib.track-event.trackEvent", "files.data-access.getFileAttachment" ], "usedBy": [ "app/api/files/[id]/route.ts" ] }, { "name": "deleteFileAction", "signature": "(id: string) => Promise>", "file": "actions.ts", "purpose": "删除文件编排:requirePermission(FILE_DELETE) + storageProvider.delete + deleteFileAttachment + trackEvent(file.deleted) + logAudit", "deps": [ "shared.lib.auth-guard.requirePermission", "shared.lib.storage-provider.storageProvider", "shared.lib.track-event.trackEvent", "shared.lib.audit-logger.logAudit", "files.data-access.getFileAttachment", "files.data-access.deleteFileAttachment" ], "usedBy": [ "app/api/files/[id]/route.ts" ] }, { "name": "batchDeleteFilesAction", "signature": "(rawIds: unknown) => Promise>", "file": "actions.ts", "purpose": "批量删除编排:requirePermission(FILE_DELETE) + Zod 校验(max 100 ids)+ getFileAttachmentsByIds + storageProvider.delete + deleteFileAttachments + trackEvent(file.batch_deleted) + logAudit", "deps": [ "shared.lib.auth-guard.requirePermission", "shared.lib.storage-provider.storageProvider", "shared.lib.track-event.trackEvent", "shared.lib.audit-logger.logAudit", "files.schema.BatchDeleteSchema", "files.data-access.getFileAttachmentsByIds", "files.data-access.deleteFileAttachments" ], "usedBy": [ "app/api/files/batch-delete/route.ts" ] }, { "name": "getFileListAction", "signature": "(params?: FileListQueryInput) => Promise>", "file": "actions.ts", "purpose": "文件列表编排:requirePermission(FILE_READ) + Zod 校验 + getFileAttachmentsWithFilters(管理员场景)/ getAllFileAttachments", "deps": [ "shared.lib.auth-guard.requirePermission", "files.schema.FileListQuerySchema", "files.data-access.getFileAttachmentsWithFilters", "files.data-access.getAllFileAttachments" ], "usedBy": [ "app/(dashboard)/admin/files/page.tsx" ] }, { "name": "getFileStatsAction", "signature": "() => Promise>", "file": "actions.ts", "purpose": "文件统计编排:requirePermission(FILE_READ) + getFileStats", "deps": [ "shared.lib.auth-guard.requirePermission", "files.data-access.getFileStats" ], "usedBy": [ "app/(dashboard)/admin/files/page.tsx" ] } ], "hooks": [ { "name": "useFileUpload", "file": "hooks/use-file-upload.ts", "purpose": "封装 XHR 上传逻辑(进度、状态机 isUploading/isSuccess/isError、文件类型/大小校验、调用 /api/upload)", "deps": [ "files.types.FileUploadResult" ], "usedBy": [ "files/components/file-upload.tsx" ] }, { "name": "useFileBatchOperations", "file": "hooks/use-file-batch-operations.ts", "purpose": "客户端筛选(typeFilter/search)、选择(selectedIds/allSelected/someSelected/toggleAll/toggleOne)、批量删除(handleBatchDelete 调用 batchDeleteFilesAction)", "deps": [ "files.actions.batchDeleteFilesAction", "files.types.FileAttachment" ], "usedBy": [ "files/components/admin-files-view.tsx" ] }, { "name": "useFilePreview", "file": "hooks/use-file-preview.ts", "purpose": "文件预览状态管理(isOpen/setOpen)", "deps": [ "react" ], "usedBy": [ "files/components/file-preview.tsx" ] }, { "name": "useImageZoom", "file": "hooks/use-file-preview.ts", "purpose": "图片缩放交互(zoom/scale/rotate/reset/toggleZoom,wheel+click 双模式)", "deps": [ "react" ], "usedBy": [ "files/components/file-preview.tsx" ] } ], "schema": [ { "name": "UploadMetadataSchema", "file": "schema.ts", "purpose": "Zod 校验上传元数据(targetType 枚举 + targetId 可选 + fileSize 正整数)", "usedBy": [ "files/actions.uploadFileAction" ] }, { "name": "BatchDeleteSchema", "file": "schema.ts", "purpose": "Zod 校验批量删除输入(ids 数组 max 100 元素,每个为非空字符串)", "usedBy": [ "files/actions.batchDeleteFilesAction" ] }, { "name": "FileListQuerySchema", "file": "schema.ts", "purpose": "Zod 校验列表查询参数(mimeType/search/limit/offset)", "usedBy": [ "files/actions.getFileListAction" ] }, { "name": "FileTargetTypeSchema", "file": "schema.ts", "purpose": "Zod 枚举校验 FileTargetType(exam/textbook/question/announcement/homework/user_avatar)", "usedBy": [ "files.schema.UploadMetadataSchema" ] }, { "name": "validateFileSize", "file": "schema.ts", "purpose": "工具函数:校验文件大小是否在 MAX_FILE_SIZE 范围内", "usedBy": [ "files.actions.uploadFileAction" ] } ] } }, "grades": { "path": "src/modules/grades", "description": "成绩分析模块:成绩录入(单条+批量)、查询(按班级/科目/考试/学期过滤)、统计报表(均分、中位数、标准差、及格率、优秀率、排名)、Excel 导出(成绩明细+统计汇总/班级多科目横向对比)、趋势对比分析(成绩趋势、班级对比、科目对比、分数分布、排名趋势)", "exports": { "dataAccess": [ { "name": "getGradeRecords", "signature": "(params: GradeQueryParams & { scope: DataScope; currentUserId?: string; limit?: number; offset?: number }) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classes", "shared.db.schema.classEnrollments", "shared.db.schema.subjects", "shared.db.schema.users" ], "usedBy": [ "grades/actions.getGradeRecordsAction" ] }, { "name": "getGradeRecordById", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords" ], "usedBy": [ "grades/actions.getGradeRecordByIdAction" ] }, { "name": "createGradeRecord", "signature": "(data: CreateGradeRecordInput, recordedBy: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords" ], "usedBy": [ "grades/actions.createGradeRecordAction" ] }, { "name": "batchCreateGradeRecords", "signature": "(data: BatchCreateGradeRecordInput, recordedBy: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords" ], "usedBy": [ "grades/actions.batchCreateGradeRecordsAction" ] }, { "name": "updateGradeRecord", "signature": "(id: string, data: UpdateGradeRecordInput) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords" ], "usedBy": [ "grades/actions.updateGradeRecordAction" ] }, { "name": "deleteGradeRecord", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords" ], "usedBy": [ "grades/actions.deleteGradeRecordAction" ] }, { "name": "bulkDeleteGradeRecords", "signature": "(ids: string[]) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords" ], "usedBy": [ "grades/actions.bulkDeleteGradeRecordsAction" ] }, { "name": "getClassGradeStats", "signature": "(classId: string, subjectId?: string, examId?: string, scope?: DataScope, currentUserId?: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "grades/lib/scope-filter.buildScopeClassFilter" ], "usedBy": [ "grades/data-access.getClassGradeStatsWithMeta" ] }, { "name": "getClassGradeStatsWithMeta", "signature": "(classId: string, subjectId?: string, examId?: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classes", "shared.db.schema.subjects" ], "usedBy": [ "grades/actions.getClassGradeStatsAction" ] }, { "name": "getStudentGradeSummary", "signature": "(studentId: string, scope?: DataScope) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.subjects", "classes/data-access.getStudentActiveClassId" ], "usedBy": [ "grades/actions.getStudentGradeSummaryAction" ] }, { "name": "getClassRanking", "signature": "(classId: string, subjectId?: string, examId?: string, scope?: DataScope, currentUserId?: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.users", "grades/lib/scope-filter.buildScopeClassFilter", "users/data-access.getUserNamesByIds" ], "usedBy": [ "grades/actions.getClassRankingAction" ] }, { "name": "getClassStudentsForEntry", "signature": "(classId: string, scope?: DataScope) => Promise<{ id: string; name: string; email: string }[]>", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.classEnrollments", "shared.db.schema.users" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "getGradeTrend", "signature": "(params: { studentId; subjectId?; semester?; examId?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classEnrollments" ], "usedBy": [ "grades/actions-analytics.getGradeTrendAction", "teacher/grades/analytics" ] }, { "name": "getClassComparison", "signature": "(params: { gradeId; subjectId; examId?; semester?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classes" ], "usedBy": [ "grades/actions-analytics.getClassComparisonAction", "teacher/grades/analytics" ] }, { "name": "getSubjectComparison", "signature": "(params: { classId; examId?; semester?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.subjects" ], "usedBy": [ "grades/actions-analytics.getSubjectComparisonAction", "teacher/grades/analytics" ] }, { "name": "getGradeDistribution", "signature": "(params: { classId; subjectId?; examId?; semester?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords" ], "usedBy": [ "grades/actions-analytics.getGradeDistributionAction", "teacher/grades/analytics" ] }, { "name": "getGradeDistributionByGradeId", "signature": "(params: { gradeId; subjectId?; examId?; semester?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "purpose": "v4-P2-7 新增:年级仪表盘维度1,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats,返回整体分布 + 按班级拆分", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "classes/data-access.getClassesByGradeId" ], "usedBy": [ "grades/actions-analytics.getGradeDistributionByGradeIdAction", "management/grade/dashboard" ] }, { "name": "getRankingTrend", "signature": "(studentId: string, subjectId?, semester?, scope?: DataScope) => Promise", "file": "data-access-ranking.ts", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classEnrollments" ], "usedBy": [ "grades/actions-analytics.getRankingTrendAction" ] }, { "name": "saveGradeDraft", "signature": "(params: { userId: string; classId: string; subjectId: string; type: string; data: GradeDraftData }) => Promise", "file": "data-access-drafts.ts", "purpose": "v3-P2 新增:保存成绩录入草稿到 DB(upsert,按 userId+classId+subjectId+type 唯一键)。P1-2 重构:从 data-access.ts 迁移至 data-access-drafts.ts 独立文件,使 data-access.ts 回到 800 行以内", "deps": [ "shared.db", "shared.db.schema.gradeDrafts", "grades/lib/type-guards.isGradeDraftData" ], "usedBy": [ "grades/actions-draft.saveGradeDraftAction" ] }, { "name": "getGradeDraft", "signature": "(params: { userId: string; classId: string; subjectId: string; type: string }) => Promise", "file": "data-access-drafts.ts", "purpose": "v3-P2 新增:获取成绩录入草稿(24 小时过期,超期返回 null)。P1-2 重构:从 data-access.ts 迁移至 data-access-drafts.ts。使用 cache() memoize,isGradeDraftData 类型守卫安全解析 DB JSON 列", "deps": [ "shared.db", "shared.db.schema.gradeDrafts", "grades/lib/type-guards.isGradeDraftData" ], "usedBy": [ "grades/actions-draft.getGradeDraftAction" ] }, { "name": "deleteGradeDraft", "signature": "(params: { userId: string; classId: string; subjectId: string; type: string }) => Promise", "file": "data-access-drafts.ts", "purpose": "v3-P2 新增:删除成绩录入草稿。P1-2 重构:从 data-access.ts 迁移至 data-access-drafts.ts", "deps": [ "shared.db", "shared.db.schema.gradeDrafts" ], "usedBy": [ "grades/actions-draft.deleteGradeDraftAction" ] }, { "name": "acquireDraftLock", "signature": "(params: { classId: string; subjectId: string; type: string; userId: string }) => Promise", "file": "data-access-drafts.ts", "purpose": "P3-6 新增:获取协同录入锁。悲观锁语义+乐观 TTL(5分钟)+令牌续期。锁以 grade_drafts 行为载体(lockedBy/lockedAt/lockToken 列)。4 种情况处理:无锁/续期/拒绝/抢占", "deps": [ "shared.db", "shared.db.schema.gradeDrafts" ], "usedBy": [ "grades/actions-lock.acquireDraftLockAction" ] }, { "name": "renewDraftLock", "signature": "(params: { classId: string; subjectId: string; type: string; userId: string; lockToken: string }) => Promise", "file": "data-access-drafts.ts", "purpose": "P3-6 新增:续期锁(心跳)。校验 lockToken+userId 匹配,返回新 token", "deps": [ "shared.db", "shared.db.schema.gradeDrafts" ], "usedBy": [ "grades/actions-lock.renewDraftLockAction" ] }, { "name": "releaseDraftLock", "signature": "(params: { classId: string; subjectId: string; type: string; userId: string; lockToken: string }) => Promise", "file": "data-access-drafts.ts", "purpose": "P3-6 新增:释放锁。仅 token+userId 匹配时释放", "deps": [ "shared.db", "shared.db.schema.gradeDrafts" ], "usedBy": [ "grades/actions-lock.releaseDraftLockAction" ] }, { "name": "createGradeAppeal", "signature": "(params: { gradeRecordId: string; studentId: string; originalScore: string; reason: string; expectedScore?: number }) => Promise", "file": "data-access-appeals.ts", "purpose": "P3-7 新增:创建成绩申诉。originalScore 快照留痕", "deps": [ "shared.db", "shared.db.schema.gradeAppeals" ], "usedBy": [ "grades/actions-appeal.createGradeAppealAction" ] }, { "name": "reviewGradeAppeal", "signature": "(params: { appealId: string; reviewerId: string; decision: 'approved'|'rejected'; reviewComment: string; adjustedScore?: number }) => Promise", "file": "data-access-appeals.ts", "purpose": "P3-7 新增:教师复核申诉。approve 且 adjustedScore 有值时同步更新 grade_records.score", "deps": [ "shared.db", "shared.db.schema.gradeAppeals", "shared.db.schema.gradeRecords" ], "usedBy": [ "grades/actions-appeal.reviewGradeAppealAction" ] }, { "name": "getReportCardData", "signature": "(studentId: string, scope: DataScope, query?: ReportCardQuery) => Promise", "file": "lib/report-card.ts", "purpose": "P3-1 新增:聚合成绩报告卡数据(学生成绩+班级信息+班主任+学年+科目排名+总体统计)。三层 scope 校验:owned/children/class_taught", "deps": [ "data-access.getStudentRankInClass", "data-access.getClassNamesByIds", "stats-service.computeAverageScore" ], "usedBy": [ "待扩展" ] }, { "name": "parseGradeImportData", "signature": "(rows: GradeImportRow[], students: { id: string; name: string }[]) => GradeImportValidation", "file": "import-export.ts", "purpose": "P3-10 新增:匹配 Excel 导入的学生姓名到 ID,处理重名(标记为 unmatchable),返回 valid/invalid/unmatched", "deps": [ "classes/data-access.getClassActiveStudentsWithInfo", "shared/lib/excel" ], "usedBy": [ "grades/actions-import.importGradesFromExcelAction" ] }, { "name": "getExamOptionsForGrades", "signature": "(params: { classId?: string; subjectId?: string }) => Promise<{ id: string; title: string }[]>", "file": "data-access-analytics.ts", "purpose": "v3-P2 新增:获取指定班级/科目下有成绩记录的考试列表(供分析页考试筛选)", "deps": [ "shared.db", "shared.db.schema.gradeRecords" ], "usedBy": [ "teacher/grades/analytics/page.tsx" ] }, { "name": "getSchoolWideGradeSummary", "signature": "() => Promise", "file": "data-access-analytics.ts", "purpose": "v3-P2 新增:获取全校各年级成绩汇总(管理员视图,按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总)", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classes", "shared.db.schema.subjects" ], "usedBy": [ "admin/school/grades/insights/page.tsx" ] }, { "name": "batchCreateGradeRecordsByExam", "signature": "(data: BatchGradeEntryByExamInput, recordedBy: string) => Promise", "file": "data-access-exam-entry.ts", "purpose": "2026-06-24 新增:按试卷批量录入成绩(每题得分)。单事务写入:grade_records(score=各题得分之和)+ grade_record_answers(每题得分明细)+ 投影到 exam_submissions(status='graded') + submission_answers(answerContent=null,只有 score)。投影设计使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩。返回创建的 gradeRecordId 列表供前端撤销。P1-2 重构:从 data-access.ts 迁移至 data-access-exam-entry.ts 独立文件,使 data-access.ts 回到 800 行以内", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.gradeRecordAnswers", "shared.db.schema.examQuestions", "shared.db.schema.examSubmissions", "shared.db.schema.submissionAnswers" ], "usedBy": [ "grades/actions.batchCreateGradeRecordsByExamAction" ] }, { "name": "getStudentRankInClass", "signature": "(studentId: string, classId: string, scope?: DataScope) => Promise", "file": "data-access.ts", "purpose": "P2-4 新增:计算单个学生在班级中的排名。解耦自 getClassRanking:避免获取整个班级排名(含姓名查询)只为查单个学生排名。使用两步法:查询该学生平均分 → 查询所有学生平均分 → 计算严格高于该学生平均分的不同平均分数(处理并列)。返回 rank(0 表示无成绩)", "deps": [ "shared.db", "shared.db.schema.gradeRecords", "grades/lib/scope-filter.buildScopeClassFilter", "grades/lib/grade-utils.toNumber" ], "usedBy": [ "grades/data-access.getStudentGradeSummary" ] } ], "actions": [ { "name": "createGradeRecordAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "创建单条成绩记录。v4-P1-6 增强:成绩录入后通知学生和家长(调用 notifyGradeEntered,内部使用 parent/data-access.getParentIdsByStudentIds 批量查询家长 ID),通知失败不阻断录入;v3-P1-5:录入后更新诊断掌握度(updateMasteryFromExamScore)", "deps": [ "requirePermission", "data-access.createGradeRecord", "diagnostic/data-access.updateMasteryFromExamScore", "grades/lib/notify.notifyGradeEntered", "revalidatePath" ], "usedBy": [ "grades/components/grade-record-form" ] }, { "name": "batchCreateGradeRecordsAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "批量创建成绩记录(db.transaction 原子操作)。v4-P1-6 增强:批量录入后通知所有相关学生和家长(调用 notifyGradeEntered);v3-P1-5:录入后更新诊断掌握度;v3-P2-3:返回创建的记录 ID 列表供前端撤销", "deps": [ "requirePermission", "data-access.batchCreateGradeRecords", "diagnostic/data-access.updateMasteryFromExamScore", "grades/lib/notify.notifyGradeEntered", "shared/lib/action-utils.safeJsonParse", "revalidatePath" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "updateGradeRecordAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "usedBy": [ "grades/components/grade-record-list" ] }, { "name": "deleteGradeRecordAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "usedBy": [ "grades/components/grade-record-list" ] }, { "name": "bulkDeleteGradeRecordsAction", "signature": "(ids: string[]) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "usedBy": [ "grades/components/grade-record-list" ] }, { "name": "getGradeRecordsAction", "signature": "(params) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": [ "teacher/grades/page" ] }, { "name": "getClassGradeStatsAction", "signature": "(classId, subjectId?, examId?) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": [ "teacher/grades/stats/page" ] }, { "name": "getStudentGradeSummaryAction", "signature": "(studentId?) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": [ "student/grades/page", "parent/grades/page" ] }, { "name": "getClassRankingAction", "signature": "(classId, subjectId?, examId?) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": [ "teacher/grades/stats/page" ] }, { "name": "getGradeRecordByIdAction", "signature": "(id) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": [ "grades/components/grade-record-list" ] }, { "name": "exportGradesAction", "signature": "(params: { classId?: string; studentId?: string; subjectId?: string; examId?: string; reportType?: \"detail\" | \"class\" }) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "purpose": "导出成绩到 Excel(detail=成绩明细+统计汇总,class=班级多科目横向对比总表),返回 base64 buffer。v4-P1-12 增强:新增可选 studentId 参数支持按学生导出(家长视角)——当提供 studentId 且 scope 为 children 时校验该学生属于家长子女,调用 exportStudentGradeRecordsToExcel 导出单学生成绩单", "deps": [ "requirePermission", "export.exportGradeRecordsToExcel", "export.exportClassGradeReportToExcel", "export.exportStudentGradeRecordsToExcel", "export.formatDateForFile", "lib/scope-check.assertClassInScope" ], "usedBy": [ "grades/components/export-button.tsx", "parent/components/parent-export-button.tsx" ] }, { "name": "getGradeTrendAction", "signature": "(params) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取成绩趋势(按学生/科目/学期,返回归一化分数趋势点)", "usedBy": [ "teacher/grades/analytics" ] }, { "name": "getClassComparisonAction", "signature": "(params) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取班级对比(同年级各班的均分/及格率/优秀率)", "usedBy": [ "teacher/grades/analytics" ] }, { "name": "getSubjectComparisonAction", "signature": "(params) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取科目对比(同班级各科目雷达图数据)", "usedBy": [ "teacher/grades/analytics" ] }, { "name": "getGradeDistributionAction", "signature": "(params) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取分数分布(90-100/80-89/70-79/60-69/<60 各区间人数)", "usedBy": [ "teacher/grades/analytics" ] }, { "name": "getGradeDistributionByGradeIdAction", "signature": "(params: { gradeId; subjectId?; examId?; semester? }) => Promise>", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "v4-P2-7 新增:年级仪表盘维度1,获取年级整体 + 按班级拆分的成绩分布", "usedBy": [ "management/grade/dashboard" ] }, { "name": "getRankingTrendAction", "signature": "(studentId, subjectId?, semester?) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取排名趋势(学生历次考试排名变化,含 DataScope 二次校验)", "usedBy": [ "待扩展" ] }, { "name": "saveGradeDraftAction", "signature": "(params: { classId: string; subjectId: string; type: string; data: GradeDraftData }) => Promise>", "file": "actions-draft.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "v3-P2 新增:保存成绩录入草稿到服务端(调用 data-access-drafts.saveGradeDraft upsert)。P1-1 重构:从 actions.ts 迁移至 actions-draft.ts 独立文件,使 actions.ts 回到 800 行以内。错误消息使用 i18n 翻译键 t(\"action.*\")", "deps": [ "requirePermission", "data-access-drafts.saveGradeDraft" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "getGradeDraftAction", "signature": "(params: { classId: string; subjectId: string; type: string }) => Promise>", "file": "actions-draft.ts", "permission": "GRADE_RECORD_READ", "purpose": "v3-P2 新增:获取服务端成绩录入草稿(24 小时过期)。P1-1 重构:从 actions.ts 迁移至 actions-draft.ts", "deps": [ "requirePermission", "data-access-drafts.getGradeDraft" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "deleteGradeDraftAction", "signature": "(params: { classId: string; subjectId: string; type: string }) => Promise>", "file": "actions-draft.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "v3-P2 新增:删除服务端成绩录入草稿。P1-1 重构:从 actions.ts 迁移至 actions-draft.ts", "deps": [ "requirePermission", "data-access-drafts.deleteGradeDraft" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "acquireDraftLockAction", "signature": "(params: { classId: string; subjectId: string; type: string }) => Promise>", "file": "actions-lock.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "P3-6 新增:获取协同录入锁。调用 acquireDraftLock,锁被他人持有时返回结构化冲突信息供前端展示。enrichLockStatusName 通过 getUserNamesByIds 解析持有者姓名", "deps": [ "requirePermission", "lib/scope-check.assertClassInScope", "data-access-drafts.acquireDraftLock", "users/data-access.getUserNamesByIds" ], "usedBy": [ "grades/hooks/use-draft-lock" ] }, { "name": "renewDraftLockAction", "signature": "(params: { classId: string; subjectId: string; type: string; lockToken: string }) => Promise>", "file": "actions-lock.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "P3-6 新增:续期锁(心跳)。校验 lockToken+userId 匹配,返回新 token。失败表示锁被抢占或过期", "deps": [ "requirePermission", "lib/scope-check.assertClassInScope", "data-access-drafts.renewDraftLock", "users/data-access.getUserNamesByIds" ], "usedBy": [ "grades/hooks/use-draft-lock" ] }, { "name": "releaseDraftLockAction", "signature": "(params: { classId: string; subjectId: string; type: string; lockToken: string }) => Promise>", "file": "actions-lock.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "P3-6 新增:释放锁。失败不阻断主流程(降级为日志),锁会自动过期", "deps": [ "requirePermission", "lib/scope-check.assertClassInScope", "data-access-drafts.releaseDraftLock" ], "usedBy": [ "grades/hooks/use-draft-lock" ] }, { "name": "createGradeAppealAction", "signature": "(prevState, formData) => Promise>", "file": "actions-appeal.ts", "permission": "GRADE_RECORD_READ", "purpose": "P3-7 新增:学生提交成绩申诉。校验成绩归属(studentId===ctx.userId)+ 无 pending 申诉,originalScore 从成绩记录快照", "deps": [ "requirePermission", "schema.CreateGradeAppealSchema", "data-access.getGradeRecordById", "data-access-appeals.hasPendingAppeal", "data-access-appeals.createGradeAppeal" ], "usedBy": [ "待扩展" ] }, { "name": "reviewGradeAppealAction", "signature": "(prevState, formData) => Promise>", "file": "actions-appeal.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "P3-7 新增:教师复核申诉(approve/reject)。approve 且提供 adjustedScore 时同步更新 grade_records.score。scope 校验:教师只能复核自己班级的申诉", "deps": [ "requirePermission", "schema.ReviewGradeAppealSchema", "data-access-appeals.getAppealById", "data-access-appeals.reviewGradeAppeal" ], "usedBy": [ "待扩展" ] }, { "name": "importGradesFromExcelAction", "signature": "(prevState, formData) => Promise>", "file": "actions-import.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "P3-10 新增:Excel 批量导入成绩。流程:文件校验→Zod 校验→scope 校验→parseGradeExcelBuffer→parseGradeImportData 匹配学生→batchCreateGradeRecords→通知→revalidatePath", "deps": [ "requirePermission", "lib/scope-check.assertClassInScope", "schema.ExcelImportGradesSchema", "import-export.parseGradeExcelBuffer", "import-export.parseGradeImportData", "data-access.batchCreateGradeRecords", "lib/notify.notifyGradeEntered" ], "usedBy": [ "grades/components/excel-import-dialog" ] }, { "name": "batchCreateGradeRecordsByExamAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "purpose": "2026-06-24 新增:按试卷批量录入成绩 Server Action。流程:requirePermission → getExamForGradeEntry(scope 校验+获取题目列表)→ assertClassInScope → safeJsonParse 解析 records → BatchGradeEntryByExamSchema 校验 → batchCreateGradeRecordsByExam(单事务写入)→ updateMasteryFromExamScore(失败不阻断)→ notifyGradeEntered(失败不阻断)→ revalidatePath。返回创建的 gradeRecordId 列表供前端撤销", "deps": [ "requirePermission", "exams/data-access.getExamForGradeEntry", "lib/scope-check.assertClassInScope", "shared/lib/action-utils.safeJsonParse", "schema.BatchGradeEntryByExamSchema", "data-access.batchCreateGradeRecordsByExam", "diagnostic/data-access.updateMasteryFromExamScore", "grades/lib/notify.notifyGradeEntered", "revalidatePath" ], "usedBy": [ "grades/components/batch-grade-entry" ] } ], "schemas": [ { "name": "CreateGradeRecordSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "createGradeRecordAction" ] }, { "name": "BatchCreateGradeRecordSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "batchCreateGradeRecordsAction" ] }, { "name": "UpdateGradeRecordSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "updateGradeRecordAction" ] }, { "name": "DeleteGradeRecordSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "deleteGradeRecordAction" ] }, { "name": "GetGradeRecordByIdSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getGradeRecordByIdAction" ] }, { "name": "GradeQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getGradeRecordsAction" ] }, { "name": "ClassGradeStatsQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getClassGradeStatsAction" ] }, { "name": "StudentGradeSummaryQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getStudentGradeSummaryAction" ] }, { "name": "ClassRankingQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getClassRankingAction" ] }, { "name": "ExportGradesSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "exportGradesAction" ] }, { "name": "GradeTrendQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getGradeTrendAction" ] }, { "name": "ClassComparisonQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getClassComparisonAction" ] }, { "name": "SubjectComparisonQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getSubjectComparisonAction" ] }, { "name": "GradeDistributionQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getGradeDistributionAction" ] }, { "name": "RankingTrendQuerySchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": [ "getRankingTrendAction" ] }, { "name": "BatchGradeEntryByExamSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "2026-06-24 新增:按试卷批量录入成绩校验。examId/classId/subjectId 必填,title 1-255 字符,fullScore 1-1000,records 1-500 条,每条含 studentId 必填 + answers 数组(questionId 必填 + score 0-满分)。导出类型 BatchGradeEntryByExamInput = z.infer", "usedBy": [ "batchCreateGradeRecordsByExamAction" ] } ], "types": [ { "name": "GradeRecord", "type": "interface", "file": "types.ts", "usedBy": [ "data-access", "actions" ] }, { "name": "GradeRecordListItem", "type": "interface", "file": "types.ts", "usedBy": [ "data-access", "actions", "components/grade-record-list" ] }, { "name": "PaginatedGradeRecords", "type": "interface", "file": "data-access.ts", "definition": "{ records: GradeRecordListItem[]; total: number }", "usedBy": [ "data-access.getGradeRecords", "actions.getGradeRecordsAction", "teacher/grades/page", "export.exportGradeRecordsToExcel" ] }, { "name": "GradeStats", "type": "interface", "file": "types.ts", "definition": "{ count, average, median, stdDev, passRate, excellentRate, maxScore, minScore }", "usedBy": [ "data-access", "components/class-grade-report" ] }, { "name": "ClassGradeStats", "type": "interface", "file": "types.ts", "usedBy": [ "data-access", "actions", "components/class-grade-report" ] }, { "name": "StudentGradeSummary", "type": "interface", "file": "types.ts", "usedBy": [ "data-access", "actions", "components/student-grade-summary" ] }, { "name": "ClassRankingItem", "type": "interface", "file": "types.ts", "usedBy": [ "data-access", "actions", "components/class-grade-report" ] }, { "name": "GradeRecordType", "type": "type", "file": "types.ts", "definition": "\"exam\" | \"quiz\" | \"assignment\" | \"monthly\" | \"midterm\" | \"final\"", "usedBy": [ "types.GradeRecord.type" ] }, { "name": "GradeRecordSemester", "type": "type", "file": "types.ts", "definition": "\"1\" | \"2\"", "usedBy": [ "types.GradeRecord.semester" ] }, { "name": "GradeQueryParams", "type": "interface", "file": "types.ts", "usedBy": [ "data-access.getGradeRecords" ] }, { "name": "GradeTrendPoint", "type": "interface", "file": "types.ts", "definition": "{ date, title, score, fullScore, normalizedScore, type }", "usedBy": [ "data-access-analytics.getGradeTrend", "grade-trend-chart" ] }, { "name": "GradeTrendResult", "type": "interface", "file": "types.ts", "definition": "{ label, points: GradeTrendPoint[], averageScore }", "usedBy": [ "data-access-analytics.getGradeTrend", "grade-trend-chart" ] }, { "name": "ClassComparisonItem", "type": "interface", "file": "types.ts", "definition": "{ classId, className, averageScore, passRate, excellentRate, studentCount }", "usedBy": [ "data-access-analytics.getClassComparison", "class-comparison-chart" ] }, { "name": "SubjectComparisonItem", "type": "interface", "file": "types.ts", "definition": "{ subjectId, subjectName, averageScore, passRate, excellentRate }", "usedBy": [ "data-access-analytics.getSubjectComparison", "subject-comparison-chart" ] }, { "name": "GradeDistributionBucket", "type": "interface", "file": "types.ts", "definition": "{ label, min, max, count, percentage }", "usedBy": [ "data-access-analytics.getGradeDistribution", "grade-distribution-chart" ] }, { "name": "GradeDistributionResult", "type": "interface", "file": "types.ts", "definition": "{ buckets: GradeDistributionBucket[], totalCount }", "usedBy": [ "data-access-analytics.getGradeDistribution", "grade-distribution-chart" ] }, { "name": "RankingTrendPoint", "type": "interface", "file": "types.ts", "definition": "{ title, date, rank, totalStudents, score }", "usedBy": [ "data-access-ranking.getRankingTrend" ] }, { "name": "RankingTrendResult", "type": "interface", "file": "types.ts", "definition": "{ studentName, points: RankingTrendPoint[] }", "usedBy": [ "data-access-ranking.getRankingTrend" ] }, { "name": "SchoolWideGradeSummaryItem", "type": "interface", "file": "types.ts", "definition": "{ gradeId, gradeName, schoolName, classCount, studentCount, averageScore, passRate, excellentRate, recordCount }", "purpose": "v3-P2 新增:全校汇总按年级聚合项", "usedBy": [ "data-access-analytics.getSchoolWideGradeSummary", "components/school-wide-summary-card" ] }, { "name": "SchoolWideGradeSummary", "type": "interface", "file": "types.ts", "definition": "{ grades: SchoolWideGradeSummaryItem[]; totals: SchoolWideGradeSummaryItem }", "purpose": "v3-P2 新增:全校汇总(grades 数组 + totals 汇总对象,加权平均计算全校汇总)", "usedBy": [ "data-access-analytics.getSchoolWideGradeSummary", "components/school-wide-summary-card", "admin/school/grades/insights/page.tsx" ] }, { "name": "GradeDraftData", "type": "interface", "file": "data-access-drafts.ts", "definition": "{ scores: Record; timestamp: number }", "purpose": "v3-P2 新增:成绩录入草稿数据结构(scores 为学生 ID 到分数字符串的映射,timestamp 为草稿创建时间戳)。P1-2 重构:从 data-access.ts 迁移至 data-access-drafts.ts", "usedBy": [ "data-access-drafts.saveGradeDraft", "data-access-drafts.getGradeDraft", "actions-draft.saveGradeDraftAction", "actions-draft.getGradeDraftAction", "components/batch-grade-entry" ] }, { "name": "BatchGradeRecordResult", "type": "interface", "file": "data-access-exam-entry.ts", "definition": "{ gradeRecordId: string; studentId: string; submissionId: string }", "purpose": "P1-2 新增:按试卷批量录入成绩返回结果项。返回 gradeRecordId 供前端撤销,submissionId 供下游错题采集模块触发 collectFromExamSubmission", "usedBy": [ "data-access-exam-entry.batchCreateGradeRecordsByExam" ] }, { "name": "GradeRecordAnswer", "type": "interface", "file": "types.ts", "definition": "{ id, gradeRecordId, questionId, score: number, fullScore: number, feedback?: string, createdAt, updatedAt }", "purpose": "2026-06-24 新增:成绩记录-题目得分明细(对应 grade_record_answers 表)", "usedBy": [ "data-access.batchCreateGradeRecordsByExam" ] }, { "name": "BatchGradeEntryByExamQuestion", "type": "interface", "file": "types.ts", "definition": "{ questionId: string; score: number }", "purpose": "2026-06-24 新增:按试卷录入时单个题目的得分", "usedBy": [ "types.BatchGradeEntryByExamItem", "schema.BatchGradeEntryByExamSchema" ] }, { "name": "BatchGradeEntryByExamItem", "type": "interface", "file": "types.ts", "definition": "{ studentId: string; answers: BatchGradeEntryByExamQuestion[] }", "purpose": "2026-06-24 新增:按试卷录入时单个学生的所有题目得分", "usedBy": [ "schema.BatchGradeEntryByExamSchema" ] } ], "importExport": [ { "name": "exportGradeRecordsToExcel", "signature": "(params: { classId: string; subjectId?: string; examId?: string; scope: DataScope; currentUserId?: string }) => Promise", "file": "export.ts", "purpose": "导出成绩单(Sheet1 成绩明细,Sheet2 统计汇总:均分/中位数/最高分/最低分/标准差/及格率/优秀率/参考人数)(P3 更新:传递 scope/currentUserId 到 data-access;P3-7:硬编码中文改用 next-intl getTranslations)", "deps": [ "shared.lib.excel.exportToExcel", "data-access.getGradeRecords", "data-access.getClassGradeStats", "next-intl/server.getTranslations" ], "usedBy": [ "actions.exportGradesAction", "app/api/export/route.ts" ] }, { "name": "exportClassGradeReportToExcel", "signature": "(params: { classId: string; scope: DataScope; currentUserId?: string }) => Promise", "file": "export.ts", "purpose": "导出班级成绩总表(多科目横向对比,含总分/平均分/排名列)(P3 更新:适配 PaginatedGradeRecords 结构 + 传递 scope/currentUserId;P3-6:复用 stats-service.computeAverageScore 替代局部 avg;P3-7:硬编码中文改用 next-intl getTranslations)", "deps": [ "shared.db", "shared.db.schema.classes", "shared.db.schema.subjects", "shared.db.schema.gradeRecords", "shared.db.schema.users", "shared.lib.excel.exportToExcel", "data-access.getGradeRecords", "stats-service.computeAverageScore", "next-intl/server.getTranslations" ], "usedBy": [ "actions.exportGradesAction" ] }, { "name": "formatDateForFile", "signature": "(d?: Date) => string", "file": "export.ts", "purpose": "⚠️ P1-c/P2-c 已迁移:本地实现已删除,改为从 @/shared/lib/utils 导入。此条目保留仅作历史记录", "deps": [ "shared/lib/utils.formatDateForFile" ], "usedBy": [], "migratedTo": "shared/lib/utils.formatDateForFile" } ], "lib": [ { "name": "toNumber", "signature": "(v: unknown) => number", "file": "lib/grade-utils.ts", "purpose": "安全将 unknown 值转换为有限数字,非有限值返回 0(P1-2 新增:从 data-access/data-access-analytics/data-access-ranking 抽取)", "usedBy": [ "data-access.serializeRecord", "data-access.getClassGradeStats", "data-access-analytics.getGradeTrend", "data-access-analytics.getClassComparison", "data-access-analytics.getSubjectComparison", "data-access-analytics.getGradeDistribution", "data-access-ranking.getRankingTrend" ] }, { "name": "normalize", "signature": "(score: number, fullScore: number) => number", "file": "lib/grade-utils.ts", "purpose": "将原始分数归一化到 0-100 分制(P1-2 新增:从 data-access-analytics/data-access-ranking 抽取)", "usedBy": [ "data-access-analytics.getGradeTrend", "data-access-analytics.getSubjectComparison", "data-access-analytics.getGradeDistribution", "data-access-ranking.getRankingTrend" ] }, { "name": "buildScopeClassFilter", "signature": "(scope: DataScope, currentUserId?: string) => SQL | null", "file": "lib/scope-filter.ts", "purpose": "根据 DataScope 构建 gradeRecords 表的行级权限过滤条件(P1-2 新增:从 data-access/data-access-analytics 抽取;P3 更新:class_members scope 内置 studentId 过滤,需传入 currentUserId 参数;P3-26:从 grade-utils.ts 迁移至 scope-filter.ts)", "usedBy": [ "data-access.getGradeRecords", "data-access.getClassGradeStats", "data-access.getStudentGradeSummary", "data-access.getClassRanking", "data-access-analytics.getGradeTrend", "data-access-analytics.getClassComparison", "data-access-analytics.getSubjectComparison", "data-access-analytics.getGradeDistribution", "data-access-ranking.getRankingTrend" ] }, { "name": "assertClassInScope", "signature": "(scope: DataScope, classId: string) => ScopeCheckErrorCode | null", "file": "lib/scope-check.ts", "purpose": "校验 classId 是否在 scope 允许范围内,返回错误码表示拒绝(调用方通过 i18n 键 grades.action.scopeError.{code} 翻译为用户可见消息),返回 null 表示允许。all/children/grade_managed 允许;class_taught/class_members 校验 classIds 包含;owned 拒绝。P3 新增原位于 actions.ts,v4-P2-6 迁移至独立文件 lib/scope-check.ts(含 import \\\"server-only\\\"),原因:actions.ts 是 \\\"use server\\\" 文件要求所有 export 必须为 async,而本函数是同步函数。P1-4 重构:返回类型从 string | null 改为 ScopeCheckErrorCode | null(类型为 \\\"class_taught\\\" | \\\"class_members\\\" | \\\"denied\\\"),消除硬编码英文字符串,使 Action 层可通过 next-intl 翻译", "usedBy": [ "grades/actions.getGradeRecordsAction", "grades/actions.getGradeRecordByIdAction", "grades/actions.exportGradesAction", "grades/actions-analytics.getGradeTrendAction", "grades/actions-analytics.getSubjectComparisonAction", "grades/actions-analytics.getGradeDistributionAction" ] }, { "name": "notifyGradeEntered", "signature": "(params: { studentIds: string[]; title: string; subjectId?: string; score?: number; fullScore?: number }) => Promise", "file": "lib/notify.ts", "purpose": "v4-P1-6 新增:成绩录入后通知学生和家长。通知失败不阻断成绩录入主流程。使用 next-intl getTranslations 获取通知标题和内容(i18n),调用 notifications/data-access.createNotification 通知学生本人,调用 parent/data-access.getParentIdsByStudentIds 查询家长 ID 后通知家长。P2-7 改进:使用 Promise.allSettled 并行化通知写入,避免批量录入 30+ 学生时串行等待。P1-1 重构:从 actions.ts 抽取到 lib/notify.ts,使 actions.ts 回到 800 行以内", "deps": [ "notifications/data-access.createNotification", "school/data-access.getSubjectNameById", "parent/data-access.getParentIdsByStudentIds", "next-intl/server.getTranslations" ], "usedBy": [ "grades/actions.createGradeRecordAction", "grades/actions.batchCreateGradeRecordsAction", "grades/actions.batchCreateGradeRecordsByExamAction" ] }, { "name": "isGradeType", "signature": "(v: unknown) => v is GradeRecordType", "file": "lib/type-guards.ts", "purpose": "类型守卫:验证 unknown 值是否为合法的 GradeRecordType。P2-9 修复:使用 GRADE_TYPES.some((t) => t === v) 替代 (GRADE_TYPES as readonly string[]).includes(v),消除 as 断言。Phase 4.6:原调用方 grade-filters/grade-query-filters 已删除,改由 app 层 student-grade-filters / teacher-grade-query-filters 复用", "usedBy": [ "app/(dashboard)/student/grades/student-grade-filters", "app/(dashboard)/teacher/grades/teacher-grade-query-filters" ] }, { "name": "isSemester", "signature": "(v: unknown) => v is GradeRecordSemester", "file": "lib/type-guards.ts", "purpose": "类型守卫:验证 unknown 值是否为合法的 GradeRecordSemester。P2-9 修复:使用 SEMESTERS.some((s) => s === v) 替代 (SEMESTERS as readonly string[]).includes(v),消除 as 断言。Phase 4.6:原调用方 grade-filters/grade-query-filters 已删除,改由 app 层 student-grade-filters / teacher-grade-query-filters 复用", "usedBy": [ "app/(dashboard)/student/grades/student-grade-filters", "app/(dashboard)/teacher/grades/teacher-grade-query-filters" ] }, { "name": "isGradeDraftData", "signature": "(v: unknown) => v is GradeDraftData", "file": "lib/type-guards.ts", "purpose": "类型守卫:安全验证 DB JSON 列解析结果是否为 GradeDraftData 结构(scores 为对象 + timestamp 为数字)。P2-9 修复:使用 in 运算符替代 as Record 断言", "usedBy": [ "data-access-drafts.getGradeDraft" ] } ], "statsService": [ { "name": "computeGradeStats", "signature": "(rows: RawScoreRow[]) => GradeStats | null", "file": "stats-service.ts", "purpose": "从原始成绩行计算班级统计(均分、中位数、标准差、及格率、优秀率、最高分、最低分、参考人数)(P1-1 新增:从 data-access.getClassGradeStats 抽取为纯函数)", "usedBy": [ "data-access.getClassGradeStats" ] }, { "name": "computeAverageScore", "signature": "(scores: number[]) => number", "file": "stats-service.ts", "purpose": "计算分数平均值(空数组返回 0)(P1-1 新增:从 data-access.getStudentGradeSummary 抽取为纯函数;P3-6:export.ts 复用此函数替代局部 avg)", "usedBy": [ "data-access.getStudentGradeSummary", "export.exportClassGradeReportToExcel" ] }, { "name": "buildGradeTrendPoints", "signature": "(rows: RawScoreRow[]) => GradeTrendPoint[]", "file": "stats-service.ts", "purpose": "构建成绩趋势数据点(按考试标题分组,归一化分数 0-100)(P1-1 新增:从 data-access-analytics.getGradeTrend 抽取为纯函数;P3-24:使用 isGradeTrendType 类型守卫替代 as 断言)", "usedBy": [ "data-access-analytics.getGradeTrend" ] }, { "name": "computeTrendAverage", "signature": "(points: GradeTrendPoint[]) => number", "file": "stats-service.ts", "purpose": "计算趋势数据点的平均分(P1-1 新增:从 data-access-analytics.getGradeTrend 抽取为纯函数)", "usedBy": [ "data-access-analytics.getGradeTrend" ] }, { "name": "computeClassComparisonStats", "signature": "(rows: RawScoreRow[]) => Pick", "file": "stats-service.ts", "purpose": "计算班级对比统计(均分、及格率、优秀率、参考人数)(P1-1 新增:从 data-access-analytics.getClassComparison 抽取为纯函数)", "usedBy": [ "data-access-analytics.getClassComparison" ] }, { "name": "computeSubjectComparisonStats", "signature": "(scores: number[]) => Pick", "file": "stats-service.ts", "purpose": "计算科目对比统计(均分、及格率、优秀率、最高分、最低分)(P1-1 新增:从 data-access-analytics.getSubjectComparison 抽取为纯函数)", "usedBy": [ "data-access-analytics.getSubjectComparison" ] }, { "name": "computeGradeDistribution", "signature": "(rows: RawScoreRow[]) => GradeDistributionResult", "file": "stats-service.ts", "purpose": "计算分数分布(90-100/80-89/70-79/60-69/<60 五个区间)(P1-1 新增:从 data-access-analytics.getGradeDistribution 抽取为纯函数)", "usedBy": [ "data-access-analytics.getGradeDistribution" ] }, { "name": "buildRankingTrendPoints", "signature": "(byTitle: Map, targetStudentId: string) => RankingTrendPoint[]", "file": "stats-service.ts", "purpose": "构建排名趋势数据点(按考试标题排序、计算每次考试学生排名)(P1-1 新增:从 data-access-ranking.getRankingTrend 抽取为纯函数)", "usedBy": [ "data-access-ranking.getRankingTrend" ] } ], "components": [ { "name": "GradeRecordForm", "file": "components/grade-record-form.tsx", "purpose": "单条成绩录入表单(选择学生、班级、科目、考试,输入分数、满分、类型、学期、备注)" }, { "name": "BatchGradeEntry", "file": "components/batch-grade-entry.tsx", "purpose": "2026-06-24 重新设计:导出名改为 BatchGradeEntryByExam,按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴(Tab 分隔);Enter 跳下一行同一列;Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低);分数校验(超过题目满分标红);撤销机制(sessionStorage 5 分钟有效)。P1-6/P1-7 重构:拆分为 3 个子组件 + 1 个自定义 Hook(BatchGradeEntryTable + BatchGradeEntryStats + BatchGradeEntryDialog + useBatchGradeEntryUndo),主组件从 604 行降至 422 行。撤销数据使用 isUndoData 类型守卫替代 as 断言。Phase 4.6 重构:进一步拆分试卷/班级选择器(含未选态引导)至 BatchGradeEntrySelectors、底部工具栏(清除/保存/粘贴提示)至 BatchGradeEntryToolbar,主组件降至 374 行,容器仅负责状态编排与 Server Action 调用。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?", "props": "{ exams: ExamOptionForEntry[]; classes: ClassOption[]; classGradeMap: Record; exam: ExamForGradeEntry | null; students: Student[]; defaultExamId?: string; defaultClassId?: string }", "deps": [ "exams/types.ExamOptionForEntry", "exams/types.ExamForGradeEntry", "grades/actions.batchCreateGradeRecordsByExamAction", "grades/actions.undoBatchCreateGradeRecordsAction", "grades/components/batch-grade-entry-table.BatchGradeEntryTable", "grades/components/batch-grade-entry-stats.BatchGradeEntryStats", "grades/components/batch-grade-entry-dialog.BatchGradeEntryDialog", "grades/components/batch-grade-entry-selectors.BatchGradeEntrySelectors", "grades/components/batch-grade-entry-toolbar.BatchGradeEntryToolbar", "grades/hooks/use-batch-grade-entry-undo.useBatchGradeEntryUndo", "shared/lib/action-utils.safeActionCall", "shared/components/ui/card", "shared/components/ui/table", "shared/components/ui/select" ], "usedBy": [ "teacher/grades/entry/page.tsx" ] }, { "name": "BatchGradeEntryTable", "file": "components/batch-grade-entry-table.tsx", "purpose": "P1-6 新增:批量录入 Excel 式表格子组件(行=学生,列=题目,末列=总分自动计算)。支持多行多列 Excel 粘贴、Enter 跳下一行、Tab 跳下一格、分数校验(超过题目满分标红)。接受 inputRefs 供父组件管理输入焦点", "props": "{ students, exam, scores, inputRefs, onScoreChange }", "deps": [ "shared/components/ui/table" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "BatchGradeEntryStats", "file": "components/batch-grade-entry-stats.tsx", "purpose": "P1-6 新增:批量录入统计栏子组件,显示已录入/总数/均分/最高/最低/无效分数徽章,含学生搜索框过滤", "props": "{ data: BatchGradeEntryStatsData; searchValue, onSearchChange }", "deps": [ "shared/components/ui/badge" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "BatchGradeEntryDialog", "file": "components/batch-grade-entry-dialog.tsx", "purpose": "P1-6 新增:批量录入切换确认对话框子组件(替代 window.confirm())。PendingSwitch 类型为联合类型 { kind: 'exam'; examId; message } | { kind: 'class'; classId; message } | null", "props": "{ pendingSwitch, onConfirm, onCancel }", "deps": [ "shared/components/ui/alert-dialog" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "BatchGradeEntrySelectors", "file": "components/batch-grade-entry-selectors.tsx", "purpose": "Phase 4.6 新增:批量录入试卷/班级选择器子组件。未选试卷时显示单选试卷 Card + Info 引导;已选试卷时显示双列选择器(试卷 Select + 班级 Select)+ 试卷信息 Badge(题目数/总分/满分)", "props": "{ exams, classes, exam, defaultExamId?, defaultClassId?, onExamChange, onClassChange }", "deps": [ "shared/components/ui/card", "shared/components/ui/select", "shared/components/ui/badge" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "BatchGradeEntryToolbar", "file": "components/batch-grade-entry-toolbar.tsx", "purpose": "Phase 4.6 新增:批量录入底部工具栏子组件(清除按钮 + 保存按钮 + Excel 粘贴提示)。保存按钮在 hasInvalidScores 时禁用", "props": "{ isSubmitting, hasInvalidScores, onClear, onSubmit }", "deps": [ "shared/components/ui/button" ], "usedBy": [ "grades/components/batch-grade-entry" ] }, { "name": "GradeRecordList", "file": "components/grade-record-list.tsx", "purpose": "成绩记录列表容器。Phase 4.6 重构:行渲染(桌面表格 + 移动端卡片堆叠)拆分至 GradeRecordRow,编辑/单条删除/批量删除对话框拆分至 GradeRecordDetail,主组件从 523 行降至 231 行,容器仅负责状态编排(useOptimistic + useTransition)与 Server Action 调用。Phase 4.5:编辑保存改用 useOptimistic + useTransition,在列表中即时显示乐观记录。v3-P3-2:批量选择与删除", "props": "{ records: GradeRecordListItem[] }", "deps": [ "grades/actions.deleteGradeRecordAction", "grades/actions.updateGradeRecordAction", "grades/actions.bulkDeleteGradeRecordsAction", "grades/components/grade-record-row.GradeRecordRow", "grades/components/grade-record-detail.GradeRecordDetail", "grades/components/grade-record-detail.toEditableFields", "shared/lib/action-utils.safeActionCall", "shared/components/ui/button" ], "usedBy": [ "teacher/grades/page.tsx" ] }, { "name": "GradeRecordRow", "file": "components/grade-record-row.tsx", "purpose": "Phase 4.6 新增:成绩记录行渲染子组件。桌面端 Table 行(含复选框 + 学生/科目/考试/分数/满分/类型/学期/备注/操作)+ 移动端卡片堆叠视图(role=list/listitem)。复选框支持单行切换与全选切换。操作按钮触发编辑与单条删除回调", "props": "{ records, selectedIds, onToggleRow, onToggleSelectAll, onEdit, onDelete }", "deps": [ "shared/components/ui/table", "shared/components/ui/checkbox", "shared/components/ui/button", "shared/components/ui/status-badge" ], "usedBy": [ "grades/components/grade-record-list" ] }, { "name": "GradeRecordDetail", "file": "components/grade-record-detail.tsx", "purpose": "Phase 4.6 新增:成绩记录详情对话框集合子组件,包含编辑对话框(title/score/fullScore/type/semester/remark 表单)、单条删除确认对话框、批量删除确认对话框。导出 EditableFields 类型 + toEditableFields(r: GradeRecordListItem): EditableFields 工具函数,用于将列表项转换为表单可编辑字段", "props": "{ editTarget, deleteId, isBulkDeleteDialogOpen, isSaving, isDeleting, isBulkDeleting, bulkDeleteCount, onEditTargetChange, onCancelEdit, onSaveEdit, onCancelDelete, onConfirmDelete, onCloseBulkDelete, onConfirmBulkDelete }", "deps": [ "shared/components/ui/dialog", "shared/components/ui/button", "shared/components/ui/label", "shared/components/ui/input", "shared/components/ui/select", "shared/components/ui/alert-dialog" ], "usedBy": [ "grades/components/grade-record-list" ] }, { "name": "StudentGradeSummary", "file": "components/student-grade-summary.tsx", "purpose": "学生成绩汇总视图(按科目分组展示成绩趋势)" }, { "name": "ClassGradeReport", "file": "components/class-grade-report.tsx", "purpose": "班级成绩报表(含统计+排名)。Phase 4.6 重构:删除 GradeStatsCard 依赖,改用 StatsGrid + StatItem 组合内联渲染 8 个统计项(average/median/max/min/stdDev/passRate/excellentRate/count),count=0 时显示空态 Card" }, { "name": "ExportButton", "file": "components/export-button.tsx", "purpose": "成绩导出按钮(DropdownMenu 选择 detail/class 报表类型,调用 exportGradesAction 并触发浏览器下载)", "usedBy": [ "teacher/grades/page.tsx", "teacher/grades/stats/page.tsx" ] }, { "name": "GradeTrendChart", "file": "components/grade-trend-chart.tsx", "purpose": "成绩趋势折线图(recharts LineChart,归一化分数 0-100)", "deps": [ "recharts", "shared/components/ui/chart" ] }, { "name": "ClassComparisonChart", "file": "components/class-comparison-chart.tsx", "purpose": "班级对比柱状图(recharts BarChart,均分/及格率/优秀率;v3-P3-5 新增:显著性分析区域,基于极差和样本量经验规则判断班级间差异是否显著,含可折叠详细分析展示最高/最低分班级)", "deps": [ "recharts", "shared/components/ui/chart", "shared/components/ui/collapsible", "shared/lib/utils" ] }, { "name": "SubjectComparisonChart", "file": "components/subject-comparison-chart.tsx", "purpose": "科目对比雷达图(recharts RadarChart)", "deps": [ "recharts", "shared/components/ui/chart" ] }, { "name": "GradeDistributionChart", "file": "components/grade-distribution-chart.tsx", "purpose": "分数分布柱状图(recharts BarChart,彩色区间 90-100/80-89/70-79/60-69/<60;v4-P3-4 改进:每个分数段使用不同 SVG pattern + 颜色双重编码,色盲友好)", "deps": [ "recharts", "shared/components/ui/chart" ] }, { "name": "StatsClassSelector", "file": "components/stats-class-selector.tsx", "purpose": "统计页班级+科目筛选器(Link 筛选按钮组,含 focus-visible 焦点样式)", "deps": [ "next/link", "shared/lib/utils.cn" ], "usedBy": [ "teacher/grades/stats/page.tsx" ] }, { "name": "WidgetBoundary", "file": "components/widget-boundary.tsx", "purpose": "通用 Widget 边界组件(Error Boundary + Suspense + Skeleton 组合,含 a11y 属性 role=alert/aria-live/aria-label)(P1-5 新增)", "deps": [ "shared/components/ui/skeleton", "shared/components/ui/button" ] }, { "name": "SchoolWideSummaryCard", "file": "components/school-wide-summary-card.tsx", "purpose": "v3-P2 新增:管理员全校成绩汇总卡片(4 个统计卡片:年级数/班级数/学生数/成绩记录数 + 各年级对比表格:年级/班级数/学生数/平均分/及格率/优秀率/记录数)", "props": "{ summary: SchoolWideGradeSummary }", "deps": [ "shared/components/ui/card", "shared/components/ui/stat-item", "shared/components/ui/table" ], "usedBy": [ "admin/school/grades/insights/page.tsx" ] }, { "name": "ScoreCell", "file": "components/score-cell.tsx", "purpose": "v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%(不及格 text-red-600)/黄60-84%(及格未优秀 text-yellow-600)/绿≥85%(优秀 text-green-600),使用语义化 Tailwind 类名避免动态拼接,fullScore<=0 时不着色(避免除零)", "props": "{ score: number; fullScore: number; className?: string }", "deps": [ "shared/lib/utils.cn" ], "usedBy": [ "grades/components/grade-record-list.tsx" ] } ], "hooks": [ { "name": "useBatchGradeEntryUndo", "signature": "() => { saveUndoToken: (ids: string[]) => void; handleUndo: () => Promise }", "file": "hooks/use-batch-grade-entry-undo.ts", "purpose": "P1-6/P1-7 新增:批量录入撤销 Hook。封装撤销令牌的序列化、sessionStorage 读写、5 分钟 TTL 过期检查、撤销 Action 调用。包含 isUndoData 类型守卫函数(替代 as { ids: string[]; timestamp: number } 断言),使用 in 运算符做安全属性访问。UNDO_TTL_MS = 5 * 60 * 1000", "deps": [ "grades/actions.undoBatchCreateGradeRecordsAction", "shared/lib/action-utils.safeActionCall" ], "usedBy": [ "grades/components/batch-grade-entry" ] } ] } }, "course-plans": { "path": "src/modules/course-plans", "description": "课程计划管理:创建、编辑、删除课程计划(含周计划条目),管理员可管理全部,教师/学生/年级主任/教务主任可查看", "exports": { "actions": [ { "name": "createCoursePlanAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建课程计划", "deps": [ "requirePermission", "shared/db", "data-access.createCoursePlan" ], "usedBy": [ "course-plan-form.tsx" ] }, { "name": "updateCoursePlanAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新课程计划", "deps": [ "requirePermission", "shared/db", "data-access.updateCoursePlan" ], "usedBy": [ "course-plan-form.tsx" ] }, { "name": "deleteCoursePlanAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string) => Promise>", "purpose": "删除课程计划", "deps": [ "requirePermission", "shared/db", "data-access.deleteCoursePlan" ], "usedBy": [ "course-plan-detail.tsx" ] }, { "name": "getCoursePlansAction", "permission": "COURSE_PLAN_READ", "signature": "(params?: GetCoursePlansParams) => Promise>", "purpose": "获取课程计划列表", "deps": [ "requirePermission", "data-access.getCoursePlans" ], "usedBy": [ "待扩展" ] }, { "name": "getCoursePlanAction", "permission": "COURSE_PLAN_READ", "signature": "(id: string) => Promise>", "purpose": "获取课程计划详情(含周计划条目)", "deps": [ "requirePermission", "data-access.getCoursePlanById" ], "usedBy": [ "待扩展" ] }, { "name": "createCoursePlanItemAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建周计划条目", "deps": [ "requirePermission", "shared/db", "data-access.createCoursePlanItem" ], "usedBy": [ "course-plan-item-editor.tsx" ] }, { "name": "updateCoursePlanItemAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新周计划条目", "deps": [ "requirePermission", "shared/db", "data-access.updateCoursePlanItem" ], "usedBy": [ "course-plan-item-editor.tsx" ] }, { "name": "deleteCoursePlanItemAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string) => Promise>", "purpose": "删除周计划条目", "deps": [ "requirePermission", "shared/db", "data-access.deleteCoursePlanItem" ], "usedBy": [ "course-plan-item-editor.tsx" ] }, { "name": "toggleCoursePlanItemCompletedAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string, completed: boolean) => Promise>", "purpose": "切换周计划条目完成状态", "deps": [ "requirePermission", "shared/db", "data-access.updateCoursePlanItem" ], "usedBy": [ "course-plan-detail.tsx" ] }, { "name": "getGradeCoursePlanProgressAction", "permission": "COURSE_PLAN_READ", "signature": "(gradeId: string) => Promise>", "purpose": "v4-P2-7 新增:年级仪表盘维度4,获取年级下所有班级的教学计划进度", "deps": [ "requirePermission", "schema.GradeIdSchema", "data-access.getGradeCoursePlanProgress" ], "usedBy": [ "management/grade/dashboard" ] }, { "name": "reorderCoursePlanItemsAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(input: { planId: string; items: ReorderCoursePlanItemInput[] }) => Promise>", "purpose": "P1-7 新增:拖拽排序周计划条目", "deps": [ "requirePermission", "schema.ReorderItemsSchema", "data-access.reorderCoursePlanItems" ], "usedBy": [ "course-plan-detail.tsx (待 P2 拖拽 UI)" ] }, { "name": "bulkToggleItemsAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(itemIds: string[], completed: boolean) => Promise>", "purpose": "P2-4 新增:批量标记周计划条目完成状态", "deps": [ "requirePermission", "schema.BulkToggleSchema", "data-access.bulkUpdateItemCompleted" ], "usedBy": [ "course-plan-detail.tsx" ] }, { "name": "copyCoursePlanAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(sourcePlanId: string, targetClassIds: string[]) => Promise>", "purpose": "P2-4 新增:复制课程计划到其他班级", "deps": [ "requirePermission", "schema.CopyPlanSchema", "data-access.copyCoursePlanToClasses" ], "usedBy": [ "待 P2 复制 UI" ] } ], "dataAccess": [ { "name": "getCoursePlans", "signature": "(params?: GetCoursePlansParams, scope?: CoursePlanQueryScope) => Promise", "file": "data-access.ts", "purpose": "P0-3 新增 scope 参数用于权限过滤;P1-1 解耦:不再 JOIN classes/subjects/users 表,通过 enrichPlanRows 动态 import 对方 data-access 批量解析名称", "deps": [ "shared.db", "shared.db.schema.coursePlans", "classes/data-access.getClassNamesByIds (动态 import)", "school/data-access.getSubjectNameMapByIds (动态 import)", "users/data-access.getUserNamesByIds (动态 import)" ], "usedBy": [ "admin/course-plans/page.tsx", "teacher/course-plans/page.tsx", "getCoursePlansAction" ] }, { "name": "getCoursePlanById", "signature": "(id: string, scope?: CoursePlanQueryScope) => Promise", "file": "data-access.ts", "purpose": "P0-1/P0-3 新增 scope 参数用于权限过滤(教师仅能查看自己负责的计划);P1-1 解耦:不再 JOIN classes/subjects/users 表", "deps": [ "shared.db", "shared.db.schema.coursePlans", "shared.db.schema.coursePlanItems", "classes/data-access.getClassNamesByIds (动态 import)", "school/data-access.getSubjectNameMapByIds (动态 import)", "users/data-access.getUserNamesByIds (动态 import)" ], "usedBy": [ "admin/course-plans/[id]/page.tsx", "teacher/course-plans/[id]/page.tsx", "getCoursePlanAction" ] }, { "name": "createCoursePlan", "signature": "(data: CreateCoursePlanInput, createdBy: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.coursePlans", "@paralleldrive/cuid2" ], "usedBy": [ "createCoursePlanAction" ] }, { "name": "updateCoursePlan", "signature": "(id: string, data: Partial) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.coursePlans" ], "usedBy": [ "updateCoursePlanAction" ] }, { "name": "deleteCoursePlan", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.coursePlans" ], "usedBy": [ "deleteCoursePlanAction" ] }, { "name": "createCoursePlanItem", "signature": "(data: CreateCoursePlanItemInput) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.coursePlanItems", "@paralleldrive/cuid2" ], "usedBy": [ "createCoursePlanItemAction" ] }, { "name": "updateCoursePlanItem", "signature": "(id: string, data: Partial) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.coursePlanItems" ], "usedBy": [ "updateCoursePlanItemAction", "toggleCoursePlanItemCompletedAction" ] }, { "name": "deleteCoursePlanItem", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.coursePlanItems" ], "usedBy": [ "deleteCoursePlanItemAction" ] }, { "name": "reorderCoursePlanItems", "signature": "(planId: string, items: ReorderCoursePlanItemInput[]) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.coursePlanItems" ], "usedBy": [ "reorderCoursePlanItemsAction" ] }, { "name": "bulkUpdateItemCompleted", "signature": "(itemIds: string[], completed: boolean) => Promise", "file": "data-access.ts", "purpose": "P2-4 新增:批量标记周计划条目完成状态", "deps": [ "shared.db", "shared.db.schema.coursePlanItems" ], "usedBy": [ "bulkToggleItemsAction" ] }, { "name": "copyCoursePlanToClasses", "signature": "(sourcePlanId: string, targetClassIds: string[]) => Promise", "file": "data-access.ts", "purpose": "P2-4 新增:复制课程计划到其他班级(含基本信息和所有周计划条目)", "deps": [ "shared.db", "shared.db.schema.coursePlans", "shared.db.schema.coursePlanItems", "@paralleldrive/cuid2" ], "usedBy": [ "copyCoursePlanAction" ] }, { "name": "getGradeCoursePlanProgress", "signature": "(params: { gradeId: string }) => Promise", "file": "data-access.ts", "purpose": "v4-P2-7 新增:年级仪表盘维度4。P1-1 解耦后通过 enrichPlanRows 批量解析名称,不再直接 JOIN classes/subjects/users 表", "deps": [ "shared.db", "shared.db.schema.coursePlans", "shared.db.schema.coursePlanItems", "classes/data-access.getClassesByGradeId (动态 import)", "classes/data-access.getClassNamesByIds (动态 import)", "school/data-access.getSubjectNameMapByIds (动态 import)", "users/data-access.getUserNamesByIds (动态 import)" ], "usedBy": [ "course-plans/actions.getGradeCoursePlanProgressAction", "management/grade/dashboard" ] } ], "schemas": [ { "name": "CreateCoursePlanSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, subjectId, teacherId, academicYearId?, semester?, totalHours?, weeklyHours?, startDate?, endDate?, syllabus?, objectives?, status? }", "usedBy": [ "createCoursePlanAction" ] }, { "name": "UpdateCoursePlanSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId?, subjectId?, teacherId?, academicYearId?, semester?, totalHours?, completedHours?, weeklyHours?, startDate?, endDate?, syllabus?, objectives?, status? }", "usedBy": [ "updateCoursePlanAction" ] }, { "name": "CreateCoursePlanItemSchema", "type": "zod", "file": "schema.ts", "definition": "{ planId, week, topic, content?, hours?, textbookChapter?, notes? }", "usedBy": [ "createCoursePlanItemAction" ] }, { "name": "UpdateCoursePlanItemSchema", "type": "zod", "file": "schema.ts", "definition": "{ week?, topic?, content?, hours?, textbookChapter?, notes?, isCompleted?, completedAt? }", "usedBy": [ "updateCoursePlanItemAction" ] }, { "name": "GetCoursePlansParamsSchema", "type": "zod", "file": "schema.ts", "purpose": "P1-6 新增:getCoursePlansAction 入参验证", "definition": "{ classId?, teacherId?, subjectId?, status? }", "usedBy": [ "getCoursePlansAction" ] }, { "name": "GradeIdSchema", "type": "zod", "file": "schema.ts", "purpose": "P1-6 新增:getGradeCoursePlanProgressAction 入参验证", "definition": "{ gradeId }", "usedBy": [ "getGradeCoursePlanProgressAction" ] }, { "name": "ReorderItemsSchema", "type": "zod", "file": "schema.ts", "purpose": "P1-7 新增:reorderCoursePlanItemsAction 入参验证", "definition": "{ planId, items: [{ id, week }] }", "usedBy": [ "reorderCoursePlanItemsAction" ] }, { "name": "BulkToggleSchema", "type": "zod", "file": "schema.ts", "purpose": "P2-4 新增:bulkToggleItemsAction 入参验证", "definition": "{ itemIds: string[], completed: boolean }", "usedBy": [ "bulkToggleItemsAction" ] }, { "name": "CopyPlanSchema", "type": "zod", "file": "schema.ts", "purpose": "P2-4 新增:copyCoursePlanAction 入参验证", "definition": "{ sourcePlanId, targetClassIds: string[] }", "usedBy": [ "copyCoursePlanAction" ] } ], "types": [ { "name": "CoursePlan", "type": "interface", "file": "types.ts", "definition": "{ id, classId, subjectId, teacherId, academicYearId, semester, totalHours, completedHours, weeklyHours, startDate, endDate, syllabus, objectives, status, createdBy, createdAt, updatedAt }", "usedBy": [ "course-plans/components", "data-access" ] }, { "name": "CoursePlanItem", "type": "interface", "file": "types.ts", "definition": "{ id, planId, week, topic, content, hours, textbookChapter, notes, isCompleted, completedAt, createdAt, updatedAt }", "usedBy": [ "course-plans/components", "data-access" ] }, { "name": "CoursePlanListItem", "type": "interface", "file": "types.ts", "definition": "= CoursePlan & { className, subjectName, teacherName }", "usedBy": [ "列表页", "data-access" ] }, { "name": "CoursePlanWithItems", "type": "interface", "file": "types.ts", "definition": "= CoursePlanListItem & { items: CoursePlanItem[] }", "usedBy": [ "详情页", "data-access" ] }, { "name": "CoursePlanStatus", "type": "type", "file": "types.ts", "definition": "\"planning\" | \"active\" | \"completed\" | \"paused\"", "usedBy": [ "data-access", "components" ] }, { "name": "CoursePlanSemester", "type": "type", "file": "types.ts", "definition": "\"1\" | \"2\"", "usedBy": [ "data-access", "components" ] }, { "name": "GetCoursePlansParams", "type": "interface", "file": "types.ts", "definition": "{ classId?, teacherId?, subjectId?, status? }", "usedBy": [ "getCoursePlans", "getCoursePlansAction" ] }, { "name": "ReorderCoursePlanItemInput", "type": "interface", "file": "types.ts", "definition": "{ id, week }", "usedBy": [ "reorderCoursePlanItems" ] }, { "name": "CoursePlanQueryScope", "type": "interface", "file": "types.ts", "purpose": "P0-3 新增:数据范围上下文,用于 data-access 层权限过滤", "definition": "{ userId, isAdmin, classIds?, teacherId?, studentId? }", "usedBy": [ "getCoursePlans", "getCoursePlanById", "resolveScope (actions.ts)" ] }, { "name": "GradeCoursePlanProgressItem", "type": "interface", "file": "types.ts", "purpose": "v4-P2-7 新增:年级进度项", "definition": "{ planId, classId, className, subjectId, subjectName, teacherName, semester, totalHours, completedHours, progressRate, status, itemCount, completedItemCount }", "usedBy": [ "getGradeCoursePlanProgress" ] }, { "name": "GradeCoursePlanProgressResult", "type": "interface", "file": "types.ts", "purpose": "v4-P2-7 新增:年级进度结果", "definition": "{ gradeId, overall: { totalPlans, totalHours, completedHours, progressRate, activePlans, completedPlans }, items: GradeCoursePlanProgressItem[] }", "usedBy": [ "getGradeCoursePlanProgress" ] }, { "name": "isCoursePlanStatus", "type": "type guard", "file": "types.ts", "purpose": "P1-4 新增:替代 as 断言,运行时类型安全", "definition": "(v: unknown) => v is CoursePlanStatus", "usedBy": [ "course-plan-list.tsx", "course-plan-form.tsx", "admin/course-plans/page.tsx", "teacher/course-plans/page.tsx" ] }, { "name": "isCoursePlanSemester", "type": "type guard", "file": "types.ts", "purpose": "P1-4 新增:替代 as 断言,运行时类型安全", "definition": "(v: unknown) => v is CoursePlanSemester", "usedBy": [ "course-plan-form.tsx" ] }, { "name": "CoursePlanWidgetId", "type": "type", "file": "types.ts", "purpose": "P2 新增:配置驱动 Widget 标识", "definition": "\"list\" | \"progress\" | \"calendar\" | \"bulkActions\" | \"export\" | \"templates\"", "usedBy": [ "ROLE_WIDGET_CONFIG" ] }, { "name": "RoleWidgetConfig", "type": "interface", "file": "types.ts", "purpose": "P2 新增:角色 Widget 配置", "definition": "{ widgets: CoursePlanWidgetId[], canManage: boolean, detailBaseHref: string }", "usedBy": [ "ROLE_WIDGET_CONFIG" ] }, { "name": "ROLE_WIDGET_CONFIG", "type": "const", "file": "types.ts", "purpose": "P2 新增:admin/teacher/parent/student 四角色 Widget 配置(配置驱动,新增角色仅需修改配置)", "definition": "Record", "usedBy": [ "未来 parent/student 路由" ] } ], "components": [ { "name": "CoursePlanProgress", "file": "components/course-plan-progress.tsx", "purpose": "进度条组件(completedHours/totalHours 百分比)。✅ P1 修复:a11y aria-valuenow/min/max + i18n" }, { "name": "CoursePlanList", "file": "components/course-plan-list.tsx", "purpose": "课程计划列表(支持状态筛选,URL同步)。✅ P1 修复:使用 Next.js + isCoursePlanStatus 类型守卫 + i18n + useMemo 优化" }, { "name": "CoursePlanForm", "file": "components/course-plan-form.tsx", "purpose": "创建/编辑表单(班级、科目、教师、学年、学期、状态、课时、日期、大纲、目标)。✅ P1 修复:isCoursePlanSemester/isCoursePlanStatus 类型守卫 + successHref prop + i18n" }, { "name": "CoursePlanItemEditor", "file": "components/course-plan-item-editor.tsx", "purpose": "周计划条目编辑器(Dialog,支持创建/编辑/删除/切换完成)。✅ P1 修复:全文本 i18n + Promise 显式返回类型" }, { "name": "CoursePlanDetail", "file": "components/course-plan-detail.tsx", "purpose": "详情视图(计划信息、进度、大纲、目标、周计划表格、删除确认)。✅ P1 修复:批量选择 + bulkToggleItemsAction + successHref prop + trackCoursePlanEvent 埋点 + Next.js + i18n" } ] } }, "parent": { "path": "src/modules/parent", "description": "家长端仪表盘:聚合家长关联子女的学习数据(课表、作业、成绩、班级),支持多子女切换查看。家长通过 parentStudentRelations 表关联子女,DataScope 解析为 children 类型", "exports": { "dataAccess": [ { "name": "getChildren", "signature": "(parentId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.parentStudentRelations", "react.cache" ], "usedBy": [ "getParentDashboardData (内部)", "parent/children/[studentId]/page.tsx" ] }, { "name": "getChildBasicInfo", "signature": "(studentId: string, relation?: string | null) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.grades", "shared.db.schema.classEnrollments", "shared.db.schema.classes", "react.cache" ], "usedBy": [ "getChildDashboardData (内部)" ] }, { "name": "getChildDashboardData", "signature": "(studentId: string, relation?: string | null) => Promise", "file": "data-access.ts", "deps": [ "getChildBasicInfo", "classes/data-access.getStudentClasses", "classes/data-access.getStudentSchedule", "homework/data-access.getStudentHomeworkAssignments", "homework/data-access.getStudentDashboardGrades", "homework/data-access.getStudentExamResults", "grades/data-access.getStudentGradeSummary", "react.cache" ], "usedBy": [ "parent/children/[studentId]/page.tsx", "getParentDashboardData (内部)" ] }, { "name": "getParentDashboardData", "signature": "(parentId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.users", "getChildren", "getChildDashboardData", "react.cache" ], "usedBy": [ "parent/dashboard/page.tsx" ] }, { "name": "verifyParentChildRelation", "signature": "(studentId: string, parentId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.parentStudentRelations", "react.cache" ], "usedBy": [ "parent/children/[studentId]/page.tsx" ] }, { "name": "getChildNameList", "signature": "(parentId: string) => Promise>", "file": "data-access.ts", "deps": [ "getChildren", "users/data-access.getUserNamesByIds", "react.cache" ], "usedBy": [ "parent/children/[studentId]/page.tsx" ] }, { "name": "getParentIdsByStudentIds", "signature": "(studentIds: string[]) => Promise", "file": "data-access.ts", "purpose": "v4-P1-5 新增:批量查询多个学生的家长 userId 列表,用于通知场景——报告/成绩发布时同步通知所有相关家长。返回去重后的 parentId 数组,使用 react.cache 包装实现请求级 memoization,空数组直接返回 []", "deps": [ "shared.db", "shared.db.schema.parentStudentRelations", "drizzle-orm.inArray", "react.cache" ], "usedBy": [ "diagnostic/actions.publishReportAction", "grades/lib/notify.notifyGradeEntered" ] }, { "name": "getParentStudentMapByStudentIds", "signature": "(studentIds: string[]) => Promise>", "file": "data-access.ts", "purpose": "L-2 新增:批量查询学生→家长映射(保留关联关系),用于考勤通知等需按家长-学生分组发送的场景,避免 N+1 查询。返回数组形如 [{ parentId, studentId }],同一家长多个学生会出现多条", "deps": [ "shared.db", "shared.db.schema.parentStudentRelations", "drizzle-orm.inArray", "react.cache" ], "usedBy": [ "attendance/notifications.notifyParentsOfAbsence" ] } ], "types": [ { "name": "ParentChildRelation", "type": "type", "file": "types.ts", "definition": "{ id, parentId, studentId, relation: string | null, createdAt: string }", "usedBy": [ "getChildren", "getParentDashboardData" ] }, { "name": "ChildBasicInfo", "type": "type", "file": "types.ts", "definition": "{ id, name: string | null, email, image: string | null, gradeName: string | null, className: string | null, classId: string | null, relation: string | null }", "usedBy": [ "getChildBasicInfo", "ChildDashboardData.basicInfo" ] }, { "name": "ChildScheduleItem", "type": "type", "file": "types.ts", "definition": "{ id, classId, className, course, startTime, endTime, location: string | null }", "usedBy": [ "ChildDashboardData.todaySchedule", "child-schedule-card.tsx" ] }, { "name": "ChildWeeklyScheduleItem", "type": "type", "file": "types.ts", "definition": "ChildScheduleItem & { weekday: 1 | 2 | 3 | 4 | 5 | 6 | 7 }", "usedBy": [ "ChildDashboardData.weeklySchedule", "child-schedule-card.tsx" ] }, { "name": "ChildHomeworkSummary", "type": "type", "file": "types.ts", "definition": "{ pendingCount, submittedCount, gradedCount, overdueCount, recentAssignments: StudentHomeworkAssignmentListItem[] }", "usedBy": [ "ChildDashboardData.homeworkSummary", "child-homework-summary.tsx" ] }, { "name": "ChildDashboardData", "type": "type", "file": "types.ts", "definition": "{ basicInfo: ChildBasicInfo, enrolledClasses: StudentEnrolledClass[], todaySchedule: ChildScheduleItem[], weeklySchedule: ChildWeeklyScheduleItem[], homeworkSummary: ChildHomeworkSummary, gradeTrend: StudentDashboardGradeProps, gradeSummary: StudentGradeSummary | null, examResults: ChildExamResultItem[] }", "usedBy": [ "getChildDashboardData", "ParentDashboardData.children", "所有 child-* 组件" ] }, { "name": "ChildExamResultItem", "type": "type", "file": "types.ts", "definition": "{ submissionId, examId, examTitle, assignmentId, assignmentTitle, score, maxScore, submittedAt: string | null, status: string }", "usedBy": [ "ChildDashboardData.examResults", "child-exam-detail.tsx" ] }, { "name": "ParentDashboardData", "type": "type", "file": "types.ts", "definition": "{ parentName: string | null, children: ChildDashboardData[] }", "usedBy": [ "getParentDashboardData", "parent-dashboard.tsx" ] } ], "components": [ { "name": "ParentDashboard", "file": "components/parent-dashboard.tsx", "purpose": "主容器组件(问候语、待办横幅、4 宫格快捷入口、子女卡片网格、空状态引导)" }, { "name": "ParentAttentionBanner", "file": "components/parent-attention-banner.tsx", "purpose": "v4 新增:仪表盘顶部待办事项横幅,聚合所有子女的逾期作业、待办、考勤、公告数量" }, { "name": "ParentAttendanceWarning", "file": "components/parent-attendance-warning.tsx", "purpose": "v4 新增:考勤页异常预警横幅,聚合缺勤/迟到/低出勤率预警" }, { "name": "ParentAttendanceRateCard", "file": "components/parent-attendance-rate-card.tsx", "purpose": "v4 新增:考勤页出勤率汇总卡片,聚合所有子女的平均出勤率、缺勤、迟到总数" }, { "name": "ParentAttendanceCalendar", "file": "components/parent-attendance-calendar.tsx", "purpose": "v4 新增:考勤月历视图,按状态着色每日出勤状态,支持按月切换(use client)" }, { "name": "ChildGradeDetail", "file": "components/child-grade-detail.tsx", "purpose": "v4 新增:成绩详情视图,按科目分组展示平均分、趋势、最近成绩,提供单科分析" }, { "name": "ChildHomeworkDetail", "file": "components/child-homework-detail.tsx", "purpose": "v4 新增:作业详情视图,展示所有作业的完整信息(状态、截止时间、提交时间、分数、尝试次数)" }, { "name": "ParentExportButton", "file": "components/parent-export-button.tsx", "purpose": "v4 新增:成绩导出按钮(占位,待后端实现 Server Action)" }, { "name": "ChildCard", "file": "components/child-card.tsx", "purpose": "子女卡片(头像、姓名、班级、待完成/逾期/平均分统计、异常红色高亮、趋势图标、点击跳转详情)" }, { "name": "ChildDetailHeader", "file": "components/child-detail-header.tsx", "purpose": "子女详情页头部(面包屑、返回按钮、头像、姓名、班级、年级、关系、邮箱掩码)" }, { "name": "ChildDetailPanel", "file": "components/child-detail-panel.tsx", "purpose": "v4 重写:子女详情面板 Tab 容器(Overview/Homework/Grades/Schedule/Attendance/Diagnostic 六 Tab 切换,支持 ?tab= 参数)" }, { "name": "SiblingSwitcher", "file": "components/child-detail-panel.tsx", "purpose": "v4 新增:详情页头部多子女切换器(Tabs 形式,避免返回仪表盘)" }, { "name": "ChildHomeworkSummary", "file": "components/child-homework-summary.tsx", "purpose": "子女作业概览(pending/submitted/graded/overdue 统计 + 最近作业列表含科目标识)" }, { "name": "ChildGradeSummary", "file": "components/child-grade-summary.tsx", "purpose": "子女成绩概览(Recharts 折线图趋势 + 最新分数 + 趋势图标 + 班级排名 Top X% + 最近成绩列表,use client)" }, { "name": "ChildScheduleCard", "file": "components/child-schedule-card.tsx", "purpose": "子女课表卡片(支持今日课表 + 完整周课表两种视图,周课表按 weekday 分组并高亮今日)" }, { "name": "ParentChildrenDataPage", "file": "components/parent-children-data-page.tsx", "purpose": "多子女数据聚合页共享布局(标题、描述、headerExtra 额外内容、空状态、子女列表)" }, { "name": "ParentNoChildrenPage", "file": "components/parent-children-data-page.tsx", "purpose": "dataScope 为空时的统一空状态页面" }, { "name": "ChildExamDetail", "file": "components/child-exam-detail.tsx", "purpose": "V3-11 新增:家长端子女考试详情视图。展示考试成绩汇总卡片(已参加考试数、平均分、最高分)、考试成绩列表(考试标题、分数、得分率 Progress、提交时间)。对标智学网家长端考试详情。集成于 child-detail-panel.tsx 的 exams Tab。P3-4 改进:所有硬编码文本替换为 next-intl 翻译键 examHomework.homework.parentExam.*(zh-CN + en 双语),触控目标 ≥ 44px,语义化标签 + ARIA 属性。", "deps": [ "next-intl.useTranslations", "next/link", "lucide-react (GraduationCap/TrendingUp/Award/BookOpen/ChevronRight)", "shared/components/ui/card", "shared/components/ui/badge", "shared/components/ui/progress", "shared/components/ui/empty-state", "shared/lib/utils.formatDate" ], "usedBy": [ "parent/components/child-detail-panel.tsx (exams Tab)" ] } ] } }, "messaging": { "path": "src/modules/messaging", "description": "站内私信系统:用户间私信收发(支持回复链)。通知相关 UI 组件和 CRUD Action 已迁移至 notifications 模块(P1-4 修复)。V3 深度审计已完成:删除死代码 getMessageDetailAction (P0-4)、getMessagesPageData 编排迁出 (P0-3)、新增 isReceiverAllowed 二次校验 (P0-1)、getMessageThread 增加 userId 权限校验 (P0-2)、parentMessageId 归属校验 (P0-6)、新增 getMessageThreadAction (P1-3)、MessageCompose 自动保存竞态修复 (P0-5)、RecipientRole 类型收紧 (P1-6)、buildReplyHref 纯函数抽取 (P1-7)、message.send_failed 失败埋点 (P1-9)、草稿列表 UI (P1-1)、星标 Tab (P1-2)、分页 i18n (P1-4)", "exports": { "actions": [ { "name": "sendMessageAction", "permission": "MESSAGE_SEND", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "发送消息(同时通过 notifications dispatcher 为收件人创建多渠道通知;支持 parentMessageId 回复;trackEvent 埋点 message.sent/message.send_failed;通知标题 i18n 化通过 getTranslations('messages') 生成 notification.messageTitle / messageTitleNoSubject)。V3 安全加固:P0-1 isReceiverAllowed 二次校验收件人在 sender DataScope 范围内(防绕过前端 Select 构造 FormData 越权发送);P0-6 校验 parentMessageId 归属当前用户参与会话(防注入他人线程);P1-9 catch 块新增 message.send_failed 失败埋点(权限拒绝除外,避免噪音)。P2-5 新增 isEitherUserBlocked 屏蔽校验:发送方或接收方任一屏蔽对方则禁止发送", "deps": [ "requirePermission", "shared/db", "data-access.createMessage", "data-access.isReceiverAllowed", "data-access.getMessageById", "data-access.isEitherUserBlocked", "notifications.dispatcher.sendNotification", "trackEvent", "revalidatePath", "next-intl/server.getTranslations" ], "usedBy": [ "message-compose.tsx" ] }, { "name": "markMessageAsReadAction", "permission": "MESSAGE_READ", "signature": "(id: string) => Promise>", "purpose": "标记消息已读(设置 readAt;P2-11 新增 trackEvent 埋点)", "deps": [ "requirePermission", "schema.MessageIdSchema", "data-access.markMessageAsRead", "trackEvent", "revalidatePath" ], "usedBy": [ "message-detail.tsx", "messages/[id]/page.tsx" ] }, { "name": "deleteMessageAction", "permission": "MESSAGE_DELETE", "signature": "(id: string) => Promise>", "purpose": "删除消息(仅发送者或接收者可删;P2-11 新增 trackEvent 埋点)", "deps": [ "requirePermission", "schema.MessageIdSchema", "data-access.deleteMessage", "trackEvent", "revalidatePath" ], "usedBy": [ "message-detail.tsx" ] }, { "name": "getMessagesAction", "permission": "MESSAGE_READ", "signature": "(params: { type: MessageType; page?, pageSize?, keyword?, starredOnly? }) => Promise>>", "purpose": "获取消息列表(收件箱/已发送/星标,分页,关键词搜索;客户端通过 useMessageSearch hook 调用)。V3 新增 starredOnly 参数支持星标 Tab 筛选 (P1-2)", "deps": [ "requirePermission", "data-access.getMessages" ], "usedBy": [ "message-list.tsx (via useMessageSearch hook)" ] }, { "name": "getMessageThreadAction", "permission": "MESSAGE_READ", "signature": "(messageId: string) => Promise>", "purpose": "V3 P1-3 新增:获取消息线程(根消息 + 回复链)。替代原 getMessageDetailAction (P0-4 删除死代码)。统一通过 Server Action 入口做权限校验,data-access.getMessageThread 内部校验当前用户对根消息的访问权(P0-2)。返回 Message[] 按 createdAt 倒序,UI 层按需 reverse 为正序展示", "deps": [ "requirePermission", "schema.MessageIdSchema", "data-access.getMessageThread" ], "usedBy": [ "message-detail.tsx" ] }, { "name": "getRecipientsAction", "permission": "MESSAGE_SEND", "signature": "() => Promise>", "purpose": "获取可发送对象列表(按 DataScope 过滤)", "deps": [ "requirePermission", "data-access.getRecipients" ], "usedBy": [ "messages/compose/page.tsx" ] }, { "name": "getUnreadMessageCountAction", "permission": "MESSAGE_READ", "signature": "() => Promise>", "purpose": "获取当前用户未读私信计数(unread-message-badge 组件每 60 秒轮询)", "deps": [ "requirePermission", "data-access.getUnreadMessageCount" ], "usedBy": [ "unread-message-badge.tsx" ] }, { "name": "toggleMessageStarAction", "permission": "MESSAGE_READ", "signature": "(messageId: string) => Promise>", "purpose": "V2-P2-13c 新增:切换消息星标状态(仅接收方可标记;trackEvent 埋点 message.star_toggled)", "deps": [ "requirePermission", "schema.MessageIdSchema", "data-access.toggleMessageStar", "trackEvent", "revalidatePath" ], "usedBy": [ "message-detail.tsx", "message-list.tsx" ] }, { "name": "getMessageDraftsAction", "permission": "MESSAGE_SEND", "signature": "() => Promise>", "purpose": "V2-P2-13c 新增:获取当前用户的消息草稿列表(按 updatedAt 倒序)", "deps": [ "requirePermission", "data-access.getMessageDrafts" ], "usedBy": [ "messages/compose/page.tsx" ] }, { "name": "saveMessageDraftAction", "permission": "MESSAGE_SEND", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "V2-P2-13c 新增:保存消息草稿(FormData 含 draftId 时更新,否则创建新草稿;返回草稿 ID)。P2-10 跨设备草稿同步增强:FormData 可携带 deviceId 和 expectedVersion 字段;updateMessageDraft 返回 \"ok\" | \"not_found\" | \"conflict\":conflict 时返回失败(客户端应拉取最新版本);not_found 时重建草稿(其他设备已删除);新建草稿时设置 version=1", "deps": [ "requirePermission", "data-access.createMessageDraft", "data-access.updateMessageDraft" ], "usedBy": [ "messages/compose/page.tsx" ] }, { "name": "deleteMessageDraftAction", "permission": "MESSAGE_SEND", "signature": "(draftId: string) => Promise>", "purpose": "V2-P2-13c 新增:删除消息草稿(仅草稿所有者可删;revalidatePath /messages/compose)", "deps": [ "requirePermission", "data-access.deleteMessageDraft", "revalidatePath" ], "usedBy": [ "messages/compose/page.tsx" ] }, { "name": "bulkMarkMessagesAsReadAction", "permission": "MESSAGE_READ", "signature": "(ids: string[]) => Promise>", "purpose": "P2-1 批量操作新增:批量标记已读(inArray + receiverId=userId + isRead=false 条件;返回 affectedRows 数量;trackEvent message.marked_read bulk=true)", "deps": [ "requirePermission", "schema.BulkMessageIdsSchema", "data-access.bulkMarkMessagesAsRead", "trackEvent", "revalidatePath" ], "usedBy": [ "message-list.tsx" ] }, { "name": "bulkDeleteMessagesAction", "permission": "MESSAGE_DELETE", "signature": "(ids: string[]) => Promise>", "purpose": "P2-1 批量操作新增:批量软删除(事务内分别更新 senderDeletedAt/receiverDeletedAt;trackEvent message.deleted bulk=true)", "deps": [ "requirePermission", "schema.BulkMessageIdsSchema", "data-access.bulkDeleteMessages", "trackEvent", "revalidatePath" ], "usedBy": [ "message-list.tsx" ] }, { "name": "bulkToggleMessagesStarAction", "permission": "MESSAGE_READ", "signature": "(ids: string[]) => Promise>", "purpose": "P2-1 批量操作新增:批量切换星标(先查当前状态,按目标状态分组批量 UPDATE;仅接收方可标记)", "deps": [ "requirePermission", "schema.BulkMessageIdsSchema", "data-access.bulkToggleMessagesStar", "trackEvent", "revalidatePath" ], "usedBy": [ "message-list.tsx" ] }, { "name": "recallMessageAction", "permission": "MESSAGE_SEND", "signature": "(messageId: string) => Promise>", "purpose": "P2-4 消息撤回:仅发送方在 MESSAGE_RECALL_WINDOW_MS (2 分钟) 窗口内可撤回;返回 ok/not_found/expired/already_recalled 状态映射为 ActionState;trackEvent message.recalled", "deps": [ "requirePermission", "schema.MessageIdSchema", "data-access.recallMessage", "trackEvent", "revalidatePath" ], "usedBy": [ "message-detail.tsx" ] }, { "name": "getMessageTemplatesAction", "permission": "MESSAGE_SEND", "signature": "() => Promise>", "purpose": "P2-6 快捷回复模板:获取当前用户的模板列表(按 sortOrder 升序)", "deps": [ "requirePermission", "data-access.getMessageTemplates" ], "usedBy": [ "message-compose.tsx" ] }, { "name": "saveMessageTemplateAction", "permission": "MESSAGE_SEND", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "P2-6 快捷回复模板:保存模板(FormData 含 templateId 时更新,否则创建新模板;trackEvent message.template_created)", "deps": [ "requirePermission", "schema.MessageTemplateSchema", "data-access.createMessageTemplate", "data-access.updateMessageTemplate", "trackEvent", "revalidatePath" ], "usedBy": [ "message-compose.tsx" ] }, { "name": "deleteMessageTemplateAction", "permission": "MESSAGE_SEND", "signature": "(templateId: string) => Promise>", "purpose": "P2-6 快捷回复模板:删除模板(仅 owner 可删;revalidatePath /messages/compose)", "deps": [ "requirePermission", "schema.MessageIdSchema", "data-access.deleteMessageTemplate", "revalidatePath" ], "usedBy": [ "message-compose.tsx" ] }, { "name": "sendGroupMessageAction", "permission": "MESSAGE_SEND", "signature": "(prevState: ActionState<{ recipientCount: number }> | null, formData: FormData) => Promise>", "purpose": "P2-2 群组消息:教师向所教班级所有家长群发消息(fan-out on write 策略)。校验 ctx.dataScope.type === \"class_taught\" 且 classId 在 dataScope.classIds 范围内(防越权群发);为每个家长创建独立 messages 行,共享同一 groupMessageId 便于后续聚合查询;通过 classes data-access 获取学生 ID 再通过 parent data-access 获取家长 ID(避免直接跨模块 JOIN 表);批量发送通知;trackEvent message.group_sent / message.group_send_failed", "deps": [ "requirePermission", "schema.SendGroupMessageSchema", "data-access.sendGroupMessage", "notifications.dispatcher.sendNotification", "trackEvent", "revalidatePath", "next-intl/server.getTranslations" ], "usedBy": [ "message-group-compose.tsx", "messages/group-compose/page.tsx" ] }, { "name": "reportMessageAction", "permission": "MESSAGE_READ", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "P2-5 消息举报:校验消息存在且当前用户为接收方(不能举报自己发的消息);防重复举报(同一用户对同一消息只能举报一次);trackEvent message.reported / message.report_failed", "deps": [ "requirePermission", "schema.ReportMessageSchema", "data-access.getMessageById", "data-access.reportMessage", "trackEvent" ], "usedBy": [ "message-report-block.tsx" ] }, { "name": "checkReportedAction", "permission": "MESSAGE_READ", "signature": "(messageId: string) => Promise>", "purpose": "P2-5 检查当前用户是否已举报某条消息(用于 UI 禁用状态展示)", "deps": [ "requirePermission", "data-access.hasUserReportedMessage" ], "usedBy": [ "message-report-block.tsx" ] }, { "name": "blockUserAction", "permission": "MESSAGE_SEND", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "P2-5 屏蔽用户:防自屏蔽(blockerId === blockedId 返回 self_block);防重复屏蔽(uniqueIndex 约束);trackEvent message.user_blocked;revalidatePath /messages", "deps": [ "requirePermission", "schema.BlockUserSchema", "data-access.blockUser", "trackEvent", "revalidatePath" ], "usedBy": [ "message-report-block.tsx" ] }, { "name": "unblockUserAction", "permission": "MESSAGE_SEND", "signature": "(blockedId: string) => Promise>", "purpose": "P2-5 取消屏蔽用户:trackEvent message.user_unblocked;revalidatePath /messages", "deps": [ "requirePermission", "data-access.unblockUser", "trackEvent", "revalidatePath" ], "usedBy": [ "message-report-block.tsx" ] }, { "name": "getUserBlocksAction", "permission": "MESSAGE_SEND", "signature": "() => Promise>", "purpose": "P2-5 获取当前用户的屏蔽列表(含被屏蔽用户名)", "deps": [ "requirePermission", "data-access.getUserBlocks" ], "usedBy": [ "settings 页面(屏蔽列表管理)" ] }, { "name": "getMessageAttachmentsAction", "permission": "MESSAGE_READ", "signature": "(messageId: string) => Promise>", "purpose": "P2-3 消息附件:校验当前用户对消息有访问权(必须是发送方或接收方,防越权访问附件);附件存储复用 files 模块的 fileAttachments 表(targetType=\"message\")", "deps": [ "requirePermission", "data-access.getMessageById", "data-access.getMessageAttachments" ], "usedBy": [ "message-attachments.tsx", "message-detail.tsx" ] } ], "dataAccess": [ { "name": "getMessages", "signature": "(params: GetMessagesParams) => Promise>", "file": "data-access.ts", "purpose": "获取消息列表(收件箱/已发送/星标)。GetMessagesParams = { userId, type: MessageType, page?, pageSize?, keyword?, starredOnly? }。V3 P1-2 新增 starredOnly 参数支持星标 Tab 筛选", "deps": [ "shared.db", "shared.db.schema.messages", "shared.db.schema.users" ], "usedBy": [ "getMessagesAction" ] }, { "name": "getMessageById", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.messages", "shared.db.schema.users" ], "usedBy": [ "sendMessageAction (P0-6 parentMessageId 归属校验)", "getMessageDetailPageData", "messages/[id]/page.tsx" ] }, { "name": "getMessageThread", "signature": "(messageId: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V3 P0-2 加固:增加 userId 参数,where 子句加 (senderId=userId OR receiverId=userId) 校验当前用户对根消息的访问权,防止越权读取他人会话线程。返回 Message[] 按 createdAt 倒序", "deps": [ "shared.db", "shared.db.schema.messages", "shared.db.schema.users" ], "usedBy": [ "getMessageThreadAction" ] }, { "name": "isReceiverAllowed", "signature": "(userId: string, receiverId: string, scope: DataScope) => Promise", "file": "data-access.ts", "purpose": "V3 P0-1 新增:校验收件人是否在 sender 的 DataScope 允许范围内。在 sendMessageAction 中作为 Server Action 二次校验,防止绕过前端 Select 直接构造 FormData 提交任意 receiverId 的越权发送。实现复用 getRecipients 的结果,避免重复解析 DataScope", "deps": [ "data-access.getRecipients" ], "usedBy": [ "sendMessageAction" ] }, { "name": "createMessage", "signature": "(input: CreateMessageInput) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.messages", "@paralleldrive/cuid2" ], "usedBy": [ "sendMessageAction" ] }, { "name": "markMessageAsRead", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.messages" ], "usedBy": [ "markMessageAsReadAction", "messages/[id]/page.tsx" ] }, { "name": "deleteMessage", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.messages" ], "usedBy": [ "deleteMessageAction" ] }, { "name": "toggleMessageStar", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13c 新增:切换消息星标状态(仅接收方可标记;查询当前 isStarred 后取反更新)", "deps": [ "shared.db", "shared.db.schema.messages" ], "usedBy": [ "toggleMessageStarAction" ] }, { "name": "getUnreadMessageCount", "signature": "(userId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.messages" ], "usedBy": [ "getUnreadMessageCountAction", "unread-message-badge.tsx" ] }, { "name": "getMessageDetailPageData", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P1-3 新增:消息详情页编排函数,一次性获取消息详情并自动标记已读(仅当 receiverId === userId 且未读时);替代 page.tsx 中 after() + getMessageById + markMessageAsRead 的分散编排", "deps": [ "data-access.getMessageById", "data-access.markMessageAsRead" ], "usedBy": [ "messages/[id]/page.tsx" ] }, { "name": "getRecipients", "signature": "(userId: string, scope: DataScope) => Promise", "file": "data-access.ts", "purpose": "按 DataScope 过滤可发送对象列表:class_taught(教师→学生)、grade_managed(年级管理员→教师/学生)、all(管理员)、class_members(学生→自己班级的任课教师/班主任)、children(家长→孩子的班主任/任课教师)。V3 P1-6 类型加固:RecipientOption.role 收紧为 RecipientRole 联合类型(student | teacher | admin | parent),admin 分支补 role 字段,所有分支显式断言为 RecipientRole", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.classEnrollments", "shared.db.schema.classes", "shared.db.schema.grades", "classes.data-access.getTeacherIdsByClassIds", "classes.data-access.getStudentActiveClassId", "users.data-access.getUserNamesByIds" ], "usedBy": [ "getRecipientsAction", "data-access.isReceiverAllowed", "messages/compose/page.tsx" ] }, { "name": "getMessageDrafts", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13c 新增:获取用户消息草稿列表(按 updatedAt 倒序;React cache 包装;批量解析 receiverName)。V3 P1-1 新增 messages/compose/page.tsx 直接调用渲染草稿列表(替代仅通过 Action 调用)", "deps": [ "shared.db", "shared.db.schema.messageDrafts", "shared.db.schema.users" ], "usedBy": [ "getMessageDraftsAction", "messages/compose/page.tsx" ] }, { "name": "createMessageDraft", "signature": "(data: CreateMessageDraftInput) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13c 新增:创建消息草稿(cuid2 生成 ID;返回新草稿 ID)", "deps": [ "shared.db", "shared.db.schema.messageDrafts", "@paralleldrive/cuid2" ], "usedBy": [ "saveMessageDraftAction" ] }, { "name": "updateMessageDraft", "signature": "(id: string, userId: string, data: UpdateMessageDraftInput) => Promise<\"ok\" | \"not_found\" | \"conflict\">", "file": "data-access.ts", "purpose": "V2-P2-13c 新增:更新消息草稿(仅 owner 可改;未提供字段保留原值;updatedAt 由 onUpdateNow 自动更新)。P2-10 跨设备同步增强:先查现有草稿的 version 字段;若 expectedVersion 不匹配则返回 \"conflict\"(客户端应拉取最新版本);若草稿不存在返回 \"not_found\";成功时 version 自增 1(乐观锁)", "deps": [ "shared.db", "shared.db.schema.messageDrafts" ], "usedBy": [ "saveMessageDraftAction" ] }, { "name": "deleteMessageDraft", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13c 新增:删除消息草稿(仅 owner 可删)", "deps": [ "shared.db", "shared.db.schema.messageDrafts" ], "usedBy": [ "deleteMessageDraftAction" ] }, { "name": "getMessageDraftById", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13c 新增:按 ID 获取单条消息草稿(仅 owner 可读;解析 receiverName)", "deps": [ "shared.db", "shared.db.schema.messageDrafts", "shared.db.schema.users" ], "usedBy": [ "messages/compose/page.tsx" ] }, { "name": "bulkMarkMessagesAsRead", "signature": "(ids: string[], userId: string) => Promise", "file": "data-access.ts", "purpose": "P2-1 批量操作新增:批量标记已读(inArray + receiverId=userId + isRead=false 条件;返回 affectedRows 数量)", "deps": [ "shared.db", "shared.db.schema.messages" ], "usedBy": [ "bulkMarkMessagesAsReadAction" ] }, { "name": "bulkDeleteMessages", "signature": "(ids: string[], userId: string) => Promise", "file": "data-access.ts", "purpose": "P2-1 批量操作新增:批量软删除(事务内分别更新 senderDeletedAt/receiverDeletedAt)", "deps": [ "shared.db", "shared.db.schema.messages" ], "usedBy": [ "bulkDeleteMessagesAction" ] }, { "name": "bulkToggleMessagesStar", "signature": "(ids: string[], userId: string) => Promise", "file": "data-access.ts", "purpose": "P2-1 批量操作新增:批量切换星标(先查当前状态,按目标状态分组批量 UPDATE;仅接收方可标记)", "deps": [ "shared.db", "shared.db.schema.messages" ], "usedBy": [ "bulkToggleMessagesStarAction" ] }, { "name": "recallMessage", "signature": "(id: string, userId: string) => Promise<\"ok\" | \"not_found\" | \"expired\" | \"already_recalled\">", "file": "data-access.ts", "purpose": "P2-4 消息撤回:仅发送方在 MESSAGE_RECALL_WINDOW_MS (2 分钟) 窗口内可撤回;设置 recalledAt=now;已撤回的消息不能再撤回;导出常量 MESSAGE_RECALL_WINDOW_MS = 2*60*1000", "deps": [ "shared.db", "shared.db.schema.messages" ], "usedBy": [ "recallMessageAction" ] }, { "name": "sendGroupMessage", "signature": "(data: SendGroupMessageInput) => Promise", "file": "data-access.ts", "purpose": "P2-2 群组消息:教师向指定班级所有家长群发消息(fan-out on write 策略)。通过 classes.data-access.getStudentIdsByClassIds 获取学生 ID,再通过 parent.data-access.getParentIdsByStudentIds 获取家长 ID(去重 + 过滤发送者自己);为每个家长创建独立 messages 行,共享同一 groupMessageId 便于后续聚合查询;批量插入。返回 { groupMessageId, recipientIds }", "deps": [ "shared.db", "shared.db.schema.messages", "@paralleldrive/cuid2", "classes.data-access.getStudentIdsByClassIds", "parent.data-access.getParentIdsByStudentIds" ], "usedBy": [ "sendGroupMessageAction" ] }, { "name": "reportMessage", "signature": "(input: CreateMessageReportInput) => Promise<\"ok\" | \"already_reported\">", "file": "data-access.ts", "purpose": "P2-5 消息举报:创建举报记录;防重复举报(先查 reporterId+messageId 是否已存在);返回 ok 或 already_reported", "deps": [ "shared.db", "shared.db.schema.messageReports" ], "usedBy": [ "reportMessageAction" ] }, { "name": "hasUserReportedMessage", "signature": "(userId: string, messageId: string) => Promise", "file": "data-access.ts", "purpose": "P2-5 检查当前用户是否已举报某条消息(用于 UI 禁用状态展示)", "deps": [ "shared.db", "shared.db.schema.messageReports" ], "usedBy": [ "checkReportedAction" ] }, { "name": "blockUser", "signature": "(blockerId: string, blockedId: string) => Promise<\"ok\" | \"self_block\" | \"already_blocked\">", "file": "data-access.ts", "purpose": "P2-5 屏蔽用户:防自屏蔽(blockerId === blockedId 返回 self_block);防重复屏蔽(先查是否已存在,uniqueIndex 兜底);返回 ok/self_block/already_blocked", "deps": [ "shared.db", "shared.db.schema.userBlocks" ], "usedBy": [ "blockUserAction" ] }, { "name": "unblockUser", "signature": "(blockerId: string, blockedId: string) => Promise", "file": "data-access.ts", "purpose": "P2-5 取消屏蔽用户(仅 owner 可操作)", "deps": [ "shared.db", "shared.db.schema.userBlocks" ], "usedBy": [ "unblockUserAction" ] }, { "name": "isUserBlocked", "signature": "(blockerId: string, blockedId: string) => Promise", "file": "data-access.ts", "purpose": "P2-5 单向校验 blockerId 是否屏蔽了 blockedId", "deps": [ "shared.db", "shared.db.schema.userBlocks" ], "usedBy": [] }, { "name": "isEitherUserBlocked", "signature": "(userIdA: string, userIdB: string) => Promise", "file": "data-access.ts", "purpose": "P2-5 双向屏蔽校验:检查 userIdA 是否屏蔽了 userIdB,或 userIdB 是否屏蔽了 userIdA。在 sendMessageAction 中作为发送前置校验,任一方向屏蔽则禁止发送", "deps": [ "shared.db", "shared.db.schema.userBlocks" ], "usedBy": [ "sendMessageAction" ] }, { "name": "getBlockedUserIds", "signature": "(blockerId: string) => Promise", "file": "data-access.ts", "purpose": "P2-5 获取当前用户屏蔽的所有用户 ID 列表(用于发件人列表过滤)", "deps": [ "shared.db", "shared.db.schema.userBlocks" ], "usedBy": [] }, { "name": "getMessageReports", "signature": "(params?: { status?: string; page?: number; pageSize?: number }) => Promise>", "file": "data-access.ts", "purpose": "P2-5 管理员查看举报列表(按 status 筛选;分页;admin 管理后台使用)", "deps": [ "shared.db", "shared.db.schema.messageReports", "shared.db.schema.users" ], "usedBy": [ "admin 举报管理页面" ] }, { "name": "getUserBlocks", "signature": "(blockerId: string) => Promise", "file": "data-access.ts", "purpose": "P2-5 获取当前用户的屏蔽列表(含被屏蔽用户名解析)", "deps": [ "shared.db", "shared.db.schema.userBlocks", "shared.db.schema.users" ], "usedBy": [ "getUserBlocksAction" ] }, { "name": "getMessageTemplates", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "P2-6 快捷回复模板:获取用户的模板列表(按 sortOrder 升序;React cache 包装)", "deps": [ "shared.db", "shared.db.schema.messageTemplates" ], "usedBy": [ "getMessageTemplatesAction" ] }, { "name": "createMessageTemplate", "signature": "(data: CreateMessageTemplateInput) => Promise", "file": "data-access.ts", "purpose": "P2-6 快捷回复模板:创建模板(cuid2 生成 ID;返回新模板 ID)", "deps": [ "shared.db", "shared.db.schema.messageTemplates", "@paralleldrive/cuid2" ], "usedBy": [ "saveMessageTemplateAction" ] }, { "name": "updateMessageTemplate", "signature": "(id: string, userId: string, data: UpdateMessageTemplateInput) => Promise", "file": "data-access.ts", "purpose": "P2-6 快捷回复模板:更新模板(仅 owner 可改;未提供字段保留原值)", "deps": [ "shared.db", "shared.db.schema.messageTemplates" ], "usedBy": [ "saveMessageTemplateAction" ] }, { "name": "deleteMessageTemplate", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "P2-6 快捷回复模板:删除模板(仅 owner 可删)", "deps": [ "shared.db", "shared.db.schema.messageTemplates" ], "usedBy": [ "deleteMessageTemplateAction" ] }, { "name": "getMessageAttachments", "signature": "(messageId: string) => Promise", "file": "data-access.ts", "purpose": "P2-3 消息附件:复用 files 模块的 fileAttachments 表(targetType=\"message\");通过 files.data-access.getFileAttachmentsByTarget 获取附件列表;导出常量 MESSAGE_ATTACHMENT_TARGET_TYPE = \"message\"", "deps": [ "files.data-access.getFileAttachmentsByTarget" ], "usedBy": [ "getMessageAttachmentsAction" ] } ], "notificationPreferences": [ { "name": "getNotificationPreferences", "signature": "(userId: string) => Promise", "file": "notification-preferences.ts", "purpose": "re-export shim(实际逻辑在 notifications/preferences.ts,P0-4 / P1-5 修复后迁移;React cache 包装;若不存在则自动创建默认记录,并发冲突回退到查询)。V3 P1-3:消费方 Action 已迁回 notifications 模块,此 shim 仅为向后兼容保留", "deps": [ "notifications.preferences.getNotificationPreferences" ], "usedBy": [ "notifications.getNotificationPreferencesAction (V3-P1-3 迁移)", "settings/page.tsx" ] }, { "name": "upsertNotificationPreferences", "signature": "(userId: string, input: UpdateNotificationPreferencesInput) => Promise", "file": "notification-preferences.ts", "purpose": "re-export shim(实际逻辑在 notifications/preferences.ts,P0-4 / P1-5 修复后迁移;存在则部分更新,不存在则插入;未提供字段保留原值)。V3 P1-3:消费方 Action 已迁回 notifications 模块,此 shim 仅为向后兼容保留", "deps": [ "notifications.preferences.upsertNotificationPreferences" ], "usedBy": [ "notifications.updateNotificationPreferencesAction (V3-P1-3 迁移)" ] } ], "schemas": [ { "name": "SendMessageSchema", "type": "zod", "file": "schema.ts", "definition": "{ receiverId: string, subject?: string, content: string, parentMessageId?: string }", "usedBy": [ "sendMessageAction" ] } ], "types": [ { "name": "Message", "type": "type", "file": "types.ts", "definition": "{ id, senderId, receiverId, subject: string | null, content, isRead, isStarred, readAt: string | null, parentMessageId: string | null, createdAt, senderName, receiverName }", "usedBy": [ "messaging/components", "页面" ] }, { "name": "MessageListItem", "type": "type", "file": "types.ts", "definition": "消息列表项类型(同 Message 精简版)", "usedBy": [ "列表页" ] }, { "name": "MessageThread", "type": "type", "file": "types.ts", "definition": "{ root: Message, replies: Message[] }", "usedBy": [ "详情页" ] }, { "name": "Notification", "type": "type", "file": "types.ts", "definition": "{ id, userId, type: NotificationType, title, content: string | null, link: string | null, isRead, priority: NotificationPriority, isArchived, createdAt }", "usedBy": [ "notification-dropdown", "notification-list" ] }, { "name": "NotificationListItem", "type": "type", "file": "types.ts", "definition": "通知列表项类型(同 Notification)", "usedBy": [ "列表页" ] }, { "name": "NotificationType", "type": "type", "file": "types.ts", "definition": "'message' | 'announcement' | 'homework' | 'grade'", "usedBy": [ "data-access", "components" ] }, { "name": "MessageType", "type": "type", "file": "types.ts", "definition": "'inbox' | 'sent' | 'all'", "usedBy": [ "getMessages 参数 type 字段", "getMessagesAction 参数" ] }, { "name": "RecipientRole", "type": "type", "file": "types.ts", "definition": "V3 P1-6 新增:'student' | 'teacher' | 'admin' | 'parent'。用于 RecipientOption.role 字段,替代原 string 类型", "usedBy": [ "RecipientOption.role" ] }, { "name": "CreateMessageInput", "type": "type", "file": "types.ts", "definition": "{ senderId, receiverId, subject?, content, parentMessageId? }", "usedBy": [ "createMessage" ] }, { "name": "CreateNotificationInput", "type": "type", "file": "types.ts", "definition": "{ userId, type: NotificationType, title, content?, link?, priority? }", "usedBy": [ "createNotification" ] }, { "name": "RecipientOption", "type": "type", "file": "types.ts", "definition": "{ id: string, name: string, email: string, role?: RecipientRole }。V3 P1-6 类型加固:role 从 string? 收紧为 RecipientRole? 联合类型(student | teacher | admin | parent)", "usedBy": [ "getRecipients", "getRecipientsAction", "compose 页面下拉选项" ] }, { "name": "MessageDraft", "type": "type", "file": "types.ts", "definition": "V2-P2-13c 新增:{ id, userId, receiverId: string | null, receiverName: string | null, subject: string | null, content: string | null, parentMessageId: string | null, createdAt, updatedAt }", "usedBy": [ "getMessageDrafts", "getMessageDraftById", "getMessageDraftsAction", "messages/compose/page.tsx" ] }, { "name": "CreateMessageDraftInput", "type": "type", "file": "types.ts", "definition": "V2-P2-13c 新增:{ userId, receiverId?, subject?, content?, parentMessageId? }", "usedBy": [ "createMessageDraft" ] }, { "name": "UpdateMessageDraftInput", "type": "type", "file": "types.ts", "definition": "V2-P2-13c 新增:{ receiverId?, subject?, content?, parentMessageId? }(未提供字段保留原值)", "usedBy": [ "updateMessageDraft" ] }, { "name": "PaginatedResult", "type": "type", "file": "types.ts", "definition": "{ items: T[], total: number, page: number, pageSize: number, totalPages: number }", "usedBy": [ "getMessages", "getNotifications" ] }, { "name": "NotificationPreferences", "type": "interface", "file": "types.ts", "definition": "{ id, userId, emailEnabled, smsEnabled, pushEnabled, homeworkNotifications, gradeNotifications, announcementNotifications, messageNotifications, attendanceNotifications, createdAt, updatedAt }", "usedBy": [ "notification-preferences", "getNotificationPreferencesAction", "updateNotificationPreferencesAction", "settings/components/notification-preferences-form" ] }, { "name": "UpdateNotificationPreferencesInput", "type": "interface", "file": "types.ts", "definition": "{ emailEnabled?, smsEnabled?, pushEnabled?, homeworkNotifications?, gradeNotifications?, announcementNotifications?, messageNotifications?, attendanceNotifications? }", "usedBy": [ "upsertNotificationPreferences", "updateNotificationPreferencesAction" ] } ], "components": [ { "name": "MessageList", "file": "components/message-list.tsx", "purpose": "消息列表(收件箱/已发送/星标 Tab 切换,已读/未读标记,usePermission 控制写消息按钮)。V3 P1-2 新增星标 Tab;P1-4 分页 aria-label/显示 i18n 化(pagination.nav/previous/next/page);星标按钮 aria-pressed;搜索框 aria-busy" }, { "name": "MessageDetail", "file": "components/message-detail.tsx", "purpose": "消息详情(含回复线程、回复/删除操作,AlertDialog 删除确认,usePermission 控制按钮可见性)。V3 P1-3 新增线程渲染区块(useEffect 调用 getMessageThreadAction 加载,时间正序展示);P1-7 使用 buildReplyHref 纯函数替代内联 URL 拼接;星标按钮 aria-pressed" }, { "name": "MessageCompose", "file": "components/message-compose.tsx", "purpose": "写消息表单(收件人 Select、主题 Input、内容 Textarea,支持回复模式;fieldErrors 状态 + aria-invalid 字段级服务端校验错误展示)。V3 P0-5 修复:submittedRef 标志位防止提交后自动保存竞态(handleSubmit 设置 true,自动保存 effect/setTimeout/Promise.then 检查并跳过,失败时重置)" }, { "name": "MessageDraftList", "file": "components/message-draft-list.tsx", "purpose": "V3 P1-1 新增:草稿列表组件(渲染草稿列表 + 继续编辑链接 + 删除草稿)。删除使用 ConfirmDeleteDialog + 乐观更新(先从 state 移除,失败回滚)+ 空状态展示。i18n 键:drafts.title/resume/delete/noContent/updatedAt" }, { "name": "UnreadMessageBadge", "file": "components/unread-message-badge.tsx", "purpose": "未读消息计数徽章(侧边栏 Messages 导航项旁显示,每 30 秒轮询 getUnreadMessageCountAction;轮询间隔提取为 POLL_INTERVAL_MS 常量)" }, { "name": "MessageGroupCompose", "file": "components/message-group-compose.tsx", "purpose": "P2-2 群组消息撰写组件:班级选择器(Select)+ 主题 Input + 内容 Textarea + 模板选择器(调用 getMessageTemplatesAction)。提交调用 sendGroupMessageAction,返回 recipientCount;表单状态 useActionState 管理;i18n 键:title.groupCompose / form.class / messages.groupSent 等", "usedBy": [ "messages/group-compose/page.tsx" ] }, { "name": "MessageReportBlock", "file": "components/message-report-block.tsx", "purpose": "P2-5 举报+屏蔽 UI 组件:两个按钮(举报 + 屏蔽)+ 举报 Dialog(理由选择 spam/harassment/inappropriate/other + 描述)+ 屏蔽确认 Dialog。仅在 senderId !== currentUserId 时渲染;调用 reportMessageAction / blockUserAction / unblockUserAction;i18n 键:actions.report / actions.block / report.* / block.*", "usedBy": [ "message-detail.tsx" ] }, { "name": "MessageAttachmentUploader", "file": "components/message-attachments.tsx", "purpose": "P2-3 附件上传组件:拖拽上传区 + 文件列表(useFileUpload hook);targetType=\"message\",targetId 通过 useState 懒初始化生成(避免 render 期调用 Date.now() 触发 react-hooks/purity 警告);i18n 键:attachments.*", "usedBy": [ "message-compose.tsx" ] }, { "name": "MessageAttachmentList", "file": "components/message-attachments.tsx", "purpose": "P2-3 附件展示组件:调用 getMessageAttachmentsAction 获取附件列表;展示文件名 + 下载链接;i18n 键:attachments.download / attachments.empty", "usedBy": [ "message-detail.tsx" ] } ], "hooks": [ { "name": "useMessageSearch", "file": "hooks/use-message-search.ts", "purpose": "消息搜索 hook(400ms 防抖 + 请求竞态取消,通过 requestIdRef 匹配最新请求)", "usedBy": [ "message-list.tsx" ] } ], "lib": [ { "name": "buildReplyHref", "file": "lib/build-reply-href.ts", "purpose": "V3 P1-7 新增:构建回复链接 href 的纯函数。替代 message-detail.tsx 内联 URL 拼接,提升可测试性。签名:buildReplyHref(message, currentUserId, canSend) => string | undefined。逻辑:canSend 为 false 返回 undefined;receiverId === currentUserId 则 replyToId = senderId,否则 = receiverId;subject 自动加 Re: 前缀", "usedBy": [ "message-detail.tsx" ] }, { "name": "isRecipientRole / isMessageReportReason / isMessageReportStatus / toMessageReportReason / toMessageReportStatus", "file": "lib/type-guards.ts", "purpose": "✅ 类型守卫专项重构(2026-07-04)新增:替代 messaging 模块内的 as 断言。isXxx 守卫收窄 unknown 到对应联合类型;toXxx 安全窄化函数在守卫失败时返回 fallback 默认值(reason→'other',status→'pending')。用于 data-access.ts mapMessageReport 替代 `r.reason as MessageReportReason`,以及 message-report-block.tsx Select onChange 替代 `v as MessageReportReason`", "usedBy": [ "data-access.ts", "components/message-report-block.tsx" ] } ] } }, "notifications": { "path": "src/modules/notifications", "description": "通知渠道集成层:基于用户通知偏好(notification_preferences)将通知分发到站内消息/SMS/微信公众号/邮件多渠道。所有渠道实现统一 NotificationChannelSender 接口,dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务)。P1-4 修复后新增通知 UI 组件和通知 CRUD Server Action。", "exports": { "actions": [ { "name": "sendNotificationAction", "permission": "MESSAGE_SEND", "signature": "(payload: NotificationPayload) => Promise>", "purpose": "发送通知给指定用户(按偏好多渠道分发)", "deps": [ "requirePermission", "dispatcher.sendNotification" ], "usedBy": [ "待扩展" ] }, { "name": "sendClassNotificationAction", "permission": "MESSAGE_SEND", "signature": "(classId: string, payload: Omit) => Promise>", "purpose": "发送班级通知(批量发送给班级所有学生;教师只能给自己所教班级发送)", "deps": [ "requirePermission", "db.schema.classEnrollments", "db.schema.classes", "dispatcher.sendBatchNotifications" ], "usedBy": [ "待扩展" ] }, { "name": "getNotificationsAction", "permission": "MESSAGE_READ", "signature": "(params?: { page?, pageSize?, unreadOnly? }) => Promise>>", "purpose": "P1-4 新增(从 messaging 迁移):获取当前用户通知列表(分页)", "deps": [ "requirePermission", "data-access.getNotifications" ], "usedBy": [ "hooks/use-notification-stream.ts", "notification-list.tsx" ] }, { "name": "getUnreadNotificationCountAction", "permission": "MESSAGE_READ", "signature": "() => Promise>", "purpose": "P1-4 新增(从 messaging 迁移):获取当前用户未读通知计数", "deps": [ "requirePermission", "data-access.getUnreadNotificationCount" ], "usedBy": [ "hooks/use-notification-stream.ts" ] }, { "name": "markNotificationAsReadAction", "permission": "MESSAGE_READ", "signature": "(notificationId: string) => Promise>", "purpose": "P1-4 新增(从 messaging 迁移):标记单条通知已读;P2-11 新增 trackEvent 埋点", "deps": [ "requirePermission", "schema.NotificationIdSchema", "data-access.markNotificationAsRead", "trackEvent", "revalidatePath" ], "usedBy": [ "notification-dropdown.tsx", "notification-list.tsx" ] }, { "name": "markAllNotificationsAsReadAction", "permission": "MESSAGE_READ", "signature": "() => Promise>", "purpose": "P1-4 新增(从 messaging 迁移):标记所有通知已读;P2-11 新增 trackEvent 埋点", "deps": [ "requirePermission", "data-access.markAllNotificationsAsRead", "trackEvent", "revalidatePath" ], "usedBy": [ "notification-dropdown.tsx", "notification-list.tsx" ] }, { "name": "archiveNotificationAction", "permission": "MESSAGE_READ", "signature": "(notificationId: string) => Promise>", "purpose": "V2-P2-13b 新增:将单条通知归档(归档后不在默认列表显示);含 trackEvent 埋点 notification.archived", "deps": [ "requirePermission", "schema.NotificationIdSchema", "data-access.archiveNotification", "trackEvent", "revalidatePath" ], "usedBy": [ "notification-list.tsx" ] }, { "name": "getNotificationPreferencesAction", "permission": "MESSAGE_READ", "signature": "() => Promise>", "purpose": "V3-P1-3 从 messaging 迁移:获取当前用户的通知偏好设置(首次访问自动创建默认记录)", "deps": [ "requirePermission", "preferences.getNotificationPreferences" ], "usedBy": [] }, { "name": "updateNotificationPreferencesAction", "permission": "MESSAGE_READ", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "V3-P1-3 从 messaging 迁移:更新当前用户的通知偏好设置(upsert 语义,未提供字段保留原值)", "deps": [ "requirePermission", "schema.UpdateNotificationPreferencesSchema", "preferences.upsertNotificationPreferences", "revalidatePath" ], "usedBy": [] } ], "dispatcher": [ { "name": "sendNotification", "signature": "(payload: NotificationPayload) => Promise", "file": "dispatcher.ts", "purpose": "发送单条通知:读取用户偏好+联系方式,按偏好选择渠道并行发送,记录日志", "deps": [ "preferences.getNotificationPreferences", "data-access.getUserContactInfo", "data-access.logNotificationSendBatch", "channels.sms-channel.createSmsSender", "channels.wechat-channel.createWechatSender", "channels.email-channel.createEmailSender", "channels.in-app-channel.createInAppSender" ], "usedBy": [ "sendNotificationAction", "sendClassNotificationAction", "messaging.sendMessageAction" ] }, { "name": "sendBatchNotifications", "signature": "(payloads: NotificationPayload[]) => Promise", "file": "dispatcher.ts", "purpose": "批量发送通知(每个用户独立选择渠道,并行发送)", "deps": [ "sendNotification" ], "usedBy": [ "sendClassNotificationAction" ] } ], "dataAccess": [ { "name": "getNotifications", "signature": "(userId: string, params?: { page?, pageSize?, unreadOnly?, unarchivedOnly?, priority? }) => Promise>", "file": "data-access.ts", "purpose": "获取用户站内通知列表(分页,支持 unreadOnly/unarchivedOnly/priority 过滤;P0-4 / P1-5 修复后从 messaging 迁移;V2-P2-13b 新增归档和优先级筛选,默认仅返回未归档)", "deps": [ "shared.db", "shared.db.schema.messageNotifications", "react.cache" ], "usedBy": [ "messaging.getNotificationsAction (via re-export)", "messages/page.tsx (via re-export)" ] }, { "name": "createNotification", "signature": "(input: CreateNotificationInput) => Promise", "file": "data-access.ts", "purpose": "创建站内通知记录(写入 message_notifications 表;P0-4 / P1-5 修复后从 messaging 迁移;V2-P2-13b 支持 priority 可选参数,默认 normal)", "deps": [ "shared.db", "shared.db.schema.messageNotifications", "@paralleldrive/cuid2" ], "usedBy": [ "channels.in-app-channel.InAppChannelSender.send" ] }, { "name": "markNotificationAsRead", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "标记单条通知已读(P0-4 / P1-5 修复后从 messaging 迁移)", "deps": [ "shared.db", "shared.db.schema.messageNotifications" ], "usedBy": [ "messaging.markNotificationAsReadAction (via re-export)" ] }, { "name": "markAllNotificationsAsRead", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "标记用户所有通知已读(P0-4 / P1-5 修复后从 messaging 迁移)", "deps": [ "shared.db", "shared.db.schema.messageNotifications" ], "usedBy": [ "messaging.markAllNotificationsAsReadAction (via re-export)" ] }, { "name": "getUnreadNotificationCount", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "获取用户未读通知数(P0-4 / P1-5 修复后从 messaging 迁移)", "deps": [ "shared.db", "shared.db.schema.messageNotifications", "react.cache" ], "usedBy": [ "待扩展" ] }, { "name": "archiveNotification", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13b 新增:将单条通知归档(isArchived 置 true,归档后不在默认列表显示)", "deps": [ "shared.db", "shared.db.schema.messageNotifications" ], "usedBy": [ "archiveNotificationAction" ] }, { "name": "unarchiveNotification", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "purpose": "V2-P2-13b 新增:取消归档通知(isArchived 置 false)", "deps": [ "shared.db", "shared.db.schema.messageNotifications" ], "usedBy": [ "待扩展" ] }, { "name": "getUserContactInfo", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "获取用户联系方式(phone/email;wechatOpenId 暂不支持,users 表无此字段)", "deps": [ "shared.db", "shared.db.schema.users", "react.cache" ], "usedBy": [ "dispatcher.sendNotification" ] }, { "name": "logNotificationSend", "signature": "(result: ChannelSendResult, payload?: { userId: string; title: string }) => Promise", "file": "data-access.ts", "purpose": "记录单条发送日志到 notification_logs 表(DB 写入失败时降级 console,不阻塞通知流程)", "deps": [ "shared.db", "shared.db.schema.notificationLogs", "@paralleldrive/cuid2" ], "usedBy": [ "logNotificationSendBatch" ] }, { "name": "logNotificationSendBatch", "signature": "(results: ChannelSendResult[], payload?: { userId: string; title: string }) => Promise", "file": "data-access.ts", "purpose": "批量记录发送日志(并行调用 logNotificationSend)", "deps": [ "logNotificationSend" ], "usedBy": [ "dispatcher.sendNotification", "dispatcher.sendBatchNotifications" ] } ], "preferences": [ { "name": "getNotificationPreferences", "signature": "(userId: string) => Promise", "file": "preferences.ts", "purpose": "获取用户通知偏好(React cache 包装;若不存在则自动创建默认记录,并发冲突回退到查询;P0-4 / P1-5 修复后从 messaging 迁移)", "deps": [ "shared.db", "shared.db.schema.notificationPreferences", "react.cache", "@paralleldrive/cuid2" ], "usedBy": [ "dispatcher.sendNotification", "messaging.getNotificationPreferencesAction (via re-export)", "settings/page.tsx (via re-export)" ] }, { "name": "upsertNotificationPreferences", "signature": "(userId: string, input: UpdateNotificationPreferencesInput) => Promise", "file": "preferences.ts", "purpose": "更新或插入用户通知偏好(存在则部分更新,不存在则插入;未提供字段保留原值;P0-4 / P1-5 修复后从 messaging 迁移)", "deps": [ "shared.db", "shared.db.schema.notificationPreferences", "@paralleldrive/cuid2" ], "usedBy": [ "messaging.updateNotificationPreferencesAction (via re-export)" ] } ], "channels": [ { "name": "createSmsSender", "file": "channels/sms-channel.ts", "purpose": "创建 SMS 渠道发送器(aliyun/tencent/mock,根据 SMS_PROVIDER 环境变量选择;SDK 动态 import)", "deps": [ "环境变量: SMS_PROVIDER, SMS_ACCESS_KEY_ID, SMS_ACCESS_KEY_SECRET, SMS_SIGN_NAME, SMS_TEMPLATE_CODE" ] }, { "name": "createWechatSender", "file": "channels/wechat-channel.ts", "purpose": "创建微信渠道发送器(配置完整用真实发送器,否则 Mock;access_token 带缓存)", "deps": [ "环境变量: WECHAT_APP_ID, WECHAT_APP_SECRET, WECHAT_TEMPLATE_ID" ] }, { "name": "createEmailSender", "file": "channels/email-channel.ts", "purpose": "创建邮件渠道发送器(配置 EMAIL_HOST 用 Nodemailer SMTP,否则 Mock;HTML 模板按 type 着色)", "deps": [ "环境变量: EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS, EMAIL_FROM" ] }, { "name": "createInAppSender", "file": "channels/in-app-channel.ts", "purpose": "创建站内消息渠道发送器(调用 notifications.data-access.createNotification 写入 message_notifications 表;总是启用;P0-4 / P1-5 修复后改为静态导入本地 createNotification,不再动态 import messaging)", "deps": [ "data-access.createNotification" ] } ], "types": [ { "name": "NotificationChannel", "type": "type", "file": "types.ts", "definition": "'in_app' | 'email' | 'sms' | 'wechat'", "usedBy": [ "所有渠道文件", "dispatcher" ] }, { "name": "NotificationPayload", "type": "interface", "file": "types.ts", "definition": "{ userId, title, content, type: 'info'|'warning'|'error'|'success', metadata?, actionUrl? }", "usedBy": [ "dispatcher", "actions", "所有渠道" ] }, { "name": "ChannelSendResult", "type": "interface", "file": "types.ts", "definition": "{ channel, success, messageId?, error?, sentAt }", "usedBy": [ "dispatcher", "actions", "所有渠道" ] }, { "name": "NotificationChannelConfig", "type": "interface", "file": "types.ts", "definition": "{ enabled, sms?, wechat?, email? }", "usedBy": [ "类型定义" ] }, { "name": "NotificationChannelSender", "type": "interface", "file": "channels/types.ts", "definition": "{ channel: NotificationChannel, send(payload, recipient), sendBatch(items) }", "usedBy": [ "所有渠道实现", "dispatcher" ] }, { "name": "ChannelRecipient", "type": "interface", "file": "channels/types.ts", "definition": "{ userId, phone?, email?, wechatOpenId? }", "usedBy": [ "所有渠道", "data-access.getUserContactInfo" ] }, { "name": "NotificationType", "type": "type", "file": "types.ts", "definition": "'message' | 'announcement' | 'homework' | 'grade'", "usedBy": [ "data-access", "channels.in-app-channel", "messaging (via re-export)" ] }, { "name": "NotificationPriority", "type": "type", "file": "types.ts", "definition": "'low' | 'normal' | 'high' | 'urgent'", "usedBy": [ "data-access", "components.notification-list", "messaging (via re-export)" ] }, { "name": "Notification", "type": "interface", "file": "types.ts", "definition": "{ id, userId, type: NotificationType, title, content: string | null, link: string | null, isRead, priority: NotificationPriority, isArchived, createdAt }", "usedBy": [ "data-access.getNotifications", "messaging (via re-export)" ] }, { "name": "NotificationListItem", "type": "type", "file": "types.ts", "definition": "通知列表项类型(同 Notification)", "usedBy": [ "messaging (via re-export)" ] }, { "name": "NotificationLog", "type": "interface", "file": "types.ts", "definition": "{ id, userId, title, channel: NotificationChannel, status: 'success' | 'failure', messageId: string | null, error: string | null, sentAt }", "usedBy": [] }, { "name": "PaginatedResult", "type": "interface", "file": "types.ts", "definition": "{ items: T[], total: number, page: number, pageSize: number, totalPages: number }", "usedBy": [ "data-access.getNotifications", "messaging (via re-export)" ] }, { "name": "GetNotificationsParams", "type": "interface", "file": "types.ts", "definition": "{ page?, pageSize?, unreadOnly?, unarchivedOnly?, priority? }", "usedBy": [ "data-access.getNotifications", "messaging (via re-export)" ] }, { "name": "CreateNotificationInput", "type": "interface", "file": "types.ts", "definition": "{ userId, type: NotificationType, title, content?, link?, priority? }", "usedBy": [ "data-access.createNotification", "channels.in-app-channel", "messaging (via re-export)" ] }, { "name": "NotificationPreferences", "type": "interface", "file": "types.ts", "definition": "{ id, userId, emailEnabled, smsEnabled, pushEnabled, homeworkNotifications, gradeNotifications, announcementNotifications, messageNotifications, attendanceNotifications, createdAt, updatedAt }", "usedBy": [ "preferences.getNotificationPreferences", "preferences.upsertNotificationPreferences", "dispatcher.sendNotification", "messaging (via re-export)" ] }, { "name": "UpdateNotificationPreferencesInput", "type": "interface", "file": "types.ts", "definition": "{ emailEnabled?, smsEnabled?, pushEnabled?, homeworkNotifications?, gradeNotifications?, announcementNotifications?, messageNotifications?, attendanceNotifications? }", "usedBy": [ "preferences.upsertNotificationPreferences", "messaging (via re-export)" ] } ], "components": [ { "name": "NotificationList", "file": "components/notification-list.tsx", "purpose": "P1-4 新增(从 messaging 迁移):通知完整列表(全部标记已读、单条标记已读、查看链接);V2-P0-1 优化:useTranslations 命名空间从 'messages' 切换到独立的 'notifications';V2-P2-13b:支持优先级 Badge 显示和归档操作;V2-P2-13c:支持按类型筛选(all/message/announcement/homework/grade)+ 空状态区分'无通知'/'无筛选结果'" }, { "name": "NotificationDropdown", "file": "components/notification-dropdown.tsx", "purpose": "P1-4 新增(从 messaging 迁移):SiteHeader 通知下拉菜单(Bell 图标 + 未读数 Badge,每 30 秒轮询,滚动列表,标记已读,查看全部链接);V2-P0-1 优化:useTranslations 命名空间从 'messages' 切换到独立的 'notifications';V2-P2-1 优化:轮询间隔提取为 POLL_INTERVAL_MS 常量;V2-P3:改用 SSE 实时推送 + 轮询降级;V2-P2-13c:集成 useDesktopNotifications 桌面推送,监听新通知并触发桌面提醒(首次加载不批量推送)" } ], "hooks": [ { "name": "useNotificationStream", "file": "hooks/use-notification-stream.ts", "purpose": "V2-P3 新增:SSE 实时推送 Hook(EventSource + 轮询降级),管理 SSE 连接生命周期,降级时通过 pollFnRef 调用 Server Actions 轮询", "usedBy": [ "components/notification-dropdown.tsx" ] }, { "name": "useDesktopNotifications", "file": "hooks/use-desktop-notifications.ts", "purpose": "V2-P2-13c 新增:浏览器桌面推送 Hook(Notification API),支持权限管理、自动请求权限、新通知桌面推送、点击跳转回调;SSR 安全(typeof window !== 'undefined' 检查)", "usedBy": [ "components/notification-dropdown.tsx" ] } ] } }, "attendance": { "path": "src/modules/attendance", "description": "学生考勤管理:教师按班级/日期点名(单条/批量)、查询考勤记录、统计出勤率/迟到率,学生/家长查看本人/子女考勤汇总,管理员查看全校考勤记录。支持班级考勤规则配置。✅ 审计重构后:5 个写 Server Action(含归属校验+trackEvent)/ data-access 移除跨模块 JOIN 委托 users/classes data-access / SQL 聚合统计 / 服务接口抽象(AttendanceDataService/Read/Write/Repository)+ Context 注入 + 工厂函数 / 状态常量下沉至 shared/constants/attendance-status / stats 拆分为 data-access-stats.ts(含纯函数 computeStats)。 L-6 节次考勤:period 字段支持 full_day/morning_reading/morning/afternoon/evening,向后兼容. L-9 考勤与成绩关联分析:data-access-correlation.ts 跨模块委托 grades data-access,correlation-compute.ts 纯函数计算 Pearson 相关系数与风险分级.", "exports": { "actions": [ { "name": "recordAttendanceAction", "permission": "ATTENDANCE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建单条考勤记录(Zod 校验 + trackEvent 埋点 attendance.recorded)", "deps": [ "requirePermission", "data-access.createAttendanceRecord", "revalidatePath", "trackEvent" ], "usedBy": [ "attendance-record-list.tsx" ] }, { "name": "batchRecordAttendanceAction", "permission": "ATTENDANCE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "批量点名(班级+日期,表格形式录入每个学生状态,trackEvent 埋点 attendance.batch_recorded);L-2 起在记录保存成功后调用 notifications.notifyParentsOfAbsence 异步通知家长(失败不阻断主流程)", "deps": [ "requirePermission", "data-access.batchCreateAttendanceRecords", "notifications.notifyParentsOfAbsence", "parent/data-access.getParentStudentMapByStudentIds", "notifications.dispatcher.sendBatchNotifications", "revalidatePath", "trackEvent" ], "usedBy": [ "attendance-sheet.tsx" ] }, { "name": "updateAttendanceAction", "permission": "ATTENDANCE_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新考勤记录(状态、备注;含资源归属校验:assertRecordOwnership 委托 classes/data-access.verifyTeacherOwnsClass;trackEvent 埋点 attendance.updated)", "deps": [ "requirePermission", "data-access.getAttendanceRecordClassId", "data-access.updateAttendanceRecord", "classes/data-access.verifyTeacherOwnsClass", "revalidatePath", "trackEvent" ], "usedBy": [ "attendance-record-list.tsx" ] }, { "name": "deleteAttendanceAction", "permission": "ATTENDANCE_MANAGE", "signature": "(id: string) => Promise>", "purpose": "删除考勤记录(含资源归属校验:assertRecordOwnership 委托 classes/data-access.verifyTeacherOwnsClass)", "deps": [ "requirePermission", "data-access.getAttendanceRecordClassId", "data-access.deleteAttendanceRecord", "classes/data-access.verifyTeacherOwnsClass", "revalidatePath", "trackEvent" ], "usedBy": [ "attendance-record-list.tsx" ] }, { "name": "saveAttendanceRulesAction", "permission": "ATTENDANCE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "保存班级考勤规则(upsert;含资源归属校验:assertClassOwnership 委托 classes/data-access.verifyTeacherOwnsClass;trackEvent 埋点 attendance.rules_saved)", "deps": [ "requirePermission", "data-access.upsertAttendanceRules", "classes/data-access.verifyTeacherOwnsClass", "revalidatePath", "trackEvent" ], "usedBy": [ "attendance-rules-form.tsx" ] } ], "dataAccess": [ { "name": "getAttendanceRecords", "signature": "(params: AttendanceQueryParams & { scope: DataScope; currentUserId?: string }) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "users/data-access.getUserNamesByIds", "classes/data-access.getClassNamesByIds", "types.DataScope", "./buildPeriodFilter (L-6)" ], "usedBy": [ "services/attendance-data-service.AttendanceReadServiceImpl.queryRecords", "admin/attendance/page.tsx", "teacher/attendance/page.tsx" ], "auditNotes": [ "✅ 审计修复 P0-5:移除跨模块 JOIN users/classes 表,改为委托 users/classes data-access 批量取名称", "✅ L-6:支持按 period 过滤(full_day 匹配 NULL OR 'full_day')" ] }, { "name": "getClassAttendanceForDate", "signature": "(classId: string, date: string, period?: AttendancePeriod) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "users/data-access.getUserNamesByIds", "classes/data-access.getClassNamesByIds" ], "usedBy": [ "services/attendance-data-service.AttendanceReadServiceImpl.getClassRecordsForDate", "teacher/attendance/sheet/page.tsx" ], "auditNotes": [ "✅ 审计修复 P0-5:移除跨模块 JOIN,委托 data-access 取名称", "✅ L-6:新增可选 period 参数支持节次过滤" ] }, { "name": "buildPeriodFilter", "signature": "(period?: AttendancePeriod) => SQL | null", "file": "data-access.ts", "purpose": "L-6 节次过滤构建器:full_day 匹配 NULL OR 'full_day'(向后兼容),其他值精确匹配;未传 period 返回 null(不过滤)", "deps": [ "drizzle-orm (eq, or, isNull, SQL)", "shared.db.schema.attendanceRecords" ], "usedBy": [ "data-access.getAttendanceRecords" ], "auditNotes": [ "✅ L-6 新增:纯函数封装节次过滤条件,统一向后兼容策略" ] }, { "name": "createAttendanceRecord", "signature": "(data: RecordAttendanceInput, recordedBy: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "@paralleldrive/cuid2" ], "usedBy": [ "recordAttendanceAction", "services/attendance-data-service.AttendanceWriteServiceImpl.recordAttendance" ], "auditNotes": [ "✅ L-6:写入 period 字段(默认 full_day)" ] }, { "name": "batchCreateAttendanceRecords", "signature": "(data: BatchRecordAttendanceInput, recordedBy: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "@paralleldrive/cuid2" ], "usedBy": [ "batchRecordAttendanceAction", "services/attendance-data-service.AttendanceWriteServiceImpl.batchRecordAttendance" ], "auditNotes": [ "✅ L-6:批量写入 period 字段" ] }, { "name": "updateAttendanceRecord", "signature": "(id: string, data: UpdateAttendanceInput) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords" ], "usedBy": [ "updateAttendanceAction", "services/attendance-data-service.AttendanceWriteServiceImpl.updateAttendance" ], "auditNotes": [ "✅ L-6:支持更新 period 字段" ] }, { "name": "deleteAttendanceRecord", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords" ], "usedBy": [ "deleteAttendanceAction", "services/attendance-data-service.AttendanceWriteServiceImpl.deleteAttendance" ] }, { "name": "getAttendanceRecordClassId", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords" ], "usedBy": [ "actions.assertRecordOwnership", "services/attendance-data-service (re-export)" ], "auditNotes": [ "✅ 审计新增:归属校验所需,暴露给 actions 与 services 层" ] }, { "name": "getClassStudentsForAttendance", "signature": "(classId: string) => Promise>", "file": "data-access.ts", "deps": [ "classes/data-access.getClassActiveStudentsWithInfo" ], "usedBy": [ "attendance-sheet.tsx", "teacher/attendance/sheet/page.tsx" ], "auditNotes": [ "✅ 审计修复 P0-5:跨模块直查 classEnrollments 改为委托 classes/data-access.getClassActiveStudentsWithInfo" ] }, { "name": "getAttendanceRules", "signature": "(classId?: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRules" ], "usedBy": [ "services/attendance-data-service.AttendanceReadServiceImpl.getRules", "attendance-rules-form.tsx (via service)" ] }, { "name": "upsertAttendanceRules", "signature": "(data: AttendanceRuleInput) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRules", "@paralleldrive/cuid2" ], "usedBy": [ "saveAttendanceRulesAction", "services/attendance-data-service.AttendanceWriteServiceImpl.saveRules" ] }, { "name": "syncExcusedFromLeaveRequest", "signature": "(studentId: string, classId: string, startDate: string, endDate: string) => Promise", "file": "data-access.ts", "purpose": "L-5 在线请假流程:审批通过后遍历日期区间,对没有考勤记录的日期插入 excused 记录(幂等,不覆盖已有记录);返回插入条数", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "@paralleldrive/cuid2" ], "usedBy": [ "leave-requests/actions.reviewLeaveRequestAction" ], "auditNotes": [ "✅ L-5 新增:跨模块联动(请假审批 → 考勤同步);幂等设计,仅插入不存在的 excused 记录,避免重复同步" ] }, { "name": "getStudentAttendanceSummary", "signature": "(studentId: string, startDate?: string, endDate?: string, recentLimit?: number) => Promise", "file": "data-access-stats.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "classes/data-access.getClassNameById", "users/data-access.getUserNamesByIds", "./data-access.getAttendanceRecords", "./computeStats" ], "usedBy": [ "services/attendance-data-service.AttendanceReadServiceImpl.getStudentSummary", "services/attendance-data-service.AttendanceReadServiceImpl.getRecentRecords", "student/attendance/page.tsx", "parent/attendance/page.tsx (via service)" ], "auditNotes": [ "✅ 审计修复 P1-11:SQL 聚合替代内存聚合;✅ P0-5:移除 JOIN,委托 data-access 取名称;✅ 新增 recentLimit 参数" ] }, { "name": "getClassAttendanceStats", "signature": "(classId: string, startDate?: string, endDate?: string) => Promise", "file": "data-access-stats.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "classes/data-access.getClassNameById", "users/data-access.getUserNamesByIds", "./data-access.getAttendanceRecords", "./computeStats" ], "usedBy": [ "services/attendance-data-service.AttendanceReadServiceImpl.getClassStats", "teacher/attendance/stats/page.tsx (via service)" ], "auditNotes": [ "✅ 审计修复 P1-11:SQL 聚合替代内存聚合;✅ P0-5:移除 JOIN,委托 data-access 取名称" ] }, { "name": "computeStats", "signature": "(rows: { status: string }[]) => AttendanceStats", "file": "data-access-stats.ts", "deps": [], "usedBy": [ "data-access-stats.getStudentAttendanceSummary", "data-access-stats.getClassAttendanceStats" ], "auditNotes": [ "✅ 审计新增 P2-6:纯函数导出,便于单测,与 IO 解耦" ] }, { "name": "getAttendanceStats", "signature": "(params: { scope: DataScope; currentUserId: string; classId?: string; date?: string }) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "types.DataScope" ], "usedBy": [ "services/attendance-data-service.AttendanceReadServiceImpl.getOverviewStats", "admin/attendance/page.tsx (via service)" ], "auditNotes": [ "✅ 审计修复 P0:SQL 聚合(COUNT + SUM(CASE WHEN))替代分页截断聚合,统计不再失真", "✅ 审计修复 P2-5:返回类型统一为 AttendanceStats,删除 AttendanceOverviewStats 类型" ] }, { "name": "getAttendanceGradeCorrelation", "signature": "(classId: string, startDate?: string, endDate?: string, scope?: DataScope) => Promise", "file": "data-access-correlation.ts", "purpose": "L-9 考勤与成绩关联分析:跨模块聚合(attendance SQL 聚合 + grades data-access 委托查询),调用纯函数 computeCorrelationSummary 计算相关系数与风险分级", "deps": [ "shared.db", "shared.db.schema.attendanceRecords", "classes/data-access.getClassActiveStudentsWithInfo", "classes/data-access.getClassNameById", "grades/data-access.getGradeRecords", "./correlation-compute.computeCorrelationSummary", "types.DataScope" ], "usedBy": [ "admin/attendance/page.tsx" ], "auditNotes": [ "✅ L-9 新增:跨模块关联分析(attendance + grades),通过对方 data-access 通信不直查对方 DB 表", "✅ Pearson 相关系数计算与风险分级委托纯函数(IO 解耦,便于单测)", "✅ scope 权限校验:class_taught 仅查自己班级,owned(学生)拒绝访问班级级分析" ] }, { "name": "canAccessCorrelationAnalysis", "signature": "(scope: DataScope, classId: string) => boolean", "file": "data-access-correlation.ts", "purpose": "L-9:检查 scope 是否允许访问班级级关联分析(仅 all / class_taught 含该班级允许)", "deps": [ "types.DataScope" ], "usedBy": [ "data-access-correlation.getAttendanceGradeCorrelation" ] }, { "name": "computePearsonCorrelation", "signature": "(x: readonly number[], y: readonly number[]) => number | null", "file": "correlation-compute.ts", "purpose": "L-9 纯函数:计算 Pearson 相关系数 r ∈ [-1, +1],数据不足或方差为 0 返回 null", "deps": [], "usedBy": [ "correlation-compute.computeCorrelationSummary" ], "auditNotes": [ "✅ L-9 新增:纯函数,与 IO 解耦,便于单测" ] }, { "name": "classifyRiskLevel", "signature": "(attendanceRate: number, averageScore: number) => AttendanceGradeRiskLevel", "file": "correlation-compute.ts", "purpose": "L-9 纯函数:根据出勤率与成绩判定风险等级(high <80% & <60 / medium <90% & <75 / low 其余)", "deps": [ "types.AttendanceGradeRiskLevel" ], "usedBy": [ "correlation-compute.computeCorrelationSummary" ] }, { "name": "computeCorrelationSummary", "signature": "(classId, className, startDate, endDate, rawItems) => AttendanceGradeCorrelationSummary", "file": "correlation-compute.ts", "purpose": "L-9 纯函数:聚合班级考勤-成绩关联汇总(Pearson 相关系数 + 风险分级 + 排序)", "deps": [ "./computePearsonCorrelation", "./classifyRiskLevel", "./interpretCorrelation", "types.AttendanceGradeCorrelationItem", "types.AttendanceGradeCorrelationSummary" ], "usedBy": [ "data-access-correlation.getAttendanceGradeCorrelation" ], "auditNotes": [ "✅ L-9 新增:纯函数聚合,排除无考勤或无成绩记录的学生" ] }, { "name": "notifyParentsOfAbsence", "signature": "(classId: string, records: AttendanceNotificationInput[]) => Promise<{ sent: number; failed: number; skipped: number }>", "file": "notifications.ts", "deps": [ "notifications.dispatcher.sendBatchNotifications", "parent/data-access.getParentStudentMapByStudentIds", "users/data-access.getUserNamesByIds", "classes/data-access.getClassNameById" ], "usedBy": [ "actions.batchRecordAttendanceAction" ], "auditNotes": [ "✅ L-2:批量考勤录入后异步通知家长(in_app/sms/email/wechat 多渠道),通知失败不阻断主流程", "✅ 一次性批量查询 parent-student 映射,避免 N+1" ] } ], "schemas": [ { "name": "AttendanceStatusEnum", "type": "zod", "file": "schema.ts", "definition": "enum('present','absent','late','early_leave','excused')", "usedBy": [ "RecordAttendanceSchema", "BatchRecordAttendanceSchema", "UpdateAttendanceSchema" ] }, { "name": "AttendancePeriodEnum", "type": "zod", "file": "schema.ts", "definition": "L-6 节次考勤:enum('morning_reading','morning','afternoon','evening','full_day')", "usedBy": [ "RecordAttendanceSchema", "UpdateAttendanceSchema" ], "auditNotes": [ "✅ L-6 新增:节次校验枚举,与 DB 层 attendancePeriodEnum 对齐" ] }, { "name": "RecordAttendanceSchema", "type": "zod", "file": "schema.ts", "definition": "{ studentId, classId, scheduleId?, date, status, period?(L-6, 默认 full_day), remark? }", "usedBy": [ "recordAttendanceAction" ] }, { "name": "BatchRecordAttendanceSchema", "type": "zod", "file": "schema.ts", "definition": "{ records: [{ studentId, classId, scheduleId?, date, status, remark? }] }", "usedBy": [ "batchRecordAttendanceAction" ] }, { "name": "UpdateAttendanceSchema", "type": "zod", "file": "schema.ts", "definition": "{ status?, remark?, scheduleId?, period?(L-6) }", "usedBy": [ "updateAttendanceAction" ] }, { "name": "AttendanceRuleSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, lateThresholdMinutes?, earlyLeaveThresholdMinutes?, enableAutoMark? }", "usedBy": [ "saveAttendanceRulesAction" ] } ], "types": [ { "name": "AttendanceStatus", "type": "type", "file": "shared/constants/attendance-status.ts (✅ 审计 P1-3:从 attendance/types.ts 下沉至 shared)", "definition": "'present' | 'absent' | 'late' | 'early_leave' | 'excused'", "usedBy": [ "attendance/data-access", "attendance/components", "attendance/services/types", "parent/components (via shared)", "attendance/constants (re-export)" ] }, { "name": "AttendancePeriod", "type": "type", "file": "types.ts", "definition": "L-6 节次考勤:'morning_reading' | 'morning' | 'afternoon' | 'evening' | 'full_day'", "usedBy": [ "attendance/data-access", "attendance/components (attendance-sheet, attendance-record-list)", "attendance/types (AttendanceRecord, AttendanceListItem, AttendanceQueryParams)" ], "auditNotes": [ "✅ L-6 新增:DB 层 period 字段 nullable,对外暴露统一为 'full_day'(mapListItem 中 null → full_day)" ] }, { "name": "AttendanceRecord", "type": "type", "file": "types.ts", "definition": "考勤记录完整类型(含 L-6 period: AttendancePeriod | null)", "usedBy": [ "attendance/data-access" ] }, { "name": "AttendanceListItem", "type": "type", "file": "types.ts", "definition": "{ id, studentId, studentName, classId, className, scheduleId, date, status, period(L-6), remark, recordedBy, recorderName, createdAt }", "usedBy": [ "attendance/components", "attendance/services/types", "页面" ] }, { "name": "AttendanceStats", "type": "type", "file": "types.ts", "definition": "{ total, present, absent, late, earlyLeave, excused, presentRate, lateRate }", "usedBy": [ "attendance-stats-card.tsx", "attendance-stats-cards.tsx", "attendance/services/types", "attendance/export", "data-access-stats.computeStats", "data-access.getAttendanceStats" ], "auditNotes": [ "✅ 审计修复 P2-5:消除 AttendanceOverviewStats 分裂类型,全模块统一 AttendanceStats" ] }, { "name": "StudentAttendanceSummary", "type": "type", "file": "types.ts", "definition": "{ studentId, studentName, stats: AttendanceStats, recentRecords: AttendanceListItem[] }", "usedBy": [ "student-attendance-view.tsx", "attendance/services/types", "parent/attendance/page.tsx (via service)" ] }, { "name": "ClassAttendanceSummary", "type": "type", "file": "types.ts", "definition": "{ classId, className, date, stats: AttendanceStats, studentRecords: AttendanceListItem[] }", "usedBy": [ "teacher/attendance/stats", "attendance/services/types" ] }, { "name": "AttendanceRule", "type": "type", "file": "types.ts", "definition": "{ id, classId, lateThresholdMinutes, earlyLeaveThresholdMinutes, enableAutoMark, createdAt, updatedAt }", "usedBy": [ "attendance-rules-form.tsx", "attendance/services/types" ] }, { "name": "AttendanceQueryParams", "type": "type", "file": "types.ts", "definition": "{ classId?, studentId?, date?, startDate?, endDate?, status?, period?(L-6), page?, pageSize? }", "usedBy": [ "data-access.getAttendanceRecords", "attendance/services/types" ] }, { "name": "AttendanceGradeRiskLevel", "type": "type", "file": "types.ts", "definition": "L-9 考勤与成绩关联分析:风险等级(high | medium | low)", "usedBy": [ "attendance/types.AttendanceGradeCorrelationItem", "attendance/components/attendance-grade-correlation-card" ], "auditNotes": [ "✅ L-9 新增:风险等级类型" ] }, { "name": "AttendanceGradeCorrelationItem", "type": "type", "file": "types.ts", "definition": "L-9:单个学生关联数据点 { studentId, studentName, attendanceRate, attendanceRecordCount, absentCount, averageScore, gradeRecordCount, riskLevel }", "usedBy": [ "attendance/components/attendance-grade-correlation-card", "attendance/types.AttendanceGradeCorrelationSummary" ] }, { "name": "AttendanceGradeCorrelationSummary", "type": "type", "file": "types.ts", "definition": "L-9:班级关联汇总 { classId, className, startDate, endDate, items[], correlation, correlationInterpretation, riskCounts }", "usedBy": [ "attendance/components/attendance-grade-correlation-card", "admin/attendance/page.tsx" ] }, { "name": "PaginatedAttendanceResult", "type": "type", "file": "types.ts", "definition": "{ items: AttendanceListItem[], total, page, pageSize, totalPages }", "usedBy": [ "data-access.getAttendanceRecords", "attendance/services/types" ] } ], "components": [ { "name": "AttendanceSheet", "file": "components/attendance-sheet.tsx", "purpose": "批量点名表单(班级/日期/节次选择器 + 学生表格 + 每行状态 Select + 全部标记到场按钮);L-6 起新增节次选择器(默认 full_day),提交时携带 period" }, { "name": "AttendanceRecordList", "file": "components/attendance-record-list.tsx", "purpose": "考勤记录列表表格(含删除确认对话框);L-6 起新增节次列(null 视为 full_day 展示)" }, { "name": "AttendanceGradeCorrelationCard", "file": "components/attendance-grade-correlation-card.tsx", "purpose": "L-9 考勤与成绩关联分析卡片(散点图 + 风险分级表格 + Pearson 相关系数汇总),use client,recharts ScatterChart" }, { "name": "AttendanceStatsCard", "file": "components/attendance-stats-card.tsx", "purpose": "统计卡片(总数、到场、缺勤、迟到、早退、请假、出勤率、迟到率)" }, { "name": "AttendanceStatsCards", "file": "components/attendance-stats-cards.tsx", "purpose": "管理员考勤总览页统计概览卡片组(总记录数、出勤、缺勤、迟到、早退、出勤率,6 卡片网格布局)" }, { "name": "AttendanceFilters", "file": "components/attendance-filters.tsx", "purpose": "URL 同步筛选器(班级、状态、日期)" }, { "name": "StudentAttendanceView", "file": "components/student-attendance-view.tsx", "purpose": "学生/家长视图(统计卡片 + 最近记录表格)" }, { "name": "AttendanceRulesForm", "file": "components/attendance-rules-form.tsx", "purpose": "考勤规则配置表单(班级选择器、迟到/早退阈值、自动标记勾选)" }, { "name": "AttendanceStatsClassSelector", "file": "components/attendance-stats-class-selector.tsx", "purpose": "考勤统计页班级筛选器(Link 筛选按钮组,含 focus-visible 焦点样式)", "deps": [ "next/link", "shared/lib/utils.cn" ], "usedBy": [ "teacher/attendance/stats/page.tsx" ] }, { "name": "AttendancePageLayout", "file": "components/attendance-page-layout.tsx", "purpose": "考勤页面布局骨架(P2-3 新增:抽取 admin/teacher 考勤页重复的 header/stats/filters/children 布局结构,统一 spacing 与响应式)", "deps": [ "shared/lib/utils.cn" ], "usedBy": [ "admin/attendance/page.tsx", "teacher/attendance/page.tsx" ] } ], "importExport": [ { "name": "exportAttendanceRecordsToExcel", "signature": "(params: { scope: DataScope; currentUserId?: string; classId?: string; status?: string; date?: string }) => Promise", "file": "export.ts", "purpose": "导出考勤记录到 Excel(Sheet1 考勤明细—学生/班级/日期/状态/备注/记录人/创建时间;Sheet2 统计汇总—总记录数/到场/缺勤/迟到/早退/请假/出勤率;列头使用 next-intl getTranslations 国际化)", "deps": [ "shared.lib.excel.exportToExcel", "data-access.getAttendanceRecords", "data-access.getAttendanceStats", "next-intl/server.getTranslations", "types.AttendanceStats", "types.AttendanceListItem" ], "usedBy": [ "app/api/export/route.ts" ], "auditNotes": [ "✅ 审计修复 P1-6:类型守卫 isAttendanceStatus 替代 as 断言", "✅ 审计修复 P2-5:statsRows 字段名同步为 AttendanceStats(total/present/absent/late/earlyLeave/excused/presentRate)" ] } ], "services": [ { "name": "AttendanceReadService", "type": "interface", "file": "services/types.ts", "purpose": "考勤数据读取服务接口(查询契约):7 个读方法(getStudentSummary / getRecentRecords / getClassRecordsForDate / getClassStats / queryRecords / getOverviewStats / getRules)", "deps": [ "attendance/types" ], "usedBy": [ "parent/attendance/page.tsx", "student/attendance/page.tsx", "teacher/attendance/stats/page.tsx" ], "auditNotes": [ "✅ 审计新增 P1-12:消费方依赖接口而非具体 data-access 实现" ] }, { "name": "AttendanceWriteService", "type": "interface", "file": "services/types.ts", "purpose": "考勤数据写入服务接口(命令契约):5 个写方法(recordAttendance / batchRecordAttendance / updateAttendance / deleteAttendance / saveRules);仅教师/管理员实现", "deps": [ "attendance/schema" ], "auditNotes": [ "✅ 审计新增 P1-12:写操作接口契约,编译期阻止学生/家长误用" ] }, { "name": "AttendanceDataService", "type": "interface", "file": "services/types.ts", "purpose": "完整的考勤数据服务接口(AttendanceReadService + AttendanceWriteService);教师与管理员角色使用", "deps": [ "AttendanceReadService", "AttendanceWriteService" ], "usedBy": [ "services/attendance-context.AttendanceProvider" ] }, { "name": "AttendanceRepository", "type": "interface", "file": "services/types.ts", "purpose": "考勤 Repository 接口(底层 CRUD 契约,仅供 data-access 层实现与测试 mock 使用)", "deps": [ "attendance/types", "attendance/schema" ], "auditNotes": [ "✅ 审计新增 P2-6:底层 CRUD 契约抽象,便于 mock 测试" ] }, { "name": "AttendanceStatsCalculator", "type": "interface", "file": "services/types.ts", "purpose": "考勤纯计算服务接口(computeStats):纯逻辑与 UI 分离,便于单测", "deps": [ "attendance/types.AttendanceStats" ], "usedBy": [ "data-access-stats.computeStats (implements)" ] }, { "name": "DateRange", "type": "type", "file": "services/types.ts", "purpose": "日期范围参数类型 { startDate?, endDate? }", "deps": [] }, { "name": "AttendanceProvider", "type": "component", "file": "services/attendance-context.tsx", "purpose": "考勤服务 Provider(React Context 注入服务实现,客户端组件通过 useAttendanceService 消费)", "deps": [ "react.createContext", "AttendanceDataService" ], "usedBy": [ "client components (待接入:当前 Server Component 直接以接口类型消费 createAttendanceDataService)" ], "auditNotes": [ "✅ 审计新增 P1-12:Context 注入机制;注意 Server Component 无法使用 Context,应直接以接口类型消费工厂函数" ] }, { "name": "useAttendanceService", "type": "hook", "file": "services/attendance-context.tsx", "purpose": "读取注入的考勤数据服务(未在 Provider 内调用时抛出明确错误)", "deps": [ "react.useContext" ], "usedBy": [] }, { "name": "createAttendanceDataService", "type": "factory", "file": "services/attendance-data-service.ts", "purpose": "获取完整考勤数据服务实例(读+写);Server Component 调用此工厂以接口类型 AttendanceDataService 消费,避免直接 import data-access", "deps": [ "data-access.getAttendanceRecords", "data-access.getClassAttendanceForDate", "data-access.createAttendanceRecord", "data-access.batchCreateAttendanceRecords", "data-access.updateAttendanceRecord", "data-access.deleteAttendanceRecord", "data-access.getAttendanceRules", "data-access.upsertAttendanceRules", "data-access.getAttendanceStats", "data-access-stats.getStudentAttendanceSummary", "data-access-stats.getClassAttendanceStats" ], "usedBy": [ "admin/attendance/page.tsx", "teacher/attendance/page.tsx", "teacher/attendance/stats/page.tsx", "teacher/attendance/sheet/page.tsx" ], "auditNotes": [ "✅ 审计新增 P1-12:Server Component 工厂函数,单例无状态可安全共享" ] }, { "name": "createAttendanceReadService", "type": "factory", "file": "services/attendance-data-service.ts", "purpose": "获取只读考勤数据服务(学生/家长角色使用);返回的接口不包含写操作,编译期即阻止误用", "deps": [ "createAttendanceDataService (sharedService)" ], "usedBy": [ "student/attendance/page.tsx", "parent/attendance/page.tsx" ], "auditNotes": [ "✅ 审计新增 P1-12:学生/家长只读工厂,编译期阻止写操作误用" ] } ] } }, "leave-requests": { "path": "src/modules/leave-requests", "description": "L-5 在线请假流程:学生/家长代子女提交请假申请(sick/personal/family/other),教师/年级主任/教务主任审批本班/本年级请假申请,审批通过后自动同步 excused 考勤记录(幂等),申请人可在 pending 状态下撤销。状态机:pending → approved/rejected;pending/approved → cancelled。dataScope 行级过滤(all/class_taught/children/class_members/owned)。日期重叠校验防止重复请假。", "exports": { "actions": [ { "name": "createLeaveRequestAction", "permission": "LEAVE_REQUEST_CREATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建请假申请(校验 requester-student 关系:家长代子女/学生本人;校验 classId 确为学生当前班级;校验日期无重叠;trackEvent 埋点 leave_request.created)", "deps": [ "requirePermission", "getAuthContext", "data-access.createLeaveRequest", "data-access.hasOverlappingLeave", "parent/data-access.getParentStudentMapByStudentIds", "classes/data-access.getStudentActiveClass", "shared.lib.track-event.trackEvent", "revalidatePath" ], "usedBy": [ "leave-request-form.tsx" ] }, { "name": "reviewLeaveRequestAction", "permission": "LEAVE_REQUEST_REVIEW", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "审批请假申请(pending → approved/rejected;拒绝需 reviewComment;审批通过后调用 attendance/data-access.syncExcusedFromLeaveRequest 同步考勤,try/catch 不阻断;trackEvent 埋点 leave_request.reviewed)", "deps": [ "requirePermission", "getAuthContext", "data-access.reviewLeaveRequest", "data-access.markAttendanceSynced", "attendance/data-access.syncExcusedFromLeaveRequest", "shared.lib.track-event.trackEvent", "revalidatePath" ], "usedBy": [ "leave-review-dialog.tsx" ] }, { "name": "cancelLeaveRequestAction", "permission": "LEAVE_REQUEST_CREATE", "signature": "(id: string) => Promise>", "purpose": "撤销请假申请(仅 requester 本人的 pending 状态可撤销;trackEvent 埋点 leave_request.cancelled)", "deps": [ "requirePermission", "getAuthContext", "data-access.cancelLeaveRequest", "shared.lib.track-event.trackEvent", "revalidatePath" ], "usedBy": [ "leave-request-list.tsx" ] } ], "dataAccess": [ { "name": "getLeaveRequests", "signature": "(params: LeaveQueryParams & { scope: DataScope; currentUserId?: string }) => Promise", "file": "data-access.ts", "purpose": "分页查询请假列表(按 dataScope 过滤;class_members 由 currentUserId 过滤本人提交;含学生/班级/提交人/审批人名称批量查询)", "deps": [ "shared.db", "shared.db.schema.leaveRequests", "users/data-access.getUserNamesByIds", "classes/data-access.getClassNamesByIds", "types.DataScope" ], "usedBy": [ "parent/leave/page.tsx", "student/leave/page.tsx", "teacher/leave/page.tsx" ] }, { "name": "getLeaveRequest", "signature": "(id: string, scope: DataScope, currentUserId?: string) => Promise", "file": "data-access.ts", "purpose": "获取单条请假详情(按 dataScope 校验访问权限)", "deps": [ "shared.db", "shared.db.schema.leaveRequests" ], "usedBy": [] }, { "name": "hasOverlappingLeave", "signature": "(studentId: string, startDate: string, endDate: string, excludeId?: string) => Promise", "file": "data-access.ts", "purpose": "校验同一学生在日期范围内是否已有未结束请假(pending/approved),避免重复请假", "deps": [ "shared.db", "shared.db.schema.leaveRequests" ], "usedBy": [ "createLeaveRequestAction" ] }, { "name": "createLeaveRequest", "signature": "(data: CreateLeaveRequestInput, requesterId: string) => Promise", "file": "data-access.ts", "purpose": "创建请假申请(new Date(startDate) 转换字符串为 Date;状态默认 pending)", "deps": [ "shared.db", "shared.db.schema.leaveRequests", "@paralleldrive/cuid2" ], "usedBy": [ "createLeaveRequestAction" ] }, { "name": "reviewLeaveRequest", "signature": "(id: string, review: ReviewLeaveRequestInput, reviewerId: string) => Promise", "file": "data-access.ts", "purpose": "审批请假申请(update 后重新 select 获取更新行;Drizzle db.update 返回 ResultSetHeader 而非行数据)", "deps": [ "shared.db", "shared.db.schema.leaveRequests" ], "usedBy": [ "reviewLeaveRequestAction" ] }, { "name": "cancelLeaveRequest", "signature": "(id: string, requesterId: string) => Promise", "file": "data-access.ts", "purpose": "撤销请假(仅 requester 本人的 pending 状态可撤销)", "deps": [ "shared.db", "shared.db.schema.leaveRequests" ], "usedBy": [ "cancelLeaveRequestAction" ] }, { "name": "markAttendanceSynced", "signature": "(id: string) => Promise", "file": "data-access.ts", "purpose": "标记考勤已同步(attendanceSynced=true),防止重复触发同步", "deps": [ "shared.db", "shared.db.schema.leaveRequests" ], "usedBy": [ "reviewLeaveRequestAction" ] }, { "name": "getLeaveRequestsByIds", "signature": "(ids: string[]) => Promise", "file": "data-access.ts", "purpose": "按 ID 批量查询请假申请(按 startDate 升序)", "deps": [ "shared.db", "shared.db.schema.leaveRequests" ], "usedBy": [] }, { "name": "countPendingForClasses", "signature": "(classIds: string[]) => Promise", "file": "data-access.ts", "purpose": "教师审批待办:按 classIds 统计 pending 数量", "deps": [ "shared.db", "shared.db.schema.leaveRequests" ], "usedBy": [ "teacher/leave/page.tsx" ] } ], "components": [ { "name": "LeaveRequestForm", "type": "component", "file": "components/leave-request-form.tsx", "purpose": "请假申请表单(useActionState + useFormStatus);家长模式 childOptions 下拉选择子女,学生模式 hidden input 写入 studentId/classId", "deps": [ "actions.createLeaveRequestAction", "shared.components.ui.button/input/label/textarea/select", "next-intl.useTranslations", "sonner.toast", "react.useActionState", "react-dom.useFormStatus" ], "usedBy": [ "parent/leave/page.tsx", "student/leave/page.tsx" ], "auditNotes": [ "✅ L-5 新增:prop 名为 childOptions 而非 children,避免与 React 特殊 children prop 冲突触发 react/no-children-prop ESLint 规则" ] }, { "name": "LeaveRequestList", "type": "component", "file": "components/leave-request-list.tsx", "purpose": "只读请假申请列表展示(含状态徽章 STATUS_STYLES + STATUS_ICONS)", "deps": [ "shared.components.ui.badge/card", "types.LeaveRequestListItem", "next-intl.useTranslations" ], "usedBy": [ "parent/leave/page.tsx", "student/leave/page.tsx" ] }, { "name": "LeaveReviewDialog", "type": "component", "file": "components/leave-review-dialog.tsx", "purpose": "审批对话框(批准/拒绝按钮切换;拒绝必填 reviewComment)", "deps": [ "actions.reviewLeaveRequestAction", "shared.components.ui.dialog/button/textarea/label", "react.useActionState" ], "usedBy": [ "leave-review-list.tsx" ] }, { "name": "LeaveReviewList", "type": "component", "file": "components/leave-review-list.tsx", "purpose": "教师审批列表(pending 项显示审批按钮,打开 LeaveReviewDialog)", "deps": [ "LeaveReviewDialog", "shared.components.ui.badge/card", "types.LeaveRequestListItem" ], "usedBy": [ "teacher/leave/page.tsx" ] } ], "types": [ { "name": "LeaveType", "type": "type", "file": "types.ts", "purpose": "请假类型联合类型 'sick' | 'personal' | 'family' | 'other'" }, { "name": "LeaveStatus", "type": "type", "file": "types.ts", "purpose": "请假状态联合类型 'pending' | 'approved' | 'rejected' | 'cancelled'" }, { "name": "LeaveRequest", "type": "type", "file": "types.ts", "purpose": "请假申请详情类型(id/studentId/requesterId/classId/leaveType/startDate/endDate/reason/status/reviewerId/reviewComment/reviewedAt/attendanceSynced/createdAt/updatedAt)" }, { "name": "LeaveRequestListItem", "type": "type", "file": "types.ts", "purpose": "请假列表项类型(LeaveRequest + studentName/className/requesterName/reviewerName)" }, { "name": "LeaveQueryParams", "type": "type", "file": "types.ts", "purpose": "查询参数类型 { page?/pageSize?/classId?/studentId?/status? }" }, { "name": "PaginatedLeaveResult", "type": "type", "file": "types.ts", "purpose": "分页结果类型 { items/total/page/pageSize/totalPages }" }, { "name": "LeaveReviewInput", "type": "type", "file": "types.ts", "purpose": "审批输入类型 { id/status/reviewComment? }" } ], "schemas": [ { "name": "CreateLeaveRequestSchema", "type": "schema", "file": "schema.ts", "purpose": "创建请假 Zod 校验(含 .refine() 跨字段校验 endDate >= startDate)" }, { "name": "ReviewLeaveRequestSchema", "type": "schema", "file": "schema.ts", "purpose": "审批请假 Zod 校验(status: approved/rejected;rejected 时 reviewComment 必填)" } ] } }, "scheduling": { "path": "src/modules/scheduling", "description": "排课与调课:管理员配置班级排课规则(每日课时、连续课时、午休、上下学时间、避免背靠背、科目均衡),自动排课引擎按规则生成周课表,调课/代课申请与审批流程,课表冲突检测。", "exports": { "actions": [ { "name": "saveSchedulingRulesAction", "permission": "SCHEDULE_ADJUST", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "保存班级排课规则(upsert,classId 为空时为全局规则)", "deps": [ "requirePermission", "data-access.upsertSchedulingRules", "revalidatePath" ], "usedBy": [ "scheduling-rules-form.tsx" ] }, { "name": "autoScheduleAction", "permission": "SCHEDULE_AUTO", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "根据规则与科目分配生成预览课表(不落库)", "deps": [ "requirePermission", "data-access.getSchedulingRules", "data-access.getClassSubjectsForScheduling", "data-access.getClassroomsForScheduling", "auto-scheduler.autoSchedule", "auto-scheduler.buildDefaultTimeSlots", "revalidatePath" ], "usedBy": [ "auto-schedule-panel.tsx" ] }, { "name": "applyAutoScheduleAction", "permission": "SCHEDULE_AUTO", "signature": "(classId: string, schedules: Array<{ weekday, startTime, endTime, course, location }>) => Promise>", "purpose": "将生成的课表写入 classSchedule 表(先删除该班旧课表再插入新课表,事务)", "deps": [ "requirePermission", "shared.db", "shared.db.schema.classSchedule", "@paralleldrive/cuid2", "revalidatePath" ], "usedBy": [ "auto-schedule-panel.tsx" ] }, { "name": "requestScheduleChangeAction", "permission": "SCHEDULE_ADJUST", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "提交调课/代课申请(status=pending)", "deps": [ "requirePermission", "data-access.createScheduleChange", "revalidatePath" ], "usedBy": [ "schedule-change-form.tsx" ] }, { "name": "approveScheduleChangeAction", "permission": "SCHEDULE_AUTO", "signature": "(changeId: string) => Promise", "purpose": "审批通过调课申请(status=approved)", "deps": [ "requirePermission", "data-access.updateScheduleChangeStatus", "revalidatePath" ], "usedBy": [ "schedule-change-list.tsx" ] }, { "name": "rejectScheduleChangeAction", "permission": "SCHEDULE_AUTO", "signature": "(changeId: string, reason?: string) => Promise", "purpose": "驳回调课申请(status=rejected)", "deps": [ "requirePermission", "data-access.updateScheduleChangeStatus", "revalidatePath" ], "usedBy": [ "schedule-change-list.tsx" ] }, { "name": "getScheduleChangesAction", "permission": "SCHEDULE_ADJUST", "signature": "(params: ScheduleChangeQueryParams) => Promise>", "purpose": "查询调课申请列表(可按 classId/status/requesterId 过滤)", "deps": [ "requirePermission", "data-access.getScheduleChanges" ], "usedBy": [ "admin/scheduling/changes/page.tsx", "teacher/schedule-changes/page.tsx" ] }, { "name": "getClassConflictsAction", "permission": "SCHEDULE_ADJUST", "signature": "(classId: string) => Promise>", "purpose": "检测班级课表时间重叠冲突", "deps": [ "requirePermission", "data-access.getClassConflicts" ], "usedBy": [ "schedule-conflicts-view.tsx" ] } ], "dataAccess": [ { "name": "getSchedulingRules", "signature": "(classId?: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.schedulingRules" ], "usedBy": [ "saveSchedulingRulesAction", "autoScheduleAction", "admin/scheduling/rules/page.tsx" ] }, { "name": "upsertSchedulingRules", "signature": "(data: SchedulingRuleInput) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.schedulingRules", "@paralleldrive/cuid2" ], "usedBy": [ "saveSchedulingRulesAction" ] }, { "name": "getScheduleChanges", "signature": "(params: ScheduleChangeQueryParams) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.scheduleChanges", "shared.db.schema.classes", "shared.db.schema.users" ], "usedBy": [ "getScheduleChangesAction", "admin/scheduling/changes/page.tsx", "teacher/schedule-changes/page.tsx" ] }, { "name": "createScheduleChange", "signature": "(data: ScheduleChangeInput, requestedBy: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.scheduleChanges", "@paralleldrive/cuid2" ], "usedBy": [ "requestScheduleChangeAction" ] }, { "name": "updateScheduleChangeStatus", "signature": "(id: string, status: 'approved'|'rejected'|'completed', approverId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.scheduleChanges" ], "usedBy": [ "approveScheduleChangeAction", "rejectScheduleChangeAction" ] }, { "name": "getClassConflicts", "signature": "(classId: string) => Promise", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.classSchedule" ], "usedBy": [ "getClassConflictsAction" ] }, { "name": "getAdminClassesForScheduling", "signature": "() => Promise>", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.classes" ], "usedBy": [ "admin/scheduling/rules/page.tsx", "admin/scheduling/auto/page.tsx", "admin/scheduling/changes/page.tsx", "teacher/schedule-changes/page.tsx" ] }, { "name": "getTeachersForScheduling", "signature": "() => Promise>", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.users", "shared.db.schema.classSubjectTeachers" ], "usedBy": [ "teacher/schedule-changes/page.tsx" ] }, { "name": "getClassroomsForScheduling", "signature": "() => Promise>", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.classrooms" ], "usedBy": [ "autoScheduleAction" ] }, { "name": "getClassSubjectsForScheduling", "signature": "(classId: string) => Promise>", "file": "data-access.ts", "deps": [ "shared.db", "shared.db.schema.classSubjectTeachers", "shared.db.schema.subjects" ], "usedBy": [ "autoScheduleAction" ] }, { "name": "createClassScheduleItem", "signature": "(data: CreateClassScheduleItemInput) => Promise", "file": "data-access-class-schedule.ts", "purpose": "创建课表项(P0-5 从 classes 模块迁移,含教师班级归属校验)", "deps": [ "classes/data-access.getTeacherIdForMutations", "classes/data-access.verifyTeacherOwnsClass", "data-access.insertClassScheduleItem" ], "usedBy": [ "classes/actions.createClassScheduleItemAction" ] }, { "name": "updateClassScheduleItem", "signature": "(scheduleId: string, data: UpdateClassScheduleItemInput) => Promise", "file": "data-access-class-schedule.ts", "purpose": "更新课表项(P0-5 从 classes 模块迁移,含教师班级归属校验)", "deps": [ "classes/data-access.getTeacherIdForMutations", "classes/data-access.verifyTeacherOwnsClass", "data-access.updateClassScheduleItemById" ], "usedBy": [ "classes/actions.updateClassScheduleItemAction" ] }, { "name": "deleteClassScheduleItem", "signature": "(scheduleId: string) => Promise", "file": "data-access-class-schedule.ts", "purpose": "删除课表项(P0-5 从 classes 模块迁移,含教师班级归属校验)", "deps": [ "classes/data-access.getTeacherIdForMutations", "classes/data-access.verifyTeacherOwnsClass", "data-access.deleteClassScheduleItemById" ], "usedBy": [ "classes/actions.deleteClassScheduleItemAction" ] } ], "autoScheduler": [ { "name": "autoSchedule", "signature": "(params: AutoScheduleParams) => AutoScheduleResult", "file": "auto-scheduler.ts", "purpose": "贪心+冲突检测排课算法:按科目每周课时降序,为每节课选择第一个满足约束的时段(午休、每日窗口、班级/教师/教室冲突、每日最大课时、避免背靠背)", "deps": [ "findOptimalSlot", "validateSchedule" ], "usedBy": [ "autoScheduleAction" ] }, { "name": "findOptimalSlot", "signature": "(args) => TimeSlot | null", "file": "auto-scheduler.ts", "purpose": "在候选时段中找到第一个满足所有约束的时段", "usedBy": [ "autoSchedule" ] }, { "name": "validateSchedule", "signature": "(schedules: GeneratedSchedule[], rules: SchedulingRule) => ScheduleConflict[]", "file": "auto-scheduler.ts", "purpose": "校验生成的课表是否违反规则,返回冲突列表", "usedBy": [ "autoSchedule" ] }, { "name": "buildDefaultTimeSlots", "signature": "(morningStart, afternoonEnd, lunchBreakStart, lunchBreakEnd) => TimeSlot[]", "file": "auto-scheduler.ts", "purpose": "根据上下学时间和午休时间构建默认时段(周一至周五,上午4节+下午4节)", "usedBy": [ "autoScheduleAction" ] } ], "schemas": [ { "name": "SchedulingRuleSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, maxDailyHours?, maxContinuousHours?, lunchBreakStart?, lunchBreakEnd?, morningStart?, afternoonEnd?, avoidBackToBack?, balancedSubjects? }", "usedBy": [ "saveSchedulingRulesAction" ] }, { "name": "ScheduleChangeSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, originalScheduleId?, originalTeacherId?, substituteTeacherId?, originalDate?, newDate?, newStartTime?, newEndTime?, reason }", "usedBy": [ "requestScheduleChangeAction" ] }, { "name": "AutoScheduleParamsSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, rules: SchedulingRuleSchema, subjects: [{ subjectId, subjectName, weeklyHours, teacherId? }], teachers: [{ id, name }], classrooms: [{ id, name }], timeSlots: [{ weekday, startTime, endTime }] }", "usedBy": [ "autoScheduleAction" ] }, { "name": "ScheduleChangeStatusEnum", "type": "zod", "file": "schema.ts", "definition": "enum('pending','approved','rejected','completed')", "usedBy": [ "ScheduleChangeSchema" ] }, { "name": "ApproveScheduleChangeSchema", "type": "zod", "file": "schema.ts", "definition": "{ changeId, reason? }", "usedBy": [ "approveScheduleChangeAction" ] } ], "types": [ { "name": "ScheduleChangeStatus", "type": "type", "file": "types.ts", "definition": "'pending' | 'approved' | 'rejected' | 'completed'", "usedBy": [ "scheduling/data-access", "scheduling/components" ] }, { "name": "Weekday", "type": "type", "file": "types.ts", "definition": "1 | 2 | 3 | 4 | 5 | 6 | 7", "usedBy": [ "CreateClassScheduleItemInput", "UpdateClassScheduleItemInput" ] }, { "name": "CreateClassScheduleItemInput", "type": "type", "file": "types.ts", "definition": "{ classId, weekday: Weekday, startTime, endTime, course, location? }(P0-5 从 classes/types.ts 迁移)", "usedBy": [ "scheduling/data-access-class-schedule.createClassScheduleItem" ] }, { "name": "UpdateClassScheduleItemInput", "type": "type", "file": "types.ts", "definition": "{ classId?, weekday?: Weekday, startTime?, endTime?, course?, location? }(P0-5 从 classes/types.ts 迁移)", "usedBy": [ "scheduling/data-access-class-schedule.updateClassScheduleItem" ] }, { "name": "SchedulingRule", "type": "type", "file": "types.ts", "definition": "{ id, classId, maxDailyHours, maxContinuousHours, lunchBreakStart, lunchBreakEnd, morningStart, afternoonEnd, avoidBackToBack, balancedSubjects, createdAt, updatedAt }", "usedBy": [ "scheduling-rules-form.tsx", "auto-scheduler.ts" ] }, { "name": "ScheduleChange", "type": "type", "file": "types.ts", "definition": "调课申请完整类型", "usedBy": [ "scheduling/data-access" ] }, { "name": "ScheduleChangeListItem", "type": "type", "file": "types.ts", "definition": "{ ...ScheduleChange, className, originalTeacherName, substituteTeacherName, requesterName, approverName }", "usedBy": [ "schedule-change-list.tsx", "页面" ] }, { "name": "TimeSlot", "type": "type", "file": "types.ts", "definition": "{ weekday, startTime, endTime }", "usedBy": [ "auto-scheduler.ts" ] }, { "name": "ScheduleConflict", "type": "type", "file": "types.ts", "definition": "{ type: 'teacher_overlap'|'classroom_overlap'|'class_overlap'|'rule_violation', description, scheduleIds }", "usedBy": [ "auto-schedule-result.tsx", "schedule-conflicts-view.tsx" ] }, { "name": "AutoScheduleResult", "type": "type", "file": "types.ts", "definition": "{ success, scheduledCount, conflictCount, conflicts, schedules: GeneratedSchedule[] }", "usedBy": [ "auto-schedule-panel.tsx", "auto-schedule-result.tsx" ] }, { "name": "GeneratedSchedule", "type": "type", "file": "types.ts", "definition": "{ classId, weekday, startTime, endTime, course, location, teacherId, subjectId }", "usedBy": [ "auto-scheduler.ts", "auto-schedule-result.tsx" ] }, { "name": "AutoScheduleParams", "type": "type", "file": "types.ts", "definition": "{ classId, rules, subjects, teachers, classrooms, timeSlots }", "usedBy": [ "auto-scheduler.ts", "autoScheduleAction" ] }, { "name": "ScheduleChangeQueryParams", "type": "type", "file": "types.ts", "definition": "{ classId?, status?, requesterId? }", "usedBy": [ "getScheduleChanges", "getScheduleChangesAction" ] }, { "name": "SCHEDULE_CHANGE_STATUS_LABELS", "type": "const", "file": "types.ts", "definition": "状态英文标签常量", "usedBy": [ "schedule-change-list.tsx" ] }, { "name": "SCHEDULE_CHANGE_STATUS_COLORS", "type": "const", "file": "types.ts", "definition": "状态颜色常量(用于 Badge)", "usedBy": [ "schedule-change-list.tsx" ] } ], "components": [ { "name": "SchedulingRulesForm", "file": "components/scheduling-rules-form.tsx", "purpose": "排课规则配置表单(班级选择器、每日最大课时、连续课时、午休时间、上下学时间、避免背靠背、科目均衡)" }, { "name": "AutoSchedulePanel", "file": "components/auto-schedule-panel.tsx", "purpose": "自动排课面板(班级选择→预览→应用流程,调用 autoScheduleAction 和 applyAutoScheduleAction)" }, { "name": "AutoScheduleResultView", "file": "components/auto-schedule-result.tsx", "purpose": "排课结果预览(课表表格 + 冲突/警告列表)" }, { "name": "ScheduleChangeForm", "file": "components/schedule-change-form.tsx", "purpose": "调课/代课申请表单(班级、原任课教师、代课教师、原日期、新日期、新时间、原因)" }, { "name": "ScheduleChangeList", "file": "components/schedule-change-list.tsx", "purpose": "调课申请列表表格(含审批/驳回对话框,canApprove 控制是否显示审批按钮)" }, { "name": "ScheduleConflictsView", "file": "components/schedule-conflicts-view.tsx", "purpose": "冲突检测视图(班级选择器 + 检测按钮 + 冲突结果列表)" } ] } }, "proctoring": { "path": "src/modules/proctoring", "description": "考试监考模块:监考模式考试实时监控、防作弊事件采集、教师监考面板、学生端防作弊监控、考试模式配置", "exports": { "actions": [ { "name": "recordProctoringEventAction", "permission": "EXAM_SUBMIT", "signature": "(prevState: ActionState<{id:string}> | null, formData: FormData) => Promise>", "purpose": "学生端上报监考事件(含 submission 归属校验,刷新监考面板缓存)", "deps": [ "requirePermission(EXAM_SUBMIT)", "data-access.getExamSubmissionForProctoring", "data-access.recordProctoringEvent", "revalidatePath" ], "usedBy": [ "anti-cheat-monitor.tsx" ] }, { "name": "getProctoringDashboardAction", "permission": "EXAM_PROCTOR", "signature": "(examId: string) => Promise>", "purpose": "获取监考面板数据(摘要+学生状态+最近事件)", "deps": [ "requirePermission(EXAM_PROCTOR)", "data-access.getExamForProctoring,getExamProctoringSummary,getStudentProctoringStatuses,getRecentProctoringEvents" ], "usedBy": [ "proctoring-dashboard.tsx" ] } ], "dataAccess": [ { "name": "getExamSubmissionForProctoring", "signature": "(submissionId: string, studentId: string) => Promise<{id,examId,studentId} | null>", "purpose": "校验提交记录归属(监考事件上报前的安全校验)", "usedBy": [ "actions.recordProctoringEventAction" ] }, { "name": "recordProctoringEvent", "signature": "(input: RecordProctoringEventInput) => Promise", "purpose": "记录一条监考事件", "usedBy": [ "actions.recordProctoringEventAction", "api/proctoring/event/route.ts" ] }, { "name": "getProctoringEvents", "signature": "(examId: string, filters?: GetProctoringEventsFilters) => Promise", "purpose": "查询考试监考事件(含学生姓名、考试标题)", "usedBy": [ "待扩展" ] }, { "name": "getProctoringEventsBySubmission", "signature": "(submissionId: string) => Promise", "purpose": "查询提交的监考事件", "usedBy": [ "待扩展" ] }, { "name": "getExamProctoringSummary", "signature": "(examId: string) => Promise", "purpose": "获取考试监考摘要", "usedBy": [ "actions.getProctoringDashboardAction", "teacher/exams/[id]/proctoring/page.tsx" ] }, { "name": "getStudentProctoringStatuses", "signature": "(examId: string) => Promise", "purpose": "获取所有学生监考状态(v3 优化:Promise.all 并行执行 getUserNamesByIds 与事件聚合查询)", "usedBy": [ "actions.getProctoringDashboardAction", "teacher/exams/[id]/proctoring/page.tsx" ] }, { "name": "getExamForProctoring", "signature": "(examId: string) => Promise<{id,title,examMode,config} | null>", "purpose": "获取考试信息(含 examMode 设置)", "usedBy": [ "actions.getProctoringDashboardAction", "teacher/exams/[id]/proctoring/page.tsx" ] }, { "name": "getRecentProctoringEvents", "signature": "(examId: string, limit?: number) => Promise", "purpose": "获取最近 N 条监考事件", "usedBy": [ "actions.getProctoringDashboardAction", "teacher/exams/[id]/proctoring/page.tsx" ] } ], "types": [ { "name": "ProctoringEventType", "type": "type", "definition": "\"tab_switch\" | \"window_blur\" | \"copy_attempt\" | \"paste_attempt\" | \"right_click\" | \"devtools_open\" | \"fullscreen_exit\" | \"idle_timeout\"" }, { "name": "ExamMode", "type": "type", "definition": "\"homework\" | \"timed\" | \"proctored\"" }, { "name": "ProctoringEvent", "type": "interface", "definition": "{ id, submissionId, studentId, examId, eventType, eventDetail?, occurredAt, createdAt }" }, { "name": "ProctoringEventWithDetails", "type": "interface", "definition": "ProctoringEvent & { studentName, examTitle }" }, { "name": "ExamProctoringSummary", "type": "interface", "definition": "{ examId, examTitle, examMode, totalStudents, startedStudents, submittedStudents, totalEvents, abnormalStudents, eventsByType }" }, { "name": "StudentProctoringStatus", "type": "interface", "definition": "{ studentId, studentName, submissionId, submissionStatus, eventCount, lastEventAt, isAbnormal, eventsByType }" }, { "name": "ProctoringDashboardData", "type": "interface", "definition": "{ summary, students, recentEvents }" }, { "name": "ExamModeConfig", "type": "interface", "definition": "{ examMode, durationMinutes, shuffleQuestions, allowLateStart, lateStartGraceMinutes, antiCheatEnabled }" }, { "name": "PROCTORING_EVENT_LABELS", "type": "const", "description": "事件类型中文标签常量" }, { "name": "EXAM_MODE_LABELS", "type": "const", "description": "考试模式中文标签常量" }, { "name": "ABNORMAL_EVENT_THRESHOLD", "type": "const", "description": "异常学生事件数阈值(3)" } ], "components": [ { "name": "ProctoringDashboard", "file": "components/proctoring-dashboard.tsx", "purpose": "教师监考面板(实时学生状态、异常事件统计、异常学生高亮、10 秒轮询、usePermission 权限控制;✅ P1-3 已修复:用类型守卫函数 isProctoringEventType + toProctoringEventTypes 替代 Object.keys(...) as ProctoringEventType[] 断言)" }, { "name": "AntiCheatMonitor", "file": "components/anti-cheat-monitor.tsx", "purpose": "学生端防作弊监控(visibilitychange/blur/copy/paste/contextmenu/keydown/fullscreenchange 监听、空闲超时检测、强制全屏、警告提示、事件上报)" }, { "name": "ExamModeConfig", "file": "components/exam-mode-config.tsx", "purpose": "考试模式配置(✅ P1-2 已修复:已集成到 exam-form.tsx,useFormContext 替代 Control prop 避免 Control 不变型问题,移除全部 10 处 as 类型断言;作业/限时/监考模式选择,限时设置时长,监考设置防作弊选项)" } ] } }, "diagnostic": { "path": "src/modules/diagnostic", "description": "学情诊断报告模块:基于知识点掌握度(knowledgePointMastery)生成个人/班级诊断报告,掌握度雷达图(学生 vs 班级平均),强项/弱项分析,知识点掌握度热力图,需重点关注学生列表,报告发布/删除管理", "exports": { "dataAccess": [ { "name": "getStudentMastery", "signature": "(studentId: string) => Promise", "file": "data-access.ts", "purpose": "获取学生在所有知识点的掌握度(含知识点名称,按掌握度降序)。P3-19 修复:移除 export,改为模块内部函数", "deps": [ "shared.db", "shared.db.schema.knowledgePointMastery", "shared.db.schema.knowledgePoints" ], "usedBy": [ "data-access.getStudentMasterySummary" ] }, { "name": "getStudentMasterySummary", "signature": "(studentId: string) => Promise", "file": "data-access.ts", "purpose": "获取学生掌握度摘要(平均掌握度、强项≥80%、弱项<80%[P3-16修复]。P3-18 修复:getUserNamesByIds 与 getStudentMastery 并行查询)", "deps": [ "shared.db", "shared.db.schema.users", "data-access.getStudentMastery" ], "usedBy": [ "data-access-reports.generateDiagnosticReport", "teacher/diagnostic/student/[studentId]/page.tsx", "student/diagnostic/page.tsx" ] }, { "name": "updateMasteryFromSubmission", "signature": "(submissionId: string) => Promise", "file": "data-access.ts", "purpose": "从提交答案更新掌握度(按知识点聚合正确率,onDuplicateKeyUpdate upsert;v3 优化:Promise.all 并行执行多个知识点 upsert)", "deps": [ "shared.db", "shared.db.schema.examSubmissions", "shared.db.schema.submissionAnswers", "shared.db.schema.questionsToKnowledgePoints", "shared.db.schema.knowledgePointMastery" ], "usedBy": [ "待扩展(作业/考试提交后触发)" ] }, { "name": "getClassMasterySummary", "signature": "(classId: string) => Promise", "file": "data-access.ts", "purpose": "获取班级掌握度摘要(学生数、平均掌握度、知识点统计、需重点关注学生;v3 优化:两阶段 Promise.all 并行查询班级信息+学生 ID、用户名+掌握度)", "deps": [ "shared.db", "shared.db.schema.classes", "shared.db.schema.classEnrollments", "shared.db.schema.users", "shared.db.schema.knowledgePointMastery", "shared.db.schema.knowledgePoints" ], "usedBy": [ "data-access-reports.generateClassDiagnosticReport", "teacher/diagnostic/class/[classId]/page.tsx" ] }, { "name": "getGradeMasterySummary", "signature": "(gradeId: string) => Promise", "file": "data-access.ts", "purpose": "v4-P2-3 新增:获取年级掌握度摘要(学生数、平均掌握度、知识点统计、需重点关注学生;两阶段 Promise.all 并行查询年级名称+学生 ID、用户名+掌握度)", "deps": [ "shared.db", "shared.db.schema.knowledgePointMastery", "shared.db.schema.knowledgePoints", "users.data-access.getUserIdsByGradeId", "users.data-access.getUserNamesByIds", "school.data-access.getGradeNameById" ], "usedBy": [ "data-access-reports.generateGradeDiagnosticReport" ] }, { "name": "getKnowledgePointStats", "signature": "(classId?: string, gradeId?: string) => Promise", "file": "data-access.ts", "purpose": "获取知识点统计(按班级或年级聚合平均掌握度、掌握人数、未掌握人数)", "deps": [ "shared.db", "shared.db.schema.classEnrollments", "shared.db.schema.users", "shared.db.schema.knowledgePointMastery", "shared.db.schema.knowledgePoints" ], "usedBy": [ "teacher/diagnostic/student/[studentId]/page.tsx (班级平均对比)" ] }, { "name": "generateDiagnosticReport", "signature": "(studentId: string, period: string, generatedBy: string) => Promise", "file": "data-access-reports.ts", "purpose": "生成个人诊断报告(计算 overallScore、强项/弱项列表、复习建议,status=draft。P3-27 修复:使用 DiagnosticReportError 结构化错误码;P3-1 修复:toNumber 从 grades 模块导入)", "deps": [ "shared.db", "shared.db.schema.learningDiagnosticReports", "data-access.getStudentMasterySummary", "grades.lib.grade-utils.toNumber", "@paralleldrive/cuid2" ], "usedBy": [ "actions.generateStudentReportAction" ] }, { "name": "generateClassDiagnosticReport", "signature": "(classId: string, period: string, generatedBy: string) => Promise", "file": "data-access-reports.ts", "purpose": "生成班级诊断报告(聚合班级掌握度,识别薄弱知识点,status=draft,studentId 存生成者 ID。P3-27 修复:使用 DiagnosticReportError 结构化错误码)", "deps": [ "shared.db", "shared.db.schema.learningDiagnosticReports", "data-access.getClassMasterySummary", "@paralleldrive/cuid2" ], "usedBy": [ "actions.generateClassReportAction" ] }, { "name": "generateGradeDiagnosticReport", "signature": "(gradeId: string, period: string, generatedBy: string) => Promise", "file": "data-access-reports.ts", "purpose": "v4-P2-3 新增:生成年级诊断报告(聚合年级掌握度,识别薄弱知识点,status=draft,studentId/classId 置空,gradeId 关联年级。使用 DiagnosticReportError 结构化错误码)", "deps": [ "shared.db", "shared.db.schema.learningDiagnosticReports", "data-access.getGradeMasterySummary", "@paralleldrive/cuid2" ], "usedBy": [ "actions.generateGradeReportAction" ] }, { "name": "getDiagnosticReports", "signature": "(filters: DiagnosticReportQueryParams, scope?: DataScope) => Promise", "file": "data-access-reports.ts", "purpose": "查询诊断报告列表(可按 studentId/reportType/status/period 过滤,含学生名和生成者名。P3-15 修复:支持分页 limit/offset,返回 { reports, total } 结构,Promise.all 并行查询总数和数据)", "deps": [ "shared.db", "shared.db.schema.learningDiagnosticReports", "shared.db.schema.users", "grades.lib.grade-utils.toNumber" ], "usedBy": [ "teacher/diagnostic/page.tsx", "teacher/diagnostic/student/[studentId]/page.tsx", "student/diagnostic/page.tsx", "parent/diagnostic/page.tsx" ] }, { "name": "getDiagnosticReportById", "signature": "(id: string) => Promise", "file": "data-access-reports.ts", "purpose": "获取报告详情(含学生名和生成者名)", "deps": [ "shared.db", "shared.db.schema.learningDiagnosticReports", "shared.db.schema.users" ], "usedBy": [ "actions.getDiagnosticReportByIdAction" ] }, { "name": "publishDiagnosticReport", "signature": "(id: string) => Promise", "file": "data-access-reports.ts", "purpose": "发布诊断报告(status=published)", "deps": [ "shared.db", "shared.db.schema.learningDiagnosticReports" ], "usedBy": [ "actions.publishReportAction" ] }, { "name": "deleteDiagnosticReport", "signature": "(id: string) => Promise", "file": "data-access-reports.ts", "purpose": "删除诊断报告", "deps": [ "shared.db", "shared.db.schema.learningDiagnosticReports" ], "usedBy": [ "actions.deleteReportAction" ] } ], "actions": [ { "name": "generateStudentReportAction", "permission": "DIAGNOSTIC_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "生成学生个人诊断报告(formData: studentId, period)", "deps": [ "requirePermission", "data-access-reports.generateDiagnosticReport", "revalidatePath" ], "usedBy": [ "components/student-diagnostic-view.tsx" ] }, { "name": "generateClassReportAction", "permission": "DIAGNOSTIC_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "生成班级诊断报告(formData: classId, period)", "deps": [ "requirePermission", "data-access-reports.generateClassDiagnosticReport", "revalidatePath" ], "usedBy": [ "components/class-diagnostic-view.tsx" ] }, { "name": "publishReportAction", "permission": "DIAGNOSTIC_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "发布诊断报告(formData: id)", "deps": [ "requirePermission", "data-access-reports.publishDiagnosticReport", "revalidatePath" ], "usedBy": [ "components/report-list.tsx" ] }, { "name": "deleteReportAction", "permission": "DIAGNOSTIC_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "删除诊断报告(formData: id)", "deps": [ "requirePermission", "data-access-reports.deleteDiagnosticReport", "revalidatePath" ], "usedBy": [ "components/report-list.tsx" ] }, { "name": "getDiagnosticReportsAction", "permission": "DIAGNOSTIC_READ", "signature": "(params: DiagnosticReportQueryParams) => Promise>", "file": "actions.ts", "purpose": "查询诊断报告列表(读权限)", "deps": [ "requirePermission", "data-access-reports.getDiagnosticReports" ], "usedBy": [ "待扩展" ] }, { "name": "getDiagnosticReportByIdAction", "permission": "DIAGNOSTIC_READ", "signature": "(id: string) => Promise>", "file": "actions.ts", "purpose": "获取诊断报告详情(读权限)", "deps": [ "requirePermission", "data-access-reports.getDiagnosticReportById" ], "usedBy": [ "待扩展" ] } ], "schema": [ { "name": "GenerateStudentReportSchema", "type": "const", "file": "schema.ts", "description": "zod schema 生成学生个人诊断报告(studentId, period)", "usedBy": [ "actions.generateStudentReportAction" ] }, { "name": "GenerateClassReportSchema", "type": "const", "file": "schema.ts", "description": "zod schema 生成班级诊断报告(classId, period)", "usedBy": [ "actions.generateClassReportAction" ] }, { "name": "PublishReportSchema", "type": "const", "file": "schema.ts", "description": "zod schema 发布诊断报告(id)", "usedBy": [ "actions.publishReportAction" ] }, { "name": "DeleteReportSchema", "type": "const", "file": "schema.ts", "description": "zod schema 删除诊断报告(id)", "usedBy": [ "actions.deleteReportAction" ] }, { "name": "GetDiagnosticReportsSchema", "type": "const", "file": "schema.ts", "description": "zod schema 查询诊断报告列表(studentId?, reportType?, status?, period?)", "usedBy": [ "actions.getDiagnosticReportsAction" ] }, { "name": "GetDiagnosticReportByIdSchema", "type": "const", "file": "schema.ts", "description": "zod schema 获取诊断报告详情(id)", "usedBy": [ "actions.getDiagnosticReportByIdAction" ] } ], "types": [ { "name": "DiagnosticReportType", "type": "type", "file": "types.ts", "definition": "\"individual\" | \"class\" | \"grade\"", "usedBy": [ "types.DiagnosticReport.reportType", "actions", "components/report-list.tsx" ] }, { "name": "DiagnosticReportStatus", "type": "type", "file": "types.ts", "definition": "\"draft\" | \"published\" | \"archived\"", "usedBy": [ "types.DiagnosticReport.status", "actions", "components/report-list.tsx" ] }, { "name": "KnowledgePointMastery", "type": "interface", "file": "types.ts", "definition": "{ id, studentId, knowledgePointId, masteryLevel(0-100), totalQuestions, correctQuestions, lastAssessedAt, createdAt, updatedAt }", "usedBy": [ "data-access", "types.MasteryWithKnowledgePoint" ] }, { "name": "MasteryWithKnowledgePoint", "type": "interface", "file": "types.ts", "definition": "KnowledgePointMastery & { knowledgePointName, knowledgePointDescription }", "usedBy": [ "data-access.getStudentMastery", "types.StudentMasterySummary" ] }, { "name": "StudentMasterySummary", "type": "interface", "file": "types.ts", "definition": "{ studentId, studentName, averageMastery, totalKnowledgePoints, strengths(≥80), weaknesses(<80)[P3-16修复:消除60-79盲区], allMastery }", "usedBy": [ "data-access.getStudentMasterySummary", "data-access-reports.generateDiagnosticReport", "components/student-diagnostic-view.tsx" ] }, { "name": "DiagnosticReport", "type": "interface", "file": "types.ts", "definition": "{ id, studentId, classId(v4-P1-4 新增: 班级报告关联班级 ID,个人报告为 null), gradeId(v4-P2-3 新增: 年级报告关联年级 ID,个人/班级报告为 null), generatedBy, reportType, period, summary, strengths[], weaknesses[], recommendations[], overallScore, status, createdAt, updatedAt }", "usedBy": [ "data-access-reports", "types.DiagnosticReportWithDetails" ] }, { "name": "DiagnosticReportWithDetails", "type": "interface", "file": "types.ts", "definition": "DiagnosticReport & { studentName, generatedByName }", "usedBy": [ "data-access-reports.getDiagnosticReports", "actions", "components/report-list.tsx", "components/student-diagnostic-view.tsx" ] }, { "name": "ClassMasterySummary", "type": "interface", "file": "types.ts", "definition": "{ classId, className, studentCount, averageMastery, knowledgePointStats[], studentsNeedingAttention[] }", "usedBy": [ "data-access.getClassMasterySummary", "data-access-reports.generateClassDiagnosticReport", "components/class-diagnostic-view.tsx" ] }, { "name": "GradeMasterySummary", "type": "interface", "file": "types.ts", "definition": "{ gradeId, gradeName, studentCount, averageMastery, knowledgePointStats[], studentsNeedingAttention[] }(v4-P2-3 新增:年级掌握度摘要)", "usedBy": [ "data-access.getGradeMasterySummary", "data-access-reports.generateGradeDiagnosticReport" ] }, { "name": "KnowledgePointStat", "type": "interface", "file": "types.ts", "definition": "{ knowledgePointId, knowledgePointName, averageMastery, masteredCount(≥80), notMasteredCount(<60), totalStudents }", "usedBy": [ "data-access.getKnowledgePointStats", "types.ClassMasterySummary", "components/class-diagnostic-view.tsx" ] }, { "name": "DiagnosticReportQueryParams", "type": "interface", "file": "types.ts", "definition": "{ studentId?, reportType?, status?, period?, limit?(P3-15), offset?(P3-15) }", "usedBy": [ "data-access-reports.getDiagnosticReports", "actions.getDiagnosticReportsAction" ] }, { "name": "DiagnosticReportListResult", "type": "interface", "file": "types.ts", "definition": "{ reports: DiagnosticReportWithDetails[], total: number }(P3-15 修复:分页查询结果)", "usedBy": [ "data-access-reports.getDiagnosticReports" ] }, { "name": "MasteryRadarPoint", "type": "interface", "file": "types.ts", "definition": "{ knowledgePoint, student(0-100), classAverage?(0-100) }", "usedBy": [ "components/mastery-radar-chart.tsx", "components/student-diagnostic-view.tsx", "teacher/diagnostic/student/[studentId]/page.tsx" ] } ], "components": [ { "name": "MasteryRadarChart", "file": "components/mastery-radar-chart.tsx", "purpose": "知识点掌握度雷达图(recharts RadarChart,学生 vs 班级平均对比,无数据时显示 EmptyState)", "deps": [ "recharts", "shared/components/ui/card", "shared/components/ui/chart", "shared/components/ui/empty-state" ] }, { "name": "StudentDiagnosticView", "file": "components/student-diagnostic-view.tsx", "purpose": "学生诊断视图(概览卡片、雷达图、强项/弱项列表、生成报告表单[DIAGNOSTIC_MANAGE]、最新报告与建议展示;v3-P2 新增:practiceHrefBase prop,null 时隐藏练习按钮;P3-22 改进:练习按钮添加 aria-label 含知识点名)", "props": "{ studentId, summary, classAverage?, reports?, practiceHrefBase?: string | null }", "deps": [ "usePermission", "actions.generateStudentReportAction", "components/mastery-radar-chart", "shared/components/ui/*" ] }, { "name": "ClassDiagnosticView", "file": "components/class-diagnostic-view.tsx", "purpose": "班级诊断视图(概览卡片、知识点掌握度热力图[绿/黄/橙/红]、知识点排名表、需重点关注学生表[链接到学生视图]、生成班级报告表单[DIAGNOSTIC_MANAGE])", "deps": [ "usePermission", "actions.generateClassReportAction", "shared/components/ui/*" ] }, { "name": "ReportList", "file": "components/report-list.tsx", "purpose": "诊断报告列表(reportType/status 过滤器[URL searchParams]、报告表格、发布/删除操作[DIAGNOSTIC_MANAGE]、确认对话框;v2-P1-4 改用 useDiagnosticService Context 注入)", "deps": [ "usePermission", "services.useDiagnosticService", "shared/components/ui/*" ] } ], "services": [ { "name": "DiagnosticService", "type": "interface", "file": "services/diagnostic-service.ts", "description": "v2-P1-4 新增:诊断模块客户端服务接口,抽象 generateStudentReport/generateClassReport/generateGradeReport/publishReport/deleteReport/exportReport/getClassStudentsByKp 7 个操作", "usedBy": [ "services.diagnostic-service-context.DiagnosticServiceProvider", "services.default-diagnostic-service.defaultDiagnosticService", "services.monitored-diagnostic-service.createMonitoredDiagnosticService" ] }, { "name": "DiagnosticServiceProvider", "type": "component", "file": "services/diagnostic-service-context.tsx", "description": "v2-P1-4 新增:通过 React Context 注入 DiagnosticService 实现", "usedBy": [ "teacher/diagnostic/page.tsx", "teacher/diagnostic/class/[classId]/page.tsx" ] }, { "name": "useDiagnosticService", "type": "hook", "file": "services/diagnostic-service-context.tsx", "description": "v2-P1-4 新增:获取当前注入的 DiagnosticService 实例,必须在 Provider 内使用", "usedBy": [ "components/report-list.tsx", "components/class-diagnostic-view.tsx" ] }, { "name": "defaultDiagnosticService", "type": "const", "file": "services/default-diagnostic-service.ts", "description": "v2-P1-4 新增:默认服务实现,绑定现有 Server Actions(FormData 创建 + Action 调用)", "usedBy": [ "teacher/diagnostic/page.tsx", "teacher/diagnostic/class/[classId]/page.tsx" ] }, { "name": "DiagnosticMonitor", "type": "interface", "file": "services/diagnostic-monitor.ts", "description": "v2-P2-7 新增:诊断模块监控埋点接口,track(eventName, properties) 方法;默认 noopDiagnosticMonitor 不发送任何事件,生产环境可注入真实实现", "usedBy": [ "services.diagnostic-monitor-context.DiagnosticMonitorProvider", "services.monitored-diagnostic-service.createMonitoredDiagnosticService" ] }, { "name": "noopDiagnosticMonitor", "type": "const", "file": "services/diagnostic-monitor.ts", "description": "v2-P2-7 新增:默认 no-op 监控实现,静默返回不发送事件", "usedBy": [ "teacher/diagnostic/page.tsx", "teacher/diagnostic/class/[classId]/page.tsx" ] }, { "name": "DiagnosticMonitorProvider", "type": "component", "file": "services/diagnostic-monitor-context.tsx", "description": "v2-P2-7 新增:通过 React Context 注入 DiagnosticMonitor 实现", "usedBy": [ "teacher/diagnostic/page.tsx", "teacher/diagnostic/class/[classId]/page.tsx" ] }, { "name": "useDiagnosticMonitor", "type": "hook", "file": "services/diagnostic-monitor-context.tsx", "description": "v2-P2-7 新增:获取当前注入的 DiagnosticMonitor 实例,未注入 Provider 时返回 no-op", "usedBy": [] }, { "name": "createMonitoredDiagnosticService", "type": "function", "file": "services/monitored-diagnostic-service.ts", "description": "v2-P2-7 新增:工厂函数,通过组合模式包装 DiagnosticService 添加监控埋点,记录操作耗时/成功失败/错误信息,埋点失败不阻断业务流程", "usedBy": [ "teacher/diagnostic/page.tsx", "teacher/diagnostic/class/[classId]/page.tsx" ] } ] } }, "elective": { "path": "src/modules/elective", "description": "选课管理模块:选修课程 CRUD、选课开放/关闭、学生选课/退课、抽签模式批量录取、FCFS 即时录取、DataScope 行级过滤(admin 全部、teacher 所教、grade_head 所管年级、student 可选课程)", "exports": { "actions": [ { "name": "createElectiveCourseAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "创建选修课程(formData: name, subjectId?, teacherId, gradeId?, description?, capacity?, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, selectionMode?, credit?)", "deps": [ "requirePermission(ELECTIVE_MANAGE)", "data-access.createElectiveCourse", "revalidatePath" ], "usedBy": [ "admin/elective/create/page.tsx" ] }, { "name": "updateElectiveCourseAction", "permission": "ELECTIVE_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "更新选修课程", "deps": [ "requirePermission(ELECTIVE_MANAGE)", "data-access.getElectiveCourseById", "data-access.updateElectiveCourse", "revalidatePath" ], "usedBy": [ "admin/elective/[id]/edit/page.tsx" ] }, { "name": "deleteElectiveCourseAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "删除选修课程(formData: courseId)", "deps": [ "requirePermission(ELECTIVE_MANAGE)", "data-access.deleteElectiveCourse", "revalidatePath" ], "usedBy": [ "components/elective-course-list.tsx" ] }, { "name": "openSelectionAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "开放选课(formData: courseId)", "deps": [ "requirePermission(ELECTIVE_MANAGE)", "data-access.openSelection", "revalidatePath" ], "usedBy": [ "components/elective-course-list.tsx" ] }, { "name": "closeSelectionAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "关闭选课(formData: courseId)", "deps": [ "requirePermission(ELECTIVE_MANAGE)", "data-access.closeSelection", "revalidatePath" ], "usedBy": [ "components/elective-course-list.tsx" ] }, { "name": "runLotteryAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState<{enrolled:number,waitlist:number}> | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "执行抽签录取(formData: courseId)", "deps": [ "requirePermission(ELECTIVE_MANAGE)", "data-access-operations.runLottery", "revalidatePath" ], "usedBy": [ "components/elective-course-list.tsx" ] }, { "name": "selectCourseAction", "permission": "ELECTIVE_SELECT", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "学生选课(formData: courseId, priority?)", "deps": [ "requirePermission(ELECTIVE_SELECT)", "data-access-operations.selectCourse", "revalidatePath" ], "usedBy": [ "components/student-selection-view.tsx (StudentAvailableCoursesSection)" ] }, { "name": "dropCourseAction", "permission": "ELECTIVE_SELECT", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "学生退课(formData: courseId, dropReason?)—— P2-4:透传 dropReason 给 dropCourse,可选参数,trim 后非空才入库", "deps": [ "requirePermission(ELECTIVE_SELECT)", "data-access-operations.dropCourse", "revalidatePath" ], "usedBy": [ "components/student-selection-view.tsx (StudentMySelectionsSection)" ] }, { "name": "getElectiveCoursesAction", "permission": "ELECTIVE_READ", "signature": "(params?: GetElectiveCoursesParams) => Promise>", "file": "actions.ts", "purpose": "查询选修课程列表(按 DataScope 过滤)", "deps": [ "requirePermission(ELECTIVE_READ)", "data-access.getElectiveCourses (scope, currentUserId)" ], "usedBy": [], "issues": [ "P0: 无调用方——admin/teacher 页面绕过 Action 直接调用 data-access.getElectiveCourses,违反三层架构" ] }, { "name": "getStudentSelectionsAction", "permission": "ELECTIVE_READ", "signature": "(studentId: string) => Promise>", "file": "actions.ts", "purpose": "查询学生选课记录(含 DataScope 二次校验:class_members 仅自己,children 仅子女)", "deps": [ "requirePermission(ELECTIVE_READ)", "data-access-selections.getStudentSelections" ], "usedBy": [], "issues": [ "P0: 无调用方——页面绕过 Action 直接调用 data-access-selections.getStudentSelections" ] }, { "name": "getAvailableCoursesAction", "permission": "ELECTIVE_SELECT", "signature": "() => Promise>", "file": "actions.ts", "purpose": "获取学生可选课程(status=open 且匹配年级)", "deps": [ "requirePermission(ELECTIVE_SELECT)", "data-access-selections.getAvailableCoursesForStudent" ], "usedBy": [], "issues": [ "P0: 无调用方——student/elective 页面绕过 Action 直接调用 data-access-selections.getAvailableCoursesForStudent" ] } ], "dataAccess": [ { "name": "getElectiveCourses", "file": "data-access.ts", "signature": "(params?: GetElectiveCoursesParams & { scope?: DataScope; currentUserId?: string }) => Promise", "purpose": "查询选修课程列表(按 scope 行级过滤:owned/class_taught 按 teacherId,grade_managed 按 gradeIds)", "usedBy": [ "actions.getElectiveCoursesAction", "admin/elective/page.tsx", "teacher/elective/page.tsx" ] }, { "name": "getElectiveCourseById", "file": "data-access.ts", "signature": "(id: string) => Promise", "purpose": "获取课程详情", "usedBy": [ "actions.updateElectiveCourseAction", "admin/elective/[id]/edit/page.tsx" ] }, { "name": "createElectiveCourse", "file": "data-access.ts", "signature": "(data: CreateElectiveCourseInput, teacherId: string) => Promise", "purpose": "创建选修课程(status=draft, enrolledCount=0)", "usedBy": [ "actions.createElectiveCourseAction" ] }, { "name": "updateElectiveCourse", "file": "data-access.ts", "signature": "(id: string, data: Partial) => Promise", "purpose": "更新选修课程字段", "usedBy": [ "actions.updateElectiveCourseAction" ] }, { "name": "deleteElectiveCourse", "file": "data-access.ts", "signature": "(id: string) => Promise", "purpose": "删除选修课程", "usedBy": [ "actions.deleteElectiveCourseAction" ] }, { "name": "openSelection", "file": "data-access.ts", "signature": "(courseId: string) => Promise", "purpose": "开放选课(status=open)", "usedBy": [ "actions.openSelectionAction" ] }, { "name": "closeSelection", "file": "data-access.ts", "signature": "(courseId: string) => Promise", "purpose": "关闭选课(status=closed)", "usedBy": [ "actions.closeSelectionAction" ] }, { "name": "buildCourseSelect", "file": "data-access.ts", "signature": "() => query builder", "purpose": "构建 electiveCourses 表查询(仅查询本表字段,不跨表 JOIN;v3 重构:移除跨模块 LEFT JOIN,名称解析改由 resolveCourseDisplayNames 异步聚合)", "usedBy": [ "data-access.getElectiveCourses", "data-access.getElectiveCourseById", "data-access-selections.getAvailableCoursesForStudent" ] }, { "name": "mapCourseRow", "file": "data-access.ts", "signature": "(row: CourseCoreRow, display: {teacherName?, subjectName?, gradeName?}) => ElectiveCourseWithDetails", "purpose": "将核心行 + 显示名映射为 ElectiveCourseWithDetails(v3 抽取:消除 data-access 与 data-access-selections 重复代码)", "usedBy": [ "data-access.getElectiveCourses", "data-access.getElectiveCourseById", "data-access-selections.getAvailableCoursesForStudent" ] }, { "name": "resolveCourseDisplayNames", "file": "data-access.ts", "signature": "(rows: CourseCoreRow[]) => Promise<{teacherName?, subjectName?, gradeName?}[]>", "purpose": "并行聚合教师名(users.getUserNamesByIds)、学科(school.getSubjectOptions)、年级(school.getGradeOptions),返回每行的显示名映射(v3 重构:替代跨模块 LEFT JOIN)", "usedBy": [ "data-access.getElectiveCourses", "data-access.getElectiveCourseById" ] }, { "name": "getCourseSelections", "file": "data-access-selections.ts", "signature": "(courseId: string) => Promise", "purpose": "查询课程所有选课记录(按 priority, selectedAt 排序)", "usedBy": [ "待扩展" ] }, { "name": "getStudentSelections", "file": "data-access-selections.ts", "signature": "(studentId: string) => Promise", "purpose": "查询学生选课记录(按 selectedAt 降序,React cache() 包装请求级去重)", "usedBy": [ "actions.getStudentSelectionsAction", "student/elective/_components/my-selections-loader.tsx", "student/elective/_components/available-courses-loader.tsx", "parent/elective/page.tsx" ] }, { "name": "getStudentGradeId", "file": "data-access-selections.ts", "signature": "(studentId: string) => Promise", "purpose": "获取学生所在年级 ID(通过 classEnrollments active 记录)", "usedBy": [ "data-access-selections.getAvailableCoursesForStudent" ] }, { "name": "getAvailableCoursesForStudent", "file": "data-access-selections.ts", "signature": "(studentId: string, gradeId?: string | null) => Promise", "purpose": "获取学生可选课程(status=open 且 gradeId 匹配或为空)", "usedBy": [ "actions.getAvailableCoursesAction", "student/elective/_components/available-courses-loader.tsx" ] }, { "name": "runLottery", "file": "data-access-operations.ts", "signature": "(courseId: string) => Promise<{enrolled: number, waitlist: number}>", "purpose": "抽签录取(Fisher-Yates 无偏洗牌 selected 记录,前 capacity 名 enrolled,其余 waitlist;P1-12 重写:不再自动将课程置为 closed,保留 status=open 支持管理员重抽;v3 修复:替换 sort(Math.random) 有偏洗牌)", "usedBy": [ "actions.runLotteryAction" ] }, { "name": "selectCourse", "file": "data-access-operations.ts", "signature": "(courseId: string, studentId: string, priority?: number) => Promise<{ status: CourseSelectionStatus }>", "purpose": "学生选课(校验课程状态/时间窗口/重复选课;FCFS 模式即时 enrolled/waitlist,lottery 模式 selected;P0-4:所有 throw 改用 ElectiveBusinessError(code, params);返回值仅含 status,message 由 actions 层翻译;v3 修复:db.transaction 包裹 + .for('update') 锁课程行防 FCFS 超卖。P2-4 增强:1) 事务前通过 getStudentGradeId(studentId) 拿到学生年级 ID 传入 checkCreditLimit;2) checkCreditLimit 改用 getElectiveCreditLimit(gradeId) 读取可配置学分上限;3) 事务成功后调用 notifyCapacityThresholdIfNeeded 触发容量阈值通知(fire-and-forget 不阻塞主流程))", "deps": [ "data-access-selections.getStudentGradeId", "data-access-operations.checkCreditLimit", "data-access-operations.notifyCapacityThresholdIfNeeded" ], "usedBy": [ "actions.selectCourseAction" ] }, { "name": "dropCourse", "file": "data-access-operations.ts", "signature": "(courseId: string, studentId: string, dropReason?: string) => Promise", "purpose": "学生退课(status=dropped;FCFS 模式自动递补 waitlist 首位;v3 修复:db.transaction 包裹 + .for('update') 锁课程行保证递补一致性。P2-4 增强:1) 校验 course.dropDeadline,超过则抛 ElectiveBusinessError(\"dropDeadlinePassed\");2) 接受 dropReason 参数(已 trim),非空时写入 courseSelections.dropReason 字段,为空则写 null)", "usedBy": [ "actions.dropCourseAction" ] }, { "name": "checkScheduleConflict", "file": "data-access-operations.ts", "signature": "(tx: Tx, studentId: string, courseId: string) => Promise", "purpose": "查询学生已选课程时间段,与新课程时间段逐一调用 isScheduleConflict 判断冲突(P2-9 新增:选课时间冲突检测,selectCourse 调用)", "deps": [ "shared.db", "shared.db.schema.courseSelections", "shared.db.schema.electiveCourses", "lib.isScheduleConflict" ], "usedBy": [ "data-access-operations.selectCourse" ] }, { "name": "checkCreditLimit", "file": "data-access-operations.ts", "signature": "(tx: Tx, studentId: string, courseId: string, studentGradeId: string | null) => Promise<{ exceeded: boolean; current: number; max: number }>", "purpose": "查询学生本学期已选课程学分总和,加上新课程学分后判断是否超过学期学分上限(P2-10 新增,P2-4 增强:移除硬编码 MAX_CREDIT_PER_TERM=10,改用 data-access-settings.getElectiveCreditLimit(studentGradeId) 按年级可配置读取,未配置时 fallback 到全局默认值 10。selectCourse 调用)", "deps": [ "shared.db", "shared.db.schema.courseSelections", "shared.db.schema.electiveCourses", "data-access-settings.getElectiveCreditLimit" ], "usedBy": [ "data-access-operations.selectCourse" ] }, { "name": "getElectiveOverviewStats", "file": "data-access-stats.ts", "signature": "() => Promise", "purpose": "管理员概览统计(P1-13 新增):4 项关键指标——课程总数、总选课人数、平均容量使用率、待抽签课程数。使用 SQL 聚合(count/AVG/CASE WHEN)避免拉全表,3 个并行查询合并", "deps": [ "shared.db", "shared.db.schema.electiveCourses", "shared.db.schema.courseSelections" ], "usedBy": [ "admin/elective/page.tsx" ] }, { "name": "getElectiveCreditLimit", "file": "data-access-settings.ts", "signature": "(gradeId?: string | null) => Promise", "purpose": "获取学期学分上限(P2-4 新增):1) 若传入 gradeId 先查 system_settings 中 creditLimit:grade:;2) fallback 到 creditLimit:default;3) 均未配置返回默认值 10。React cache() 包装请求级去重", "deps": [ "shared.db", "shared.db.schema.systemSettings" ], "usedBy": [ "data-access-operations.checkCreditLimit" ] }, { "name": "getCapacityNotifyThreshold", "file": "data-access-settings.ts", "signature": "() => Promise", "purpose": "获取容量阈值通知比例(P2-4 新增):当课程 enrolledCount >= capacity * threshold 时触发教师通知。默认 0.9(90%),可由 system_settings 配置。React cache() 包装请求级去重", "deps": [ "shared.db", "shared.db.schema.systemSettings" ], "usedBy": [ "data-access-operations.notifyCapacityThresholdIfNeeded" ] }, { "name": "ELECTIVE_DEFAULTS", "file": "data-access-settings.ts", "type": "const", "purpose": "默认值常量(P2-4 新增):MAX_CREDIT_PER_TERM=10、CAPACITY_NOTIFY_THRESHOLD=0.9。供测试与文档引用,避免硬编码" }, { "name": "notifyCapacityThresholdIfNeeded", "file": "data-access-operations.ts", "signature": "(course: { id: string; name: string; teacherId: string | null; capacity: number; enrolledCount: number }) => Promise", "purpose": "容量阈值通知(P2-4 新增):仅当 newEnrolledCount === Math.ceil(capacity * threshold) 时触发一次通知(避免每次递增都发通知)。Fire-and-forget:catch 中吞错并 console.error,不阻塞主流程。通过 notifications.sendNotification 发送给 course.teacherId,type=\"warning\",附带 actionUrl 指向课程详情。i18n 标题/内容通过 next-intl getTranslations(\"elective\") 翻译", "deps": [ "data-access-settings.getCapacityNotifyThreshold", "notifications.sendNotification", "next-intl/server.getTranslations" ], "usedBy": [ "data-access-operations.selectCourse" ] } ], "types": [ { "name": "ElectiveCourseStatus", "type": "type", "file": "types.ts", "definition": "\"draft\" | \"open\" | \"closed\" | \"cancelled\"" }, { "name": "ElectiveSelectionMode", "type": "type", "file": "types.ts", "definition": "\"fcfs\" | \"lottery\"" }, { "name": "CourseSelectionStatus", "type": "type", "file": "types.ts", "definition": "\"selected\" | \"enrolled\" | \"waitlist\" | \"dropped\" | \"rejected\"" }, { "name": "ElectiveCourse", "type": "interface", "file": "types.ts", "definition": "{ id, name, subjectId?, teacherId, gradeId?, description?, capacity, enrolledCount, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, dropDeadline?, status, selectionMode, credit, createdAt, updatedAt } —— P2-4 新增 dropDeadline?: string | null(退课截止时间 ISO)" }, { "name": "ElectiveCourseWithDetails", "type": "interface", "file": "types.ts", "definition": "ElectiveCourse & { teacherName?, subjectName?, gradeName? }" }, { "name": "CourseCoreRow", "type": "type", "file": "data-access.ts", "definition": "buildCourseSelect 返回行的推断类型(v3 新增:供 mapCourseRow/resolveCourseDisplayNames 共享)" }, { "name": "CourseSelection", "type": "interface", "file": "types.ts", "definition": "{ id, courseId, studentId, status, priority?, selectedAt, enrolledAt?, droppedAt?, lotteryRank?, dropReason?, createdAt, updatedAt } —— P2-4 新增 dropReason?: string | null(退课理由,最多 255 字符)" }, { "name": "CourseSelectionWithDetails", "type": "interface", "file": "types.ts", "definition": "CourseSelection & { courseName?, studentName?, courseCapacity?, courseEnrolledCount?, courseStatus? }" }, { "name": "GetElectiveCoursesParams", "type": "interface", "file": "types.ts", "definition": "{ status?, gradeId?, subjectId?, teacherId? }" }, { "name": "ELECTIVE_STATUS_LABELS", "type": "const", "file": "types.ts", "description": "课程状态标签常量" }, { "name": "ELECTIVE_STATUS_COLORS", "type": "const", "file": "types.ts", "description": "课程状态颜色常量(Badge variant)" }, { "name": "SELECTION_MODE_LABELS", "type": "const", "file": "types.ts", "description": "选课模式标签常量" }, { "name": "COURSE_SELECTION_STATUS_LABELS", "type": "const", "file": "types.ts", "description": "选课状态标签常量" }, { "name": "COURSE_SELECTION_STATUS_COLORS", "type": "const", "file": "types.ts", "description": "选课状态颜色常量(Badge variant)" } ], "schemas": [ { "name": "ElectiveCourseStatusEnum", "file": "schema.ts", "definition": "z.enum([\"draft\",\"open\",\"closed\",\"cancelled\"])" }, { "name": "ElectiveSelectionModeEnum", "file": "schema.ts", "definition": "z.enum([\"fcfs\",\"lottery\"])" }, { "name": "CourseSelectionStatusEnum", "file": "schema.ts", "definition": "z.enum([\"selected\",\"enrolled\",\"waitlist\",\"dropped\",\"rejected\"])" }, { "name": "CreateElectiveCourseSchema", "file": "schema.ts", "purpose": "创建课程校验(name 必填,teacherId 必填,capacity 1-500 默认 30,selectionMode 默认 fcfs,credit 默认 1.0,P2-4 新增 dropDeadline 可选 datetime-local 字符串)" }, { "name": "UpdateElectiveCourseSchema", "file": "schema.ts", "purpose": "更新课程校验(所有字段可选,含 status;P2-4 新增 dropDeadline 可选)" }, { "name": "SelectCourseSchema", "file": "schema.ts", "purpose": "选课校验(courseId 必填,priority 1-10 可选)" }, { "name": "DropCourseSchema", "file": "schema.ts", "purpose": "退课校验(courseId 必填,P2-4 新增 dropReason: z.string().trim().max(255).optional())" }, { "name": "RunLotterySchema", "file": "schema.ts", "purpose": "抽签校验(courseId 必填)" } ], "components": [ { "name": "ElectiveCourseList", "file": "components/elective-course-list.tsx", "purpose": "课程卡片列表(管理员/教师视图,含编辑/开放/关闭/抽签/删除操作按钮,usePermission 控制权限)", "deps": [ "usePermission", "actions.deleteElectiveCourseAction", "actions.openSelectionAction", "actions.closeSelectionAction", "actions.runLotteryAction", "shared/components/ui/*" ] }, { "name": "ElectiveCourseForm", "file": "components/elective-course-form.tsx", "purpose": "课程创建/编辑表单(name, subjectId, teacherId, gradeId, description, capacity, classroom, schedule, dates, selectionMode, credit)", "deps": [ "react-hook-form", "actions.createElectiveCourseAction", "actions.updateElectiveCourseAction", "shared/components/ui/*" ] }, { "name": "StudentMySelectionsSection", "file": "components/student-selection-view.tsx", "purpose": "学生\"我的选课\"区块客户端组件(P2-3:由原 StudentSelectionView 拆分):展示已选/已录取/候补课程卡片 + 退课按钮,独立 Suspense 边界", "deps": [ "actions.dropCourseAction", "constants.COURSE_SELECTION_STATUS_BADGE_VARIANTS", "constants.COURSE_SELECTION_STATUS_LABEL_KEYS", "shared/components/ui/*" ], "usedBy": [ "student/elective/_components/my-selections-loader.tsx" ] }, { "name": "StudentAvailableCoursesSection", "file": "components/student-selection-view.tsx", "purpose": "学生\"可选课程\"区块客户端组件(P2-3:由原 StudentSelectionView 拆分):展示可选课程卡片 + 选课按钮,通过 selectedCourseIds prop 接收已选课程集合,独立 Suspense 边界", "deps": [ "actions.selectCourseAction", "constants.ELECTIVE_STATUS_BADGE_VARIANTS", "constants.ELECTIVE_STATUS_LABEL_KEYS", "constants.SELECTION_MODE_LABEL_KEYS", "shared/components/ui/*" ], "usedBy": [ "student/elective/_components/available-courses-loader.tsx" ] }, { "name": "ElectivePageLayout", "file": "components/elective-page-layout.tsx", "purpose": "选课页面布局骨架(P2-4 新增:抽取 admin/teacher 选课页重复的 header/children 布局结构,统一 spacing)", "deps": [ "shared/lib/utils.cn" ], "usedBy": [ "admin/elective/page.tsx", "teacher/elective/page.tsx" ] }, { "name": "ElectiveStatsCards", "file": "components/elective-stats-cards.tsx", "purpose": "管理员统计卡片网格(P1-13 新增):4 卡片展示总课程数/总选课人数/平均容量使用率/待抽签课程数,复用 attendance-stats-cards 模式,通过 props 注入数据(不直接调用 data-access)", "deps": [ "data-access-stats.ElectiveOverviewStats (type only)", "shared/components/ui/card" ], "usedBy": [ "admin/elective/page.tsx" ] }, { "name": "ParentSelectionView", "file": "components/parent-selection-view.tsx", "purpose": "家长查看子女选课只读视图(P0-2 新增):展示子女已选/已录取/候补课程卡片,复用 Badge variant + i18n key,组件本身不依赖 studentId,由父页面注入已过滤的数据。家长无选/退课操作权限", "deps": [ "constants.COURSE_SELECTION_STATUS_BADGE_VARIANTS", "constants.COURSE_SELECTION_STATUS_LABEL_KEYS", "shared/components/ui/card" ], "usedBy": [ "parent/elective/page.tsx" ] } ], "resolvers": [ { "name": "CourseDisplayResolver", "type": "interface", "file": "resolvers.ts", "definition": "{ getUserNamesByIds(ids: string[]): Promise>; getSubjectOptions(): Promise>; getGradeOptions(): Promise> }", "purpose": "课程显示名解析接口(P2-8 新增:抽象跨模块依赖 users/school data-access,便于测试注入 mock)", "usedBy": [ "data-access.resolveCourseDisplayNames", "data-access-selections.resolveStudentDisplayNames" ] }, { "name": "StudentGradeResolver", "type": "interface", "file": "resolvers.ts", "definition": "{ getStudentActiveGradeId(studentId: string): Promise }", "purpose": "学生年级解析接口(P2-8 新增:抽象跨模块依赖 classes data-access,便于测试注入 mock)", "usedBy": [ "data-access-selections.getStudentGradeId" ] }, { "name": "getCourseDisplayResolver", "signature": "() => CourseDisplayResolver", "file": "resolvers.ts", "purpose": "获取当前课程显示名解析器(默认委托 users/school data-access;测试可通过 setCourseDisplayResolver 注入 mock)", "usedBy": [ "data-access.resolveCourseDisplayNames", "data-access-selections.resolveStudentDisplayNames" ] }, { "name": "getStudentGradeResolver", "signature": "() => StudentGradeResolver", "file": "resolvers.ts", "purpose": "获取当前学生年级解析器(默认委托 classes data-access;测试可通过 setStudentGradeResolver 注入 mock)", "usedBy": [ "data-access-selections.getStudentGradeId" ] }, { "name": "setCourseDisplayResolver", "signature": "(resolver: CourseDisplayResolver) => void", "file": "resolvers.ts", "purpose": "注入自定义课程显示名解析器(仅测试用,生产代码勿调)" }, { "name": "setStudentGradeResolver", "signature": "(resolver: StudentGradeResolver) => void", "file": "resolvers.ts", "purpose": "注入自定义学生年级解析器(仅测试用,生产代码勿调)" }, { "name": "resetResolvers", "signature": "() => void", "file": "resolvers.ts", "purpose": "重置解析器为默认实现(测试 afterEach 清理用)" } ], "lib": [ { "name": "parseSchedule", "signature": "(schedule: string | null) => Array<{ day: string; start: string; end: string }>", "file": "data-access-operations.ts", "purpose": "纯函数:解析时间段字符串(P1-11 重写:支持中文 周一/星期一 + 英文 Mon/Monday 全称/缩写;支持多时段,按逗号/分号/中文分号拆分;返回数组,空数组表示无法解析)。配合 normalizeDay 将星期统一为数字字符串。", "usedBy": [ "data-access-operations.isScheduleConflict", "data-access-operations.checkScheduleConflict" ] }, { "name": "isScheduleConflict", "signature": "(a: string, b: string) => boolean", "file": "data-access-operations.ts", "purpose": "纯函数:判断两个时间段是否冲突(同一天且时间区间重叠);任一无法解析返回 false(P2-9 新增,可独立单测)", "usedBy": [ "data-access-operations.checkScheduleConflict" ] } ], "importExport": [ { "name": "exportElectiveCoursesToExcel", "signature": "(params: { status?: string; teacherId?: string }) => Promise", "file": "export.ts", "purpose": "导出选修课程列表到 Excel(P2-11 新增:课程名/学科/教师/年级/容量/已选/状态/模式/学分/时间段;列头使用 next-intl getTranslations 国际化)", "deps": [ "shared.lib.excel.exportToExcel", "data-access.getElectiveCourses", "next-intl/server.getTranslations" ], "usedBy": [ "app/api/export/route.ts" ] }, { "name": "exportCourseSelectionsToExcel", "signature": "(params: { courseId: string }) => Promise", "file": "export.ts", "purpose": "导出课程选课名单到 Excel(P2-11 新增:学生名/状态/优先级/选课时间/录取时间;列头使用 next-intl getTranslations 国际化)", "deps": [ "shared.lib.excel.exportToExcel", "data-access-selections.getCourseSelections", "next-intl/server.getTranslations" ], "usedBy": [ "app/api/export/route.ts" ] } ] } }, "lesson_preparation": { "path": "src/modules/lesson-preparation", "description": "教师备课模块(V4 纸感重构):抛弃 React Flow 画布,改用三栏布局(260px 结构树 + 1fr 纸感编辑器 + 380px 详情面板)。正文采用 Tiptap 富文本编辑器,锚点从字符串偏移升级为 Tiptap Mark(AnchorMark 区间锚/AnchorPoint 点锚),v3 旧锚点通过 AnchorMigrationBanner 提示失效。doc.version=4,editor-slice 移除 updateNodePosition/connect/setEdges/autoLayout。新增 interaction block(师生交互对话回合,含 designIntent/turns[]/expectedAnswer)。结构树支持节点展开到正文流(expanded-slice)。PrintView 改为按 nodes.order 排序渲染,不再依赖画布。v1→v2→v3→v4 链式迁移。保留模板、版本管理、知识点标注、题目创建/拉取、作业发布、撤销/重做、AI 反馈/差异化、一致性校验、课时安排等能力", "exports": { "dataAccess": [ { "name": "getLessonPlans", "file": "data-access.ts", "purpose": "查询课案列表(按教师/教材/章节过滤),按 textbookId+chapterId+creatorId 聚合版本:同一教师对同一章节的多个课案合并为一张卡片,返回代表项 + versionCount + versions 摘要数组" }, { "name": "getLessonPlanById", "file": "data-access.ts", "purpose": "按 ID 获取课案详情" }, { "name": "createLessonPlan", "file": "data-access.ts", "purpose": "创建课案(V2-2:接受 translateTitle 函数翻译 SYSTEM_TEMPLATES 的 i18n 键后存储到 DB)" }, { "name": "updateLessonPlanContent", "file": "data-access.ts", "purpose": "更新课案内容(v2 nodes+edges JSON)" }, { "name": "softDeleteLessonPlan", "file": "data-access.ts", "purpose": "软删除课案" }, { "name": "duplicateLessonPlan", "file": "data-access.ts", "purpose": "复制课案(V3:接受 duplicateSuffix 参数消除硬编码中文,由 actions 层传入 i18n 翻译的副本后缀)" }, { "name": "getTemplateById", "file": "data-access.ts", "purpose": "按 ID 获取模板" }, { "name": "buildInitialContent", "file": "lib/document-migration.ts(data-access.ts re-export)", "purpose": "基于模板构建初始课案内容(v3 nodes+edges+anchors+textbookContentNodeId)" }, { "name": "migrateV1ToV2", "file": "lib/document-migration.ts(data-access.ts re-export)", "purpose": "v1→v2 迁移:将旧 blocks 数组转换为 nodes + 线性 edges(节点按网格布局),使用类型守卫 isV1Document/isV2Document 替代 as 断言" }, { "name": "migrateV2ToV3", "file": "lib/document-migration.ts(data-access.ts re-export)", "purpose": "v2→v3 迁移:注入 textbook_content 节点(draggable=false)、初始化 anchors 数组、设置 textbookContentNodeId;可选传入 chapterId/chapterContent 填充正文" }, { "name": "normalizeDocument", "file": "lib/document-migration.ts(data-access.ts re-export)", "purpose": "规范化:确保 content 为 v3 格式,兼容旧 v1/v2 数据(自动调用 migrateV1ToV2 → migrateV2ToV3 链式迁移)" }, { "name": "buildDefaultSkeleton", "file": "lib/document-migration.ts", "purpose": "v3 默认骨架:生成 1 个 textbook_content 节点(画布中央)+ 10 个教学节点(objective/key_point/import/new_teaching/consolidation/summary/homework/blackboard/text_study/reflection)+ 默认 flow edges" }, { "name": "defaultDataForType", "file": "lib/document-migration.ts", "purpose": "按 BlockType 返回初始 data(11 种定制节点 + rich_text),供 addNode/buildDefaultSkeleton 使用" }, { "name": "isTextbookContentNode", "file": "lib/document-migration.ts", "purpose": "类型守卫:判断节点是否为 textbook_content 类型" }, { "name": "isAnchorEdge", "file": "lib/document-migration.ts", "purpose": "类型守卫:判断边是否为 anchor 锚点连线(vs flow 流程连线)" }, { "name": "getAnchorsForNode", "file": "lib/document-migration.ts", "purpose": "按 nodeId 过滤 anchors 数组" }, { "name": "getActiveAnchorIds", "file": "lib/document-migration.ts", "purpose": "获取选中节点关联的 anchor ID 集合(用于正文渲染时高亮)" }, { "name": "getAnchorEdges", "file": "lib/document-migration.ts", "purpose": "获取锚点连线列表(用于 rf-mappers 区分透明度策略)" }, { "name": "markdownToPlainText", "file": "lib/anchor-injector.ts", "purpose": "Markdown 转纯文本(去除语法标记),用于建立纯文本偏移↔Markdown 偏移映射" }, { "name": "injectPlaceholders", "file": "lib/anchor-injector.ts", "purpose": "将 anchors 注入 Markdown 正文:range 锚点包裹为 [[anchor:id]]...[[/anchor:id]],point 锚点插入 ①②③ 带圈数字" }, { "name": "parseAnchoredText", "file": "lib/anchor-injector.ts", "purpose": "解析注入后的 Markdown 为段落数组(text/anchor-range/anchor-point),供 textbook-content-node 自定义渲染" }, { "name": "getNextPointIndex", "file": "lib/anchor-injector.ts", "purpose": "计算下一个 point 锚点的带圈数字序号" }, { "name": "toCircledNumber", "file": "lib/anchor-injector.ts", "purpose": "数字转带圈数字字符(1→①, 2→②, ...)" }, { "name": "relocateAnchors", "file": "lib/anchor-injector.ts", "purpose": "正文变更后按文本相似度重定位锚点偏移量" }, { "name": "mergeBlockDataPatch", "file": "lib/type-guards.ts", "purpose": "✅ 类型守卫专项重构(2026-07-04)新增:安全合并 AI patch 到节点 data,替代 `{ ...node.data, ...patch } as BlockData` 断言。AI 返回的 patch 结构不可信,合并后必须通过对应 BlockType 的类型守卫校验:校验通过返回合并后的新数据;校验失败回退到原 node.data,保证编辑器数据完整性。内部通过 BLOCK_DATA_GUARDS: Record 注册表查找对应守卫。同时该文件所有 isXxxBlockData 守卫参数类型从 BlockData 放宽到 unknown(兼容 BlockDataGuard 协变签名)", "usedBy": [ "hooks/use-node-ai-assist.ts", "lib/ai-node-assist.ts" ] }, { "name": "getTextbooksForPicker", "file": "data-access.ts", "purpose": "获取教材列表(id+title),供 template-picker 选择教材" }, { "name": "getChaptersForPicker", "file": "data-access.ts", "purpose": "按 textbookId 获取章节树(含 children 嵌套),供 template-picker 选择章节" }, { "name": "getLessonPlanVersions", "file": "data-access-versions.ts", "purpose": "查询课案版本列表" }, { "name": "createLessonPlanVersion", "file": "data-access-versions.ts", "purpose": "创建课案版本(手动/自动)" }, { "name": "getVersionContent", "file": "data-access-versions.ts", "purpose": "获取指定版本内容" }, { "name": "revertToVersion", "file": "data-access-versions.ts", "purpose": "回滚到指定版本" }, { "name": "pruneAutoVersions", "file": "data-access-versions.ts", "purpose": "清理自动版本(保留最近 N 个)" }, { "name": "getLessonPlanTemplates", "file": "data-access-templates.ts", "purpose": "查询模板列表(系统/个人)" }, { "name": "saveAsTemplate", "file": "data-access-templates.ts", "purpose": "将课案保存为个人模板" }, { "name": "deletePersonalTemplate", "file": "data-access-templates.ts", "purpose": "删除个人模板" }, { "name": "getLessonPlansByKnowledgePoint", "file": "data-access-knowledge.ts", "purpose": "按知识点反查课案" }, { "name": "getLessonPlansByQuestion", "file": "data-access-knowledge.ts", "purpose": "按题目反查课案" }, { "name": "publishLessonPlanHomework", "file": "publish-service.ts", "purpose": "发布课案为作业(编排 homework/exams/classes,通过对方 data-access 调用 addExamQuestions/getStudentIdsByClassIds,无直查跨模块表;V2-1:抛出 PublishServiceError 错误码;V2-3:显式字段映射替代 as unknown as;V3:移除 as 断言改用类型守卫,移除硬编码中文,接受 homeworkTitle/homeworkDescription 参数由 actions 层 i18n 翻译后传入)" }, { "name": "getLessonPlanStats", "file": "data-access.ts", "purpose": "V3 新增:查询课案统计(total/published/draft/archived),替代 admin page 直查 DB" }, { "name": "publishLessonPlan", "file": "data-access.ts", "purpose": "发布课案:设置 status=published(V3 新增,供学生/家长/教研组长只读查看)" }, { "name": "unpublishLessonPlan", "file": "data-access.ts", "purpose": "撤回发布:将 status 从 published 改为 draft(仅 published 课案可撤回;V3 新增)" }, { "name": "suggestKnowledgePoints", "file": "ai-suggest.ts", "purpose": "AI 建议知识点(基于课案内容)" }, { "name": "autoLayout", "file": "lib/auto-layout.ts", "purpose": "V5-8 U2 新增:基于 @dagrejs/dagre 的 DAG 自动布局算法,按 edges 拓扑排序计算 nodes 位置" }, { "name": "diffDocuments", "file": "lib/version-diff.ts", "purpose": "V5-16 T2 新增:纯函数对比两个 LessonPlanDocument 节点级差异,返回 added/removed/modified/unchanged + changedFields" }, { "name": "hasChanges", "file": "lib/version-diff.ts", "purpose": "V5-16 T2 新增:判断 VersionDiffResult 是否存在变更" }, { "name": "checkConsistency", "file": "lib/consistency-check.ts", "purpose": "V5-19 T3 新增:纯函数目标-评价一致性校验,输出 ConsistencyResult(缺少 objective/exercise 警告、exercise 无知识点警告、objective 未被评价覆盖警告)" }, { "name": "hasConsistencyWarnings", "file": "lib/consistency-check.ts", "purpose": "V5-19 T3 新增:判断 ConsistencyResult 是否存在警告" }, { "name": "computeCurriculumCoverage", "file": "lib/curriculum-coverage.ts", "purpose": "V5-20 T4 新增:纯函数按 chapterId 分组统计教师所有课案对教材知识点的覆盖率,识别 isBlindSpot 教学盲点" }, { "name": "getHeatLevel", "file": "lib/curriculum-coverage.ts", "purpose": "V5-20 T4 新增:根据覆盖率返回热力图颜色等级(0-4)" }, { "name": "generateLessonPlanFeedback", "file": "lib/ai-feedback.ts", "purpose": "V5-17 A1/A2 新增:AI 反馈闭环,纯服务端函数,4 维度 strengths/improvements/alignment/differentiation,含解释性 reason 字段;AI 不可用时返回空结果" }, { "name": "generateDifferentiationSuggestions", "file": "lib/ai-differentiation.ts", "purpose": "V5-21 A3 新增:AI 差异化教学建议生成,按 basic/intermediate/advanced 三层生成建议" }, { "name": "checkCurriculumAlignment", "file": "lib/ai-differentiation.ts", "purpose": "V5-21 A4 新增:AI 课标实时核对,检查课案是否覆盖教材知识点" }, { "name": "generateExplainableAssessment", "file": "lib/ai-differentiation.ts", "purpose": "V5-21 A5 新增:AI 可解释评估,对 exercise 节点给出 conclusion/rationale/suggestion" }, { "name": "getLessonPlanSchedules", "file": "data-access-schedules.ts", "purpose": "V5-7 W3/W4 新增:查询课案绑定的课时列表" }, { "name": "createLessonPlanSchedule", "file": "data-access-schedules.ts", "purpose": "V5-7 W3/W4 新增:课案绑定课时" }, { "name": "deleteLessonPlanSchedule", "file": "data-access-schedules.ts", "purpose": "V5-7 W3/W4 新增:删除课时绑定" } ], "actions": [ { "name": "getLessonPlansAction", "permission": "LESSON_PLAN_READ", "file": "actions.ts", "purpose": "查询课案列表" }, { "name": "getLessonPlanByIdAction", "permission": "LESSON_PLAN_READ", "file": "actions.ts", "purpose": "获取课案详情" }, { "name": "createLessonPlanAction", "permission": "LESSON_PLAN_CREATE", "file": "actions.ts", "purpose": "创建课案" }, { "name": "updateLessonPlanAction", "permission": "LESSON_PLAN_UPDATE", "file": "actions.ts", "purpose": "更新课案内容" }, { "name": "saveLessonPlanVersionAction", "permission": "LESSON_PLAN_UPDATE", "file": "actions.ts", "purpose": "保存课案版本" }, { "name": "getLessonPlanVersionsAction", "permission": "LESSON_PLAN_READ", "file": "actions.ts", "purpose": "查询课案版本列表" }, { "name": "revertLessonPlanVersionAction", "permission": "LESSON_PLAN_UPDATE", "file": "actions.ts", "purpose": "回滚到指定版本" }, { "name": "deleteLessonPlanAction", "permission": "LESSON_PLAN_DELETE", "file": "actions.ts", "purpose": "删除课案" }, { "name": "duplicateLessonPlanAction", "permission": "LESSON_PLAN_CREATE", "file": "actions.ts", "purpose": "复制课案" }, { "name": "getLessonPlanTemplatesAction", "permission": "LESSON_PLAN_READ", "file": "actions.ts", "purpose": "查询模板列表" }, { "name": "saveAsTemplateAction", "permission": "LESSON_PLAN_UPDATE", "file": "actions.ts", "purpose": "保存为个人模板" }, { "name": "deleteTemplateAction", "permission": "LESSON_PLAN_DELETE", "file": "actions.ts", "purpose": "删除个人模板" }, { "name": "suggestKnowledgePointsAction", "permission": "LESSON_PLAN_UPDATE", "file": "actions-ai.ts", "purpose": "AI 建议知识点" }, { "name": "publishLessonPlanHomeworkAction", "permission": "LESSON_PLAN_PUBLISH", "file": "actions-publish.ts", "purpose": "发布课案为作业" }, { "name": "publishLessonPlanAction", "permission": "LESSON_PLAN_PUBLISH", "file": "actions-publish.ts", "purpose": "发布课案(设置 status=published,供学生/家长/教研组长只读查看;V3 新增)" }, { "name": "unpublishLessonPlanAction", "permission": "LESSON_PLAN_PUBLISH", "file": "actions-publish.ts", "purpose": "撤回发布(将 status 从 published 改为 draft;V3 新增)" }, { "name": "getKnowledgePointOptionsAction", "permission": "LESSON_PLAN_READ", "file": "actions-kp.ts", "purpose": "获取知识点选项(委托 textbooks data-access)" }, { "name": "getTextbooksForPickerAction", "permission": "LESSON_PLAN_READ", "file": "actions.ts", "purpose": "获取教材列表(供 template-picker 选择教材)" }, { "name": "getChaptersForPickerAction", "permission": "LESSON_PLAN_READ", "file": "actions.ts", "purpose": "按 textbookId 获取章节树(供 template-picker 选择章节)" }, { "name": "duplicateLessonPlanFormAction", "permission": "LESSON_PLAN_CREATE", "file": "actions.ts", "purpose": "V5-12 R4 新增:FormData wrapper 适配 ,内部调用 duplicateLessonPlanAction 后 redirect 到新课案编辑页" }, { "name": "getLessonPlanSchedulesAction", "permission": "LESSON_PLAN_READ", "file": "actions-schedules.ts", "purpose": "V5-7 W3/W4 新增:查询课案绑定的课时列表" }, { "name": "createLessonPlanScheduleAction", "permission": "LESSON_PLAN_UPDATE", "file": "actions-schedules.ts", "purpose": "V5-7 W3/W4 新增:课案绑定课时" }, { "name": "deleteLessonPlanScheduleAction", "permission": "LESSON_PLAN_UPDATE", "file": "actions-schedules.ts", "purpose": "V5-7 W3/W4 新增:删除课时绑定" }, { "name": "generateLessonPlanFeedbackAction", "permission": "LESSON_PLAN_READ + AI_CHAT", "file": "actions-ai.ts", "purpose": "V5-17 A1/A2 新增:AI 反馈闭环,调用 generateLessonPlanFeedback 生成结构化反馈" }, { "name": "generateDifferentiationSuggestionsAction", "permission": "LESSON_PLAN_READ + AI_CHAT", "file": "actions-ai.ts", "purpose": "V5-21 A3 新增:AI 差异化教学建议生成" }, { "name": "checkCurriculumAlignmentAction", "permission": "LESSON_PLAN_READ + AI_CHAT", "file": "actions-ai.ts", "purpose": "V5-21 A4 新增:AI 课标实时核对" }, { "name": "generateExplainableAssessmentAction", "permission": "LESSON_PLAN_READ + AI_CHAT", "file": "actions-ai.ts", "purpose": "V5-21 A5 新增:AI 可解释评估" }, { "name": "getKnowledgePointsForAlignmentAction", "permission": "LESSON_PLAN_READ", "file": "actions-ai.ts", "purpose": "V5-21 A4 辅助新增:按 textbookId 获取知识点列表(仅 id+name),供课标核对 Tab 按需加载" }, { "name": "generateNodeContentAction", "permission": "LESSON_PLAN_READ + LESSON_PLAN_CREATE + AI_CHAT", "file": "actions-ai.ts", "purpose": "V4-N1 新增:节点级 AI 协助 - 根据节点 type 生成对应字段,返回 patch 由前端合并到 node.data" }, { "name": "optimizeNodeExpressionAction", "permission": "LESSON_PLAN_READ + LESSON_PLAN_CREATE + AI_CHAT", "file": "actions-ai.ts", "purpose": "V4-N2 新增:节点级 AI 协助 - 优化节点现有表达,保留结构仅修改字段值" }, { "name": "suggestNodeDifferentiationAction", "permission": "LESSON_PLAN_READ + LESSON_PLAN_CREATE + AI_CHAT", "file": "actions-ai.ts", "purpose": "V4-N3 新增:节点级 AI 协助 - 判定节点适合的差异化层次(basic/intermediate/advanced),前端据此设置 node.differentiation" }, { "name": "generateLayeredQuestionsAction", "permission": "LESSON_PLAN_READ + LESSON_PLAN_CREATE + AI_CHAT", "file": "actions-ai.ts", "purpose": "V4-N4 新增:节点级 AI 协助 - 为 interaction 节点生成分层提问,返回 QATurn[] 由前端追加到 turns" } ], "schemas": [ { "name": "createLessonPlanSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "创建课案输入校验(title 必填 1-255 字符,templateId 必填,textbookId/chapterId/subjectId/gradeId 可选)", "usedBy": [ "createLessonPlanAction" ] }, { "name": "updateLessonPlanContentSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "更新课案内容输入校验(planId 必填,title 可选 1-255 字符,content 为 record)", "usedBy": [ "updateLessonPlanAction" ] }, { "name": "saveVersionSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "保存版本输入校验(planId 必填,label 可选 1-100 字符)", "usedBy": [ "saveLessonPlanVersionAction" ] }, { "name": "revertVersionSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "回滚版本输入校验(planId 必填,versionNo 正整数)", "usedBy": [ "revertLessonPlanVersionAction" ] }, { "name": "saveAsTemplateSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "保存为模板输入校验(sourcePlanId 必填,name 必填 1-100 字符)", "usedBy": [ "saveAsTemplateAction" ] }, { "name": "suggestKnowledgePointsSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "AI 知识点推荐输入校验(doc 为 record,textbookId/chapterId 可选)", "usedBy": [ "suggestKnowledgePointsAction" ] }, { "name": "getKnowledgePointOptionsSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "知识点选项查询输入校验(textbookId/chapterId 可选)", "usedBy": [ "getKnowledgePointOptionsAction" ] }, { "name": "publishLessonPlanHomeworkSchema", "type": "ZodSchema", "file": "schema.ts", "purpose": "2026-06-23 审计修复新增:发布作业输入校验(planId/blockId 必填,classIds 至少 1 个,availableAt/dueAt 日期格式校验通过 refine(new Date(v).getTime()) 验证)。导出类型 PublishLessonPlanHomeworkInput", "usedBy": [ "publishLessonPlanHomeworkAction" ] } ] }, "dependencies": [ "textbooks", "questions", "exams", "homework", "classes", "files", "shared/lib/ai", "@tiptap/react", "@tiptap/starter-kit", "@tiptap/pm", "@tiptap/extension-image" ], "files": [ "types.ts", "constants.ts", "schema.ts", "lib/type-guards.ts", "lib/i18n-errors.ts", "lib/document-migration.ts", "lib/node-summary.ts", "lib/anchor-mark.ts", "lib/ai-node-assist.ts", "lib/export.ts", "lib/scope-check.ts", "lib/document-diff.ts", "config/block-registry.tsx", "providers/lesson-plan-provider.tsx", "providers/lesson-plan-provider-setup.tsx", "services/default-data-service.ts", "services/default-question-service.ts", "services/external-questions-bridge.ts", "data-access.ts", "data-access-versions.ts", "data-access-templates.ts", "data-access-knowledge.ts", "actions.ts", "actions-publish.ts", "actions-ai.ts", "actions-kp.ts", "actions-questions.ts", "publish-service.ts", "ai-suggest.ts", "seed-templates.ts", "hooks/use-lesson-plan-editor.ts", "hooks/editor-slice.ts", "hooks/selection-slice.ts", "hooks/version-slice.ts", "hooks/history-slice.ts", "hooks/expanded-slice.ts", "hooks/use-node-ai-assist.ts", "components/lesson-plan-list.tsx", "components/lesson-plan-card.tsx", "components/lesson-plan-filters.tsx", "components/lesson-plan-editor.tsx", "components/lesson-plan-readonly-view.tsx", "components/lesson-plan-error-boundary.tsx", "components/lesson-plan-skeleton.tsx", "components/template-picker.tsx", "components/version-history-drawer.tsx", "components/knowledge-point-picker.tsx", "components/question-bank-picker.tsx", "components/inline-question-editor.tsx", "components/publish-homework-dialog.tsx", "components/anchor-migration-banner.tsx", "components/print-view.tsx", "components/schedule-dialog.tsx", "components/consistency-check-dialog.tsx", "components/ai-feedback-dialog.tsx", "components/ai-differentiation-dialog.tsx", "components/curriculum-heatmap.tsx", "components/version-diff-view.tsx", "components/version-diff-viewer.tsx", "components/lesson-plan-mobile-view.tsx", "components/attachment-picker.tsx", "components/paper-editor/paper-editor.tsx", "components/paper-editor/paper-toolbar.tsx", "components/paper-editor/paper-context-menu.tsx", "components/paper-editor/textbook-tiptap-editor.tsx", "components/paper-editor/inline-node.tsx", "components/paper-editor/inline-qa-dialog.tsx", "components/structure-tree/structure-tree.tsx", "components/structure-tree/tree-node-row.tsx", "components/detail-panel/detail-panel.tsx", "components/detail-panel/detail-head.tsx", "components/detail-panel/detail-props.tsx", "components/detail-panel/qa-editor.tsx", "components/blocks/rich-text-block.tsx", "components/blocks/text-study-block.tsx", "components/blocks/exercise-block.tsx", "components/blocks/reflection-block.tsx", "components/blocks/objective-block.tsx", "components/blocks/key-point-block.tsx", "components/blocks/import-block.tsx", "components/blocks/new-teaching-block.tsx", "components/blocks/summary-block.tsx", "components/blocks/homework-block.tsx", "components/blocks/blackboard-block.tsx", "components/blocks/interaction-block.tsx", "components/calendar-view.tsx", "components/curriculum-map-view.tsx", "data-access-calendar.ts", "data-access-review.ts", "data-access-comments.ts", "data-access-formative.ts", "data-access-attachments.ts", "data-access-substitutes.ts", "data-access-ai-evaluation.ts", "data-access-analytics.ts", "data-access-schedules.ts", "actions-calendar.ts", "actions-review.ts", "actions-comments.ts", "actions-formative.ts", "actions-attachments.ts", "actions-substitutes.ts", "actions-ai-evaluation.ts", "actions-analytics.ts", "actions-schedules.ts" ], "i18n": { "namespace": "lessonPreparation", "status": "implemented", "messageFiles": [ "shared/i18n/messages/zh-CN/lesson-preparation.json", "shared/i18n/messages/en/lesson-preparation.json" ] }, "auditFixes": { "P0-1": "publish-service.ts 跨模块直查修复:改用 exams/classes data-access", "P0-2": "i18n 接入:17 个组件改造为 useTranslations/getTranslations", "P0-3": "buildScopeCondition 按 scope 类型精确过滤(class_taught/grade_managed/class_members/children)", "P1-1": "as never 断言替换为类型守卫函数;BLOCK_TYPE_LABELS/LESSON_PLAN_STATUS_LABELS 改为 i18n 键", "P1-2": "新增 LessonPlanErrorBoundary 错误边界", "P1-3": "新增 4 个 Skeleton 骨架屏组件", "P1-4": "alert/confirm/window.location.reload 替换为 toast/AlertDialog/router.refresh", "P1-5": "新增 LessonPlanProvider + Context 注入数据服务,支持多实例", "P1-7": "新增 4 个角色配置(TEACHER/ADMIN/STUDENT/PARENT)+ ROLE_CONFIGS 注册表", "P1-8": "新增 BLOCK_REGISTRY 注册表,node-edit-panel 配置驱动渲染", "P2-1": "5 个组件添加 role=dialog/aria-modal/aria-label", "P2-4": "预留 LessonPlanTracker 接口 + noopTracker 默认实现", "V2-1": "Server Actions i18n + 错误码模式:12 个 Action 通过 getTranslations 翻译错误消息;Service/DataAccess 层抛出 PublishServiceError/LessonPlanDataError 错误码,Actions 层通过 PUBLISH_ERROR_KEY_MAP 翻译", "V2-2": "SYSTEM_TEMPLATES i18n 化:name/title 改为 i18n 键,createLessonPlan 接受 translateTitle 函数在服务端翻译后存储到 DB", "V2-3": "as unknown as 类型断言清零:8 处替换为显式类型映射函数(mapRowToLessonPlan/mapRowToListItem/mapRowToTemplate/mapRowToVersion)+ 类型守卫(isLessonPlanStatus/isTemplateType/isTemplateScope)", "V2-4": "MiniMap nodeColor 复用 lib/node-summary.ts 的 getNodeColor", "V2-5": "a11y 深度修复:lesson-plan-filters/exercise-block/inline-question-editor 的 select 添加 label htmlFor 关联;exercise-block 题目列表改为 ul/li;node-editor 画布添加 role=application + 键盘导航配置", "V2-6": "Tracker 埋点接入:新增 useLessonPlanTrackerSafe hook,在 create/save/publish/revert/duplicate/archive 6 处调用 tracker.track", "V3-1": "课案发布/撤回:新增 publishLessonPlan/unpublishLessonPlan data-access 函数 + publishLessonPlanAction/unpublishLessonPlanAction Server Actions(LESSON_PLAN_PUBLISH 权限),支持课案 status=published 供学生/家长/教研组长只读查看", "V3-2": "只读画布组件:新增 LessonPlanReadonlyView,复用 React Flow(nodesDraggable=false, nodesConnectable=false),供学生/家长/管理员/教研组长查看已发布课案", "V3-3": "多角色视图:LessonPlanCard/LessonPlanList/LessonPlanEditor 新增 viewMode prop(teacher/student/parent/admin/gradeHead),动态跳转链接 + 发布/撤回按钮(AlertDialog 确认);LessonPlanEditor 新增 initialStatus prop", "V3-4": "锚点选择器重写:TextbookContentNode 新增 anchorableNodes/onCreateNewNode props,AnchorNodeSelector 重写为节点列表+创建新节点选项;NodeEditPanel 选中 textbook_content 时显示操作提示+锚点列表(含删除功能)", "V3-5": "模板分区显示:TemplatePicker 加载并显示个人模板(调用 getLessonPlanTemplatesAction),分区显示系统/个人模板", "V3-6": "rf-mappers 增强:toRfNodes ctx 新增 anchorableNodes/onCreateNewNode 字段;toRfEdges 锚点边颜色使用 getNodeColor(anchor.nodeId) 替代硬编码,anchorId 存入 edge.data;fromRfEdges 从 e.data.anchorId 读取 anchorId(回退到 className 判断)", "V3-7": "权限扩展:student/parent/grade_head/teaching_head 角色新增 LESSON_PLAN_READ 权限,可查看已发布课案", "V3-8": "DataScope 扩展:class_members 和 children 新增可选 gradeIds? 字段,auth-guard resolveDataScope 通过 classEnrollments.innerJoin(classes) 预解析年级 ID", "V3-9": "新增路由:/student/lesson-plans、/student/lesson-plans/[planId]/view、/parent/lesson-plans、/parent/lesson-plans/[planId]/view、/admin/lesson-plans、/admin/lesson-plans/[planId]/view", "V3-10": "导航变更:admin 导航新增「课案管理」、student 导航新增「我的课案」、parent 导航新增「孩子课案」", "V3-11": "P0-1 admin 直查 DB 修复:admin/lesson-plans/page.tsx 移除 import { db },改用 data-access 新增的 getLessonPlanStats() 函数", "V3-12": "P0-2 publish-service 硬编码中文 + as 断言修复:移除 as ExerciseBlockData/as typeof validTypes[number] 断言,改用 lib/type-guards.ts 类型守卫;移除硬编码中文标题/描述,改为接受 homeworkTitle/homeworkDescription 参数由 actions 层 i18n 翻译后传入", "V3-13": "P0-3 duplicateLessonPlan 硬编码修复:接受 duplicateSuffix 参数,由 actions 层传入 t('error.duplicateSuffix')", "V3-14": "P0-4 schema Zod 错误消息 i18n 化:schema.ts 所有错误消息改为 i18n 键;新增 lib/i18n-errors.ts(translateFieldErrors/safeParseWithI18n)在 actions 层翻译 Zod 错误", "V3-15": "P0-5 loading/error 边界补全:为 6 个路由新增 12 个 loading.tsx 和 error.tsx 文件", "V3-16": "P1-2 组件完全通过 service 调用:lesson-plan-card.tsx/lesson-plan-list.tsx/lesson-plan-editor.tsx 移除所有直接 import actions,改为通过 useLessonPlanContextSafe() 获取 service 调用;LessonPlanDataService 接口扩展 5 个方法", "V3-17": "P1-3 as 断言修复:新增 lib/type-guards.ts 集中类型守卫(11 种 BlockData 类型守卫);block-registry.tsx 所有 as XxxBlockData 替换为类型守卫;node-edit-panel.tsx/rf-mappers.ts 移除冗余 as 断言;node-editor.tsx MiniMap 使用类型守卫;use-lesson-plan-editor.ts updateNode patch 类型改为 Omit, 'type'>", "V3-18": "P1-4 MiniMap 颜色硬编码修复:lesson-plan-readonly-view.tsx MiniMap 移除硬编码 #455a64/#1976d2,改用 getNodeColor(nodeData.type)", "V3-19": "P0 硬编码中文修复(6处):actions.ts 的 getLessonPlanByIdAction/revertLessonPlanVersionAction 错误消息改为 i18n 键;data-access-versions.ts 的 revertToVersion 接受 revertLabel 参数;lesson-plan-error-boundary.tsx 重写为包装组件模式注入 i18n;lesson-plan-provider.tsx 错误消息改为英文", "V3-20": "P1 组件完全通过 service 调用(5个组件迁移):version-history-drawer/template-picker/knowledge-point-picker/publish-homework-dialog/question-bank-picker 移除直接 import actions,改用 useLessonPlanContextSafe() 获取 service;LessonPlanDataService 接口扩展 7 个方法 + 6 个导出类型;新增 providers/lesson-plan-provider-setup.tsx;3 个页面用 LessonPlanProviderSetup 包裹", "V3-21": "P2 文件拆分:textbook-content-node.tsx 从 578 行拆分为 3 个文件(主文件 471行 + anchor-node-selector.tsx + textbook-segments.tsx)", "V3-22": "P3 as 断言修复(8处):textbook-content-node.tsx 的 as unknown as 替换为 isTextbookContentNodePropsData 类型守卫;7 个 block 组件 select onChange as 断言替换为类型守卫(lib/type-guards.ts 新增 7 个字段值类型守卫)", "V3-36": "P1-1a node-summary.ts as 断言修复:移除 `node.data as { html?: string; ... }` 断言,改用 isRecord/getArrayLength/getString/getNumber 安全字段提取辅助函数从 unknown 收窄", "V3-37": "P1-1b ai-suggest.ts as 断言修复:移除 `as Record` 断言,改用 isRecord 类型守卫;extractNodeText 从 unknown 节点安全提取 html/sourceText 字段", "V3-38": "P1-1c data-access-templates.ts + data-access.ts as 断言修复:新增 normalizeTemplateBlocks 规范化函数(lib/type-guards.ts),替代 `as LessonPlanTemplate[\"blocks\"]` 和 `as unknown as LessonPlanTemplate[\"blocks\"]` 断言从 DB unknown 安全转换", "V3-39": "P1-1d inline-question-editor.tsx as 断言修复:移除 `as typeof QUESTION_TYPES[number]` 断言,改用 isInlineQuestionType 类型守卫", "V3-40": "P1-1e data-access-knowledge.ts as 断言修复:移除两处 `as unknown as` 断言,改用 getKnowledgePointIds/getQuestionIds 安全字段提取辅助函数", "V3-41": "P1-2/P1-3 actions-ai.ts + actions-kp.ts 隐式 any 修复:显式类型标注 `const nodes: unknown[]` 和 `let kps: KnowledgePointOption[]` 替代隐式 any;统一使用 handleActionError(e) + translateFieldErrors 处理错误", "V3-42": "P1-6 findChapterById 提取到共享工具:新增 textbooks/utils.ts 的 findChapterById 递归函数,消除 4 个页面文件(admin/student/parent/teacher lesson-plans view/edit page)+ data-access.ts 共 5 处重复本地递归实现", "V3-43": "P1-7/P1-8 a11y + 类型守卫修复:inline-question-editor.tsx 包裹 FocusTrap 实现焦点陷阱 + 补全 aria-label(选项输入/删除按钮/知识点选择按钮)+ 新增 optionLabel i18n 键;lesson-node.tsx 使用 isLessonNodeData 类型守卫替代 `as { node: LessonPlanNode }` 断言 + 空状态回退;template-picker.tsx 通过 ChapterPickerOption.children 递归类型消除 `as ChapterPickerOption[]` 断言", "V3-44": "P2-2 lesson-plan-card.tsx useCallback 性能优化:4 个异步处理函数(handleArchive/handleDuplicate/handlePublish/handleUnpublish)用 useCallback 包裹,避免每次渲染创建新函数引用", "V3-45": "P2-4 lesson-plan-list.tsx 筛选加载状态:增加 isLoading 状态,传递给 LessonPlanFilters;lesson-plan-filters.tsx 加载时禁用输入并设置 aria-busy,避免用户重复点击触发重复请求", "V3-46": "P2-6 ai-suggest.ts AI prompt 提取为模块常量:新增 AI_SUGGEST_PROMPT_TEMPLATE 常量,便于维护和未来国际化(注:AI prompt 属于系统级提示词,非用户可见文本,暂不纳入 next-intl i18n 体系)", "V4-1": "P0-1 跨模块越权修复:parent/lesson-plans/[planId]/view/page.tsx 和 student/lesson-plans/[planId]/view/page.tsx 新增 assertPlanInScope(planId, userId) 调用,防止越权访问非本人/子女范围课案", "V4-2": "P0-2 权限校验补全:9 个 lesson-plans 路由 page.tsx(teacher/admin/student/parent)新增 requirePermission(LESSON_PLAN_READ) 调用", "V4-3": "P0-3 锚点 a11y 修复:textbook-segments.tsx 锚点 span 补全 role=button/tabIndex=0/aria-label/键盘事件处理", "V4-4": "P0-4 跨模块解耦:新增 default-data-service.ts 的 getQuestions 方法 + QuestionService 接口;question-bank-picker.tsx 移除直接 import @/modules/questions/actions,改为通过 useQuestionService() 注入;lesson-plan-provider-setup.tsx 自动注入默认 QuestionService", "V4-5": "P1-1 FocusTrap + ESC:4 个 dialog/drawer(publish-homework-dialog/knowledge-point-picker/version-history-drawer/question-bank-picker)包裹 FocusTrap(shared/components/a11y/focus-trap.tsx)+ 添加 ESC 键监听关闭", "V4-6": "P1-2 骨架屏替换加载文本:3 个 picker/drawer 的 {loading ?

{t('loading')}

} 替换为 //", "V4-7": "P1-3 ErrorBoundary 独立包裹:9 处子组件渲染(NodeEditor/VersionHistoryDrawer/QuestionBankPicker/InlineQuestionEditor/PublishHomeworkDialog/KnowledgePointPicker)独立用 包裹,避免单个区块错误导致整个编辑器崩溃", "V4-8": "P2-1 并行查询:3 个 view 页(teacher/admin/student/parent lesson-plans/[planId]/view)的 getTextbookById + getChaptersByTextbookId 改为 Promise.all 并行", "V4-9": "P2-2 notFound() 统一:3 个 view 页 if (!plan) return
...
改为 notFound(),与 Next.js 约定一致", "V4-10": "P2-3 async IIFE 替代 Promise 链:3 个组件(version-history-drawer/knowledge-point-picker/question-bank-picker)的 Promise.resolve().then()/queueMicrotask 改为 useEffect + async IIFE + cancelled flag,避免竞态", "V4-11": "P2-4 废弃文件删除:components/block-renderer.tsx(@deprecated,无源码引用)删除", "V4-12": "P2-5 anchor-injector regex 一致性:markdownToPlainText 的 /^>\\s+/gm 改为 /^\\s*>\\s+/gm;skipPatterns 5 个 regex 统一添加 g 标志,与 markdownToPlainText 保持一致", "V4-13": "P2-6 Zustand store 拆 slice:hooks/use-lesson-plan-editor.ts 从 303 行单体 store 拆分为 3 个独立 slice——hooks/editor-slice.ts(文档结构+文档操作)/hooks/selection-slice.ts(选中状态)/hooks/version-slice.ts(版本与保存状态);主文件重写为 25 行薄层,组合 3 个 slice 并导出 EditorState 类型", "V4-14": "type-guards 导出补全:lib/type-guards.ts 的 VALID_QUESTION_TYPES 常量从模块私有改为 export(供 inline-question-editor.tsx 复用,消除重复定义)", "V5-1": "自动保存失败 UI 兜底:version-slice.ts 新增 saveError/isOnline 状态 + setSaveError/setOnline 方法;lesson-plan-editor.tsx 自动保存失败时 toast 提示 + 设置 saveError,断网时不触发保存请求,监听 window online/offline 事件 toast 提示,saveError 时显示重试按钮,恢复时提示已恢复", "V5-2": "撤销/重做:新增 hooks/history-slice.ts(past/future 栈,50 步上限,canUndo/canRedo/pushHistory/undo/redo/clearHistory 方法);editor-slice.ts 所有 mutation 方法(setTitle/addNode/updateNode/removeNode/updateTextbookContent/addAnchor/removeAnchor/updateAnchor/connect/disconnect/setEdges)包装 pushHistory,updateNodePosition 除外(拖拽过程中由调用方在 onDragStart 时推入);version-slice hydrate/replaceDoc 调用 clearHistory;lesson-plan-editor.tsx 顶部新增 Undo/Redo 按钮 + Ctrl/Cmd+Z 撤销 + Ctrl/Cmd+Shift+Z 或 Ctrl/Cmd+Y 重做快捷键", "V5-3": "发布前作业单预览:publish-homework-dialog.tsx 改为 3 步流程(step=select 选班级+时间 → step=preview 预览题目+总分+班级数 → step=confirm 确认发布),第二步显示题目列表(题型/来源/题干预览/分值)+汇总信息(班级数/题目数/总分),第三步显示最终确认信息+警告文案;exercise-block.tsx 调用 PublishHomeworkDialog 时传入 items={data.items}", "V5-4": "导出/打印:新增 lib/export.ts(flattenLessonPlanForPrint 函数,将画布式 LessonPlanDocument 扁平化为 PrintableLessonPlan 线性结构,支持 detailed/concise 两种 variant,concise 仅含 objective/new_teaching/exercise/homework)+ components/print-view.tsx(打印视图组件,详细版/简洁版切换 + 调用 window.print() 通过浏览器原生能力保存为 PDF + print: CSS 媒体查询隐藏工具栏 + 页眉含教材/教师/班级/教学时长/最后保存时间 + break-inside-avoid 防止段落跨页);lesson-plan-editor.tsx 新增 printablePlan useMemo 构造 + 导出按钮 + PrintView 渲染", "V5-5": "多媒体嵌入 + 我的素材库:types.ts RichTextBlockData 新增 attachments?: RichTextAttachment[] 字段 + RichTextAttachment 类型(attachmentId/fileId/displayName/kind: image|audio|video|file/url/mimeType?);rich-text-block.tsx 集成 @tiptap/extension-image(Image.configure inline:false allowBase64:false)+ 新增附件入口按钮 + 已嵌入附件列表渲染(非图片附件列表 + 图片附件缩略图,含删除按钮)+ 图片附件通过 editor.commands.setImage 直接插入富文本;新增 components/attachment-picker.tsx(素材库 picker,复用 use-file-upload hook 调用 /api/upload + 调用 service.createLessonPlanAttachment 落库到 M6 lessonPlanAttachments 表 + 列出 service.getLessonPlanAttachments + 删除 service.deleteLessonPlanAttachment + inferAttachmentKind 工具函数);LessonPlanDataService 接口新增 getLessonPlanAttachments/createLessonPlanAttachment/deleteLessonPlanAttachment 三个方法 + LessonPlanAttachmentOption 导出类型;default-data-service.ts 实现三个方法(DB 层 Date 转 ISO string 以匹配接口类型);BlockRenderProps 新增 planId? 字段;NodeEditPanel 新增 planId prop 透传;lesson-plan-editor.tsx NodeEditPanel 调用传入 planId;新增依赖 @tiptap/extension-image;files/types.ts FileTargetType 新增 'lesson_plan' 类型", "V4-PAPER-1": "H1 注册 interaction block:config/block-registry.tsx 新增 interaction case,导入 InteractionBlock 组件 + isInteractionBlockData 类型守卫;BLOCK_REGISTRY 支持 12 种 Block 类型(含 V4 interaction)", "V4-PAPER-2": "H2 三栏布局重构:lesson-plan-editor.tsx 移除 NodeEditor/NodeEditPanel 导入与 JSX,改用 StructureTree + PaperEditor + DetailPanel 三栏布局(grid-template-columns: 260px 1fr 380px),保留顶部工具栏(标题/保存/撤销重做/发布/导出/安排课时/一致性校验/AI 反馈/AI 差异化)+ 5 个对话框(VersionHistoryDrawer/ConsistencyCheckDialog/AiFeedbackDialog/AiDifferentiationDialog/PrintView/ScheduleDialog);删除 components/node-edit-panel.tsx + app/(dashboard)/teacher/lesson-plans/[planId]/edit/ai-content-generator-slot.tsx(依赖 NodeEditPanel 类型)", "V4-PAPER-3": "I1 v3 锚点失效 banner:新增 components/anchor-migration-banner.tsx,检测 doc.anchors 中 invalid:true 或含 start 字段的旧锚点并提示用户;通过 key={planId} 实现 planId 变化时重新挂载内部组件,使用 lazy initial state 读取 localStorage(dismiss 状态按 planId 隔离),避免 useEffect 中 setState(符合 V4 react-hooks/set-state-in-effect lint 规则)", "V4-PAPER-4": "J1 删除废弃文件:删除 8 个 React Flow 画布和字符串锚点相关文件——components/node-editor.tsx、components/nodes/lesson-node.tsx、components/nodes/textbook-content-node.tsx、components/nodes/textbook-segments.tsx、components/nodes/anchor-node-selector.tsx、lib/anchor-injector.ts、lib/rf-mappers.ts、lib/auto-layout.ts;lesson-plan-readonly-view.tsx 重写为委托 LessonPlanMobileView(线性卡片视图)渲染所有视口,移除 React Flow 依赖", "V4-PAPER-5": "J2 PrintView 适配 V4:lib/export.ts 的 flattenBlockData switch 新增 interaction case + flattenInteraction 函数(输出设计意图 + 师生对话回合 + 预期回答);export.ts 已使用 nodes.sort((a, b) => a.order - b.order) 按顺序扁平化,无画布依赖;docstring 更新为 12 种 Block(含 V4 interaction)", "V4-PAPER-6": "J3 lint + tsc 零错误验证:tsc --noEmit 0 错误;lint 中 V4 引入的 react-hooks/set-state-in-effect 错误已通过 anchor-migration-banner.tsx 的 key-based remount + lazy initial state 模式修复;剩余 6 个 lint 错误为预存无关模块问题(exams/use-exam-preview-tasks react-hooks/refs 3 个、template-picker V5-9 set-state-in-effect 1 个),按计划要求记录不修复", "V4-PAPER-7": "J4 架构图同步:004_architecture_impact_map.md 新增 V4 纸感重构章节;005_architecture_data.json 更新 lesson_preparation 节点——description 改为 V4 描述、dependencies 移除 @xyflow/react 改为 @tiptap/* 包、files 数组移除 9 个废弃文件并新增 V4 组件(paper-editor/structure-tree/detail-panel/interaction-block/anchor-migration-banner/print-view/schedule-dialog/consistency-check-dialog/ai-feedback-dialog/ai-differentiation-dialog/expanded-slice/history-slice/anchor-mark/export/scope-check/document-diff 等)、auditFixes 新增 V4-PAPER-1~7 条目", "V4-PAPER-8": "J5 known-issues V4 规则:docs/troubleshooting/known-issues.md 新增 V4 纸感重构规则表,记录 8 条规则——React Flow 弃用、Tiptap Mark 锚点替代字符串偏移、setState in effect 禁用(用 key-based remount + lazy initial state 替代)、doc.version=4、editor-slice 移除画布相关方法、interaction block 注册、PrintView 按 order 排序、AnchorMigrationBanner 失效提示", "V4-PAPER-DOC": "documentVersion 升级为 4:types.ts LessonPlanDocument.version = 4;lib/document-migration.ts normalizeDocument 兼容 v1/v2/v3/v4 链式迁移;editor-slice.ts 移除 updateNodePosition/connect/setEdges/autoLayout 画布相关方法;hooks/expanded-slice.ts 新增 expandedNodeIds/toggleExpand/setExpanded 状态(节点展开到正文流);lib/anchor-mark.ts 新增 Tiptap AnchorMark + AnchorPoint 扩展(替代 v3 字符串偏移锚点)", "V4-PAPER-V2-1": "V2 复制节点功能完整实现:hooks/editor-slice.ts 新增 duplicateNode(id) 方法(深拷贝 BlockData + 新 id + 标题加'副本' + order 置末,interaction 节点的 turns 重新生成 id 避免冲突);components/paper-editor/paper-context-menu.tsx 的 copyNode 从 toast 占位改为调用 duplicateNode 并 toast 成功提示;i18n 新增 v4.contextMenu.copied/copyFailed 键(zh-CN + en)", "V4-PAPER-V2-2": "V2 节点级 AI 协助 4 项完整实现:lib/ai-node-assist.ts 新增 4 个服务函数(generateNodeContent/optimizeNodeExpression/suggestNodeDifferentiation/generateLayeredQuestions),全部纯服务端、Zod 校验、失败返回 null;actions-ai.ts 新增 4 个 Server Action,每个 action 校验 LESSON_PLAN_READ + LESSON_PLAN_CREATE + AI_CHAT 三个权限点;hooks/use-node-ai-assist.ts 新增客户端 hook;components/paper-editor/paper-editor.tsx 注入 onAiAction 回调;components/detail-panel/detail-panel.tsx 的 4 个 AiButton 接入真实 action;i18n 新增 v4.contextMenu.aiRunning/aiSuccess/aiFailed/aiComingSoon 键", "V4-I18N-1": "i18n 完整审查与修复:P0 修复 version.diff.* 3 键 + schema.ts 8 个 error.* 键 + contextMenu 6 键(zh-CN + en 同步);P1 修复 en calendar 节键名对齐 zh-CN;P2 修复 7 处硬编码中文 + 新增 6 个 i18n 键;清理 7 个 dead 节(review/comment/formative/substitute/evaluation/standards/gradeHead,analytics 因被 admin/curriculum-map 引用保留);lib/i18n-errors.ts 新增 t.has() 守卫", "TYPE-GUARD-1": "类型守卫专项重构(2026-07-04):lib/type-guards.ts 所有 isXxxBlockData 守卫参数类型从 BlockData 放宽到 unknown(兼容 BlockDataGuard 协变签名)+ 新增 mergeBlockDataPatch(node, patch) 函数与 BLOCK_DATA_GUARDS: Record 注册表,替代 `{ ...node.data, ...patch } as BlockData` 断言;lib/export.ts 14 处 as 断言替换为对应 isXxxBlockData 守卫;data-access-calendar.ts/data-access-review.ts 新增 toLessonPlanStatus 辅助函数替代 11 处 `as LessonPlanStatus` 断言;hooks/editor-slice.ts updateNode 显式标注 AnyLessonPlanNode 返回类型 + TextbookContentNode 分支使用 unknown 中间变量;hooks/use-node-ai-assist.ts 4 处 `as BlockData` 替换为 mergeBlockDataPatch + isInteractionBlockData 守卫;lib/ai-node-assist.ts NodeContentUpdate.data 类型从 Partial 改为 Record(诚实反映 AI 输出 untrusted),2 处 as Partial + 1 处 as InteractionBlockData 替换为守卫" } }, "error-book": { "path": "src/modules/error-book", "description": "错题本模块:自动采集考试/作业错题,SM-2 间隔重复算法复习,知识点薄弱度分析,支持学生/教师/家长/管理员多角色视图", "exports": { "actions": [ { "name": "getErrorBookItemsAction", "permission": "ERROR_BOOK_READ", "signature": "(params: GetErrorBookItemsParams) => Promise>", "purpose": "查询错题本列表", "deps": [ "requirePermission", "data-access.getErrorBookItems" ], "usedBy": [ "student/error-book/page.tsx" ] }, { "name": "getErrorBookItemDetailAction", "permission": "ERROR_BOOK_READ", "signature": "(itemId: string) => Promise>", "purpose": "查询错题详情(含复习历史)", "deps": [ "requirePermission", "data-access.getErrorBookItemById" ], "usedBy": [ "error-book-detail-dialog.tsx" ] }, { "name": "getErrorBookStatsAction", "permission": "ERROR_BOOK_READ", "signature": "() => Promise>", "purpose": "查询错题本统计", "deps": [ "requirePermission", "data-access.getErrorBookStats" ], "usedBy": [ "student/error-book/page.tsx", "parent/error-book/page.tsx" ] }, { "name": "createErrorBookItemAction", "permission": "ERROR_BOOK_MANAGE", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "purpose": "手动添加错题", "deps": [ "requirePermission", "data-access.createErrorBookItem" ], "usedBy": [ "add-error-book-dialog.tsx" ] }, { "name": "updateErrorBookNoteAction", "permission": "ERROR_BOOK_MANAGE", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "purpose": "更新错题笔记/标签", "deps": [ "requirePermission", "data-access.updateErrorBookNote" ], "usedBy": [ "error-book-detail-dialog.tsx" ] }, { "name": "reviewErrorBookItemAction", "permission": "ERROR_BOOK_MANAGE", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "purpose": "记录复习结果(SM-2 算法)", "deps": [ "requirePermission", "data-access.recordReview" ], "usedBy": [ "review-buttons.tsx" ] }, { "name": "deleteErrorBookItemAction", "permission": "ERROR_BOOK_MANAGE", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "purpose": "删除错题", "deps": [ "requirePermission", "data-access.deleteErrorBookItem" ], "usedBy": [ "error-book-item-card.tsx" ] }, { "name": "archiveErrorBookItemAction", "permission": "ERROR_BOOK_MANAGE", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "purpose": "归档错题", "deps": [ "requirePermission", "data-access.archiveErrorBookItem" ], "usedBy": [ "error-book-item-card.tsx" ] }, { "name": "collectFromSubmissionAction", "permission": "ERROR_BOOK_MANAGE", "signature": "(prevState: ActionState | undefined, formData: FormData) => Promise>", "purpose": "从考试/作业提交自动采集错题", "deps": [ "requirePermission", "data-access.collectFromExamSubmission", "data-access.collectFromHomeworkSubmission" ], "usedBy": [ "exam-homework 提交后调用" ] } ], "dataAccess": [ { "name": "getErrorBookItems", "signature": "(params: GetErrorBookItemsParams) => Promise", "type": "cache function", "purpose": "查询错题本列表(缓存)", "usedBy": [ "getErrorBookItemsAction", "student/error-book/page.tsx" ] }, { "name": "getErrorBookItemById", "signature": "(itemId: string, studentId: string) => Promise", "type": "cache function", "purpose": "查询错题详情(含复习历史)", "usedBy": [ "getErrorBookItemDetailAction" ] }, { "name": "getErrorBookStats", "signature": "(studentId: string) => Promise", "type": "cache function", "purpose": "查询错题本统计(总数/待复习/已掌握等)", "usedBy": [ "getErrorBookStatsAction", "student/error-book/page.tsx", "parent/error-book/page.tsx" ] }, { "name": "createErrorBookItem", "signature": "(studentId: string, input: CreateErrorBookItemInput) => Promise", "type": "async function", "purpose": "手动添加错题", "usedBy": [ "createErrorBookItemAction" ] }, { "name": "updateErrorBookNote", "signature": "(itemId: string, studentId: string, input: { note?: string; errorTags?: string[] }) => Promise", "type": "async function", "purpose": "更新错题笔记/标签", "usedBy": [ "updateErrorBookNoteAction" ] }, { "name": "recordReview", "signature": "(itemId: string, studentId: string, result: ErrorBookReviewResult) => Promise", "type": "async function", "purpose": "记录复习结果并应用 SM-2 算法更新间隔/掌握度", "usedBy": [ "reviewErrorBookItemAction" ] }, { "name": "deleteErrorBookItem", "signature": "(itemId: string, studentId: string) => Promise", "type": "async function", "purpose": "删除错题", "usedBy": [ "deleteErrorBookItemAction" ] }, { "name": "archiveErrorBookItem", "signature": "(itemId: string, studentId: string) => Promise", "type": "async function", "purpose": "归档错题", "usedBy": [ "archiveErrorBookItemAction" ] }, { "name": "collectFromExamSubmission", "signature": "(submissionId: string, studentId: string) => Promise", "type": "async function", "purpose": "从考试提交自动采集错题(去重)", "usedBy": [ "collectFromSubmissionAction" ] }, { "name": "collectFromHomeworkSubmission", "signature": "(submissionId: string, studentId: string) => Promise", "type": "async function", "purpose": "从作业提交自动采集错题(去重)", "usedBy": [ "collectFromSubmissionAction" ] }, { "name": "getStudentErrorBookSummaries", "signature": "(studentIds: string[], subjectId?: string | null) => Promise", "type": "async function", "purpose": "查询多个学生的错题统计(教师/管理员视图),支持按学科过滤,并附加 classId/className", "usedBy": [ "teacher/error-book/page.tsx", "admin/error-book/page.tsx" ] }, { "name": "getTopWrongQuestionsByStudentIds", "signature": "(studentIds: string[], limit?: number, subjectId?: string | null) => Promise", "type": "async function", "purpose": "查询高频错题(教师/家长视图),支持按学科过滤", "usedBy": [ "teacher/error-book/page.tsx", "parent/error-book/page.tsx" ] }, { "name": "getKnowledgePointWeakness", "signature": "(studentIds: string[], limit?: number, subjectId?: string | null) => Promise", "type": "async function", "purpose": "查询知识点薄弱度统计,JOIN chapters 表附加 chapterId/chapterTitle,支持按学科过滤", "usedBy": [ "teacher/error-book/page.tsx", "parent/error-book/page.tsx", "admin/error-book/page.tsx" ] }, { "name": "getChapterWeakness", "signature": "(studentIds: string[], limit?: number, subjectId?: string | null) => Promise", "type": "async function", "purpose": "查询章节薄弱度统计(哪些课在错),聚合 knowledgePoints.chapterId → chapters,每章返回 Top 3 薄弱知识点", "usedBy": [ "teacher/error-book/page.tsx", "admin/error-book/page.tsx" ] }, { "name": "getClassErrorOverviews", "signature": "(classIds: string[], subjectId?: string | null) => Promise", "type": "async function", "purpose": "查询班级错题概览(按班级分组统计:学生数/错题总数/平均掌握率/待复习数),支持按学科过滤", "usedBy": [ "teacher/error-book/page.tsx" ] }, { "name": "getSubjectErrorOverviews", "signature": "(studentIds: string[]) => Promise", "type": "async function", "purpose": "查询学科错题概览(按学科分组统计,用于学科 Tab 展示)", "usedBy": [ "teacher/error-book/page.tsx", "admin/error-book/page.tsx" ] }, { "name": "getSubjectErrorDistribution", "signature": "(studentIds: string[]) => Promise", "type": "async function", "purpose": "查询学科错题分布", "usedBy": [ "teacher/error-book/page.tsx", "admin/error-book/page.tsx" ] }, { "name": "getStudentNameMap", "signature": "(studentIds: string[]) => Promise>", "type": "async function", "purpose": "查询学生姓名映射", "usedBy": [ "teacher/error-book/page.tsx", "parent/error-book/page.tsx" ] }, { "name": "getStudentIdsByClassIdList", "signature": "(classIds: string[]) => Promise", "type": "async function", "purpose": "按班级 ID 查询学生 ID 列表(委托给 classes 模块)", "usedBy": [ "teacher/error-book/page.tsx", "admin/error-book/page.tsx" ] } ], "schema": [ { "name": "CreateErrorBookItemSchema", "type": "const", "description": "zod schema 手动添加错题", "usedBy": [ "createErrorBookItemAction" ] }, { "name": "UpdateErrorBookNoteSchema", "type": "const", "description": "zod schema 更新笔记/标签", "usedBy": [ "updateErrorBookNoteAction" ] }, { "name": "ReviewErrorBookItemSchema", "type": "const", "description": "zod schema 复习结果", "usedBy": [ "reviewErrorBookItemAction" ] }, { "name": "CollectFromSubmissionSchema", "type": "const", "description": "zod schema 自动采集", "usedBy": [ "collectFromSubmissionAction" ] } ], "types": [ { "name": "ErrorBookItem", "definition": "{ id, studentId, questionId, sourceType, sourceId, studentAnswer, correctAnswer, subjectId, knowledgePointIds, status, masteryLevel, nextReviewAt, reviewInterval, reviewCount, correctStreak, note, errorTags, createdAt, updatedAt, question?, subjectName? }", "usedBy": [ "error-book-list", "error-book-item-card" ] }, { "name": "ErrorBookItemDetail", "definition": "ErrorBookItem & { reviews: ErrorBookReviewRecord[] }", "usedBy": [ "error-book-detail-dialog" ] }, { "name": "ErrorBookStats", "definition": "{ totalCount, newCount, learningCount, masteredCount, archivedCount, dueReviewCount, masteredRate }", "usedBy": [ "error-book-stats-cards" ] }, { "name": "StudentErrorBookSummary", "definition": "{ studentId, studentName?, totalCount, newCount, learningCount, masteredCount, dueReviewCount, masteredRate, lastActivityAt, classId?, className? }", "usedBy": [ "grouped-student-error-table" ] }, { "name": "KnowledgePointWeakness", "definition": "{ knowledgePointId, knowledgePointName, errorCount, masteredCount, totalCount, masteryRate, chapterId?, chapterTitle? }", "usedBy": [ "knowledge-point-weakness-chart" ] }, { "name": "ChapterWeakness", "definition": "{ chapterId, chapterTitle, errorCount, masteredCount, knowledgePointCount, masteryRate, topKnowledgePoints: Array<{ knowledgePointId, knowledgePointName, errorCount }> }", "usedBy": [ "chapter-weakness-chart" ] }, { "name": "ClassErrorOverview", "definition": "{ classId, className, studentCount, totalErrorItems, averageErrorPerStudent, averageMasteryRate, dueReviewCount }", "usedBy": [ "class-error-bar-chart", "class-filter" ] }, { "name": "SubjectErrorOverview", "definition": "{ subjectId, subjectName, totalErrorItems, studentCount, averageMasteryRate, dueReviewCount }", "usedBy": [ "subject-tabs" ] }, { "name": "SubjectErrorDistribution", "definition": "{ subjectId, subjectName, errorCount, masteredCount, masteryRate }", "usedBy": [ "subject-distribution-chart" ] } ], "components": [ { "name": "ErrorBookStatsCards", "file": "error-book-stats-cards.tsx", "purpose": "错题统计卡片(5 个指标)" }, { "name": "AnalyticsStatsCards", "file": "analytics-stats-cards.tsx", "purpose": "教师/管理员分析页统计卡片(覆盖学生/错题总数/平均掌握率/待复习/涉及知识点,5 个彩色图标卡片)" }, { "name": "SubjectTabs", "file": "subject-tabs.tsx", "purpose": "学科筛选 Tab(Pill 风格按钮,URL 参数 subject,显示每学科错题数徽标)" }, { "name": "ClassFilter", "file": "class-filter.tsx", "purpose": "班级筛选器(Pill 风格按钮,URL 参数 classId,显示班级错题数和待复习数)" }, { "name": "ClassErrorBarChart", "file": "class-error-bar-chart.tsx", "purpose": "班级错题柱状图(recharts BarChart,自定义 tooltip 显示学生数/人均错题/掌握率/待复习)" }, { "name": "SubjectDistributionChart", "file": "subject-distribution-chart.tsx", "purpose": "学科错题分布柱状图(recharts BarChart,管理员视图)" }, { "name": "KnowledgePointWeaknessChart", "file": "knowledge-point-weakness-chart.tsx", "purpose": "知识点薄弱度水平柱状图(recharts layout=vertical,按掌握率着色:<30% rose / <60% amber / ≥60% emerald,显示章节归属)" }, { "name": "ChapterWeaknessChart", "file": "chapter-weakness-chart.tsx", "purpose": "章节薄弱度水平柱状图(哪些课在错,recharts layout=vertical,按掌握率着色,详情展示每章 Top 3 薄弱知识点徽标)" }, { "name": "GroupedStudentErrorTable", "file": "grouped-student-error-table.tsx", "purpose": "按班级分组的学生错题表格(可折叠展开,每生显示总数/新增/学习中/已掌握/待复习 + 掌握率进度条)" }, { "name": "ErrorBookFilters", "file": "error-book-filters.tsx", "purpose": "筛选栏(搜索/状态/来源/待复习)" }, { "name": "ErrorBookItemCard", "file": "error-book-item-card.tsx", "purpose": "错题卡片(预览/标签/笔记/掌握度)" }, { "name": "ReviewButtons", "file": "review-buttons.tsx", "purpose": "复习按钮组(again/hard/good/easy)" }, { "name": "ErrorBookDetailDialog", "file": "error-book-detail-dialog.tsx", "purpose": "错题详情对话框(题目/答案/复习/笔记/历史)。✅ 2026-06-24 i18n + 解耦:移除直接 import AiErrorBookAnalysis/createPracticeSessionAction,改为 aiAnalysisSlot/onStartVariantPractice props 注入;移除 as 断言", "props": { "aiAnalysisSlot": "React.ReactNode(可选,AI 分析区域插槽,未提供则不渲染)", "onStartVariantPractice": "() => void(可选,变式练习回调,未提供则不渲染)" } }, { "name": "ErrorBookList", "file": "error-book-list.tsx", "purpose": "错题列表(网格布局)。✅ 2026-06-24 i18n + 解耦:新增 aiAnalysisSlot/onStartVariantPractice props,透传至 ErrorBookItemCard/ErrorBookDetailDialog", "props": { "aiAnalysisSlot": "(item: ErrorBookItem) => React.ReactNode(可选,按错题条目渲染 AI 分析插槽,未提供则不渲染)", "onStartVariantPractice": "(item: ErrorBookItem) => void(可选,变式练习回调,未提供则不渲染)" } }, { "name": "AddErrorBookDialog", "file": "add-error-book-dialog.tsx", "purpose": "手动添加错题对话框(题库选择)。✅ 2026-06-24 i18n + 解耦:移除直接 import getQuestionsAction,改为 questionOptions/onLoadQuestions props 注入;移除 as 断言", "props": { "questionOptions": "Array<{ id: string; preview: string }>(可选,预加载题目选项)", "onLoadQuestions": "() => Promise(可选,懒加载题目选项回调)" } }, { "name": "TopWrongQuestions", "file": "top-wrong-questions.tsx", "purpose": "高频错题列表(Top 10)" } ] }, "files": [ { "path": "data-access.ts", "description": "学生端 CRUD(10 个函数)+ 共享工具函数(buildStudentErrorWhereClause/toStatus/toStringArray)+ re-export 采集函数。✅ 2026-06-24 拆分:分析函数迁移至 data-access-analytics.ts" }, { "path": "data-access-analytics.ts", "description": "✅ 2026-06-24 新增:教师/管理员分析查询(9 个函数:getStudentErrorBookSummaries/getTopWrongQuestionsByStudentIds/getKnowledgePointWeakness/getSubjectErrorDistribution/getChapterWeakness/getClassErrorOverviews/getSubjectErrorOverviews/getStudentNameMap/getStudentIdsByClassIdList/getAllStudentIds),SQL 聚合优化,getAllStudentIds 使用 ROLE_NAMES.STUDENT" }, { "path": "data-access-collection.ts", "description": "自动采集逻辑(从考试/作业提交采集错题,通过跨模块接口避免直查多表)" } ], "dependencies": { "dependsOn": [ "shared/db", "shared/lib/auth-guard", "shared/types/permissions", "modules/classes" ], "usedBy": [ "app/(dashboard)/student/error-book", "app/(dashboard)/teacher/error-book", "app/(dashboard)/parent/error-book", "app/(dashboard)/admin/error-book" ], "decoupledNotes": "✅ 2026-06-24 i18n + 解耦改造:error-book-detail-dialog 移除直接 import AiErrorBookAnalysis(modules/ai) 和 createPracticeSessionAction(modules/adaptive-practice),改为 aiAnalysisSlot/onStartVariantPractice props 注入;add-error-book-dialog 移除直接 import getQuestionsAction(modules/questions),改为 questionOptions/onLoadQuestions props 注入。跨模块依赖上移至 app/(dashboard)/student/error-book/page.tsx" }, "algorithms": { "sm2": { "name": "SM-2 间隔重复算法(简化版)", "file": "sm2-algorithm.ts", "description": "独立纯函数模块,4 级评级(again/hard/good/easy),间隔 1/2/4/7 天起,指数增长(×1.2/×1.5/×2),连续 3 次答对标记为已掌握。支持时间注入便于测试。", "functions": [ "calculateNewInterval", "calculateNewMastery", "deriveStatus", "calculateNextReviewAt", "calculateNewCorrectStreak", "calculateSm2Result" ], "constants": [ "REVIEW_INTERVALS", "INTERVAL_MULTIPLIERS", "MAX_MASTERY_LEVEL", "MIN_MASTERY_LEVEL", "MASTERED_REQUIRED_STREAK", "MASTERED_REQUIRED_MASTERY" ], "testFile": "sm2-algorithm.test.ts", "testCount": 39 } }, "dataScope": { "student": "owned(仅自己的错题)", "parent": "children(子女的错题)", "teacher": "class_taught(所教班级学生的错题)+ ctx.dataScope.subjectIds 预解析所教学科,默认按学科过滤", "admin": "all(所有错题)", "grade_head": "grade_managed(所管年级学生的错题)", "teaching_head": "grade_managed(所管年级学生的错题)" } }, "standards": { "path": "src/modules/standards", "description": "K12 课标管理(2026-07-01 新增,M1):课标树、按学科+年级筛选、课案与课标多对多关联", "exports": { "dataAccess": [ { "name": "getStandards", "file": "data-access.ts", "purpose": "查询课标列表(按学科+年级筛选)" }, { "name": "getStandardsTree", "file": "data-access.ts", "purpose": "查询课标树形结构" }, { "name": "getStandardById", "file": "data-access.ts", "purpose": "按 ID 获取课标" }, { "name": "createStandard", "file": "data-access.ts", "purpose": "创建课标" }, { "name": "updateStandard", "file": "data-access.ts", "purpose": "更新课标" }, { "name": "deactivateStandard", "file": "data-access.ts", "purpose": "停用课标" }, { "name": "searchStandards", "file": "data-access.ts", "purpose": "搜索课标" }, { "name": "getStandardsByPlanId", "file": "data-access.ts", "purpose": "按课案 ID 获取关联课标" }, { "name": "linkPlanToStandard", "file": "data-access.ts", "purpose": "课案关联课标" }, { "name": "unlinkPlanFromStandard", "file": "data-access.ts", "purpose": "取消课案与课标的关联" } ], "actions": [ { "name": "getStandardsAction", "permission": "STANDARD_READ", "file": "actions.ts", "purpose": "查询课标列表" }, { "name": "getStandardsTreeAction", "permission": "STANDARD_READ", "file": "actions.ts", "purpose": "查询课标树" }, { "name": "getStandardByIdAction", "permission": "STANDARD_READ", "file": "actions.ts", "purpose": "获取课标详情" }, { "name": "createStandardAction", "permission": "STANDARD_MANAGE", "file": "actions.ts", "purpose": "创建课标" }, { "name": "updateStandardAction", "permission": "STANDARD_MANAGE", "file": "actions.ts", "purpose": "更新课标" }, { "name": "deactivateStandardAction", "permission": "STANDARD_MANAGE", "file": "actions.ts", "purpose": "停用课标" }, { "name": "searchStandardsAction", "permission": "STANDARD_READ", "file": "actions.ts", "purpose": "搜索课标" }, { "name": "getStandardsByPlanIdAction", "permission": "STANDARD_READ", "file": "actions.ts", "purpose": "按课案获取关联课标" }, { "name": "linkPlanToStandardAction", "permission": "STANDARD_LINK", "file": "actions.ts", "purpose": "课案关联课标" }, { "name": "unlinkPlanFromStandardAction", "permission": "STANDARD_LINK", "file": "actions.ts", "purpose": "取消课案与课标的关联" } ] }, "permissions": [ "STANDARD_READ", "STANDARD_MANAGE", "STANDARD_LINK" ], "files": [ "types.ts", "schema.ts", "data-access.ts", "actions.ts" ] }, "search": { "path": "src/modules/search", "description": "✅ 2026-07-04 API 规范化重构新增:全文检索数据访问层。从 app/api/search/route.ts 下沉 4 个查询函数(searchQuestions/searchTextbooks/searchExams/searchAnnouncements),消除 app/ 层直接查询 DB 的架构违规。本模块仅负责 DB 查询,角色过滤(学生不能搜题目/考试)由路由层处理。", "exports": { "functions": [ { "name": "searchQuestions", "file": "data-access.ts", "signature": "searchQuestions(kw: string, limit?: number): Promise", "purpose": "题库检索:CAST(content AS CHAR) LIKE 模糊匹配 JSON 字段", "deps": [ "db", "questions" ], "usedBy": [ "api/search" ] }, { "name": "searchTextbooks", "file": "data-access.ts", "signature": "searchTextbooks(kw: string, limit?: number): Promise", "purpose": "教材检索:title/subject/publisher 三字段 OR 模糊匹配", "deps": [ "db", "textbooks" ], "usedBy": [ "api/search" ] }, { "name": "searchExams", "file": "data-access.ts", "signature": "searchExams(kw: string, limit?: number): Promise", "purpose": "试卷检索:title/description 模糊匹配", "deps": [ "db", "exams" ], "usedBy": [ "api/search" ] }, { "name": "searchAnnouncements", "file": "data-access.ts", "signature": "searchAnnouncements(kw: string, limit?: number): Promise", "purpose": "公告检索:仅返回 status=published 的公告,HTML 内容通过 stripHtml 提取纯文本摘要", "deps": [ "db", "announcements" ], "usedBy": [ "api/search" ] }, { "name": "DEFAULT_SEARCH_PAGE_SIZE", "file": "data-access.ts", "signature": "DEFAULT_SEARCH_PAGE_SIZE: number = 10", "purpose": "默认分页大小常量", "deps": [], "usedBy": [ "api/search" ] }, { "name": "isSearchType", "file": "types.ts", "signature": "isSearchType(v: string): v is SearchType", "purpose": "SearchType 类型守卫", "deps": [], "usedBy": [ "api/search" ] } ], "types": [ { "name": "SearchType", "file": "types.ts", "definition": "'all' | 'question' | 'textbook' | 'exam' | 'announcement'" }, { "name": "SearchResultItem", "file": "types.ts", "definition": "{ id: string; title: string; snippet: string; type: 'question' | 'textbook' | 'exam' | 'announcement'; href: string; createdAt: string }" }, { "name": "SearchResponse", "file": "types.ts", "definition": "{ success: boolean; query: string; type: SearchType; results: SearchResultItem[]; total: number; page: number; pageSize: number }" } ], "files": [ { "path": "data-access.ts", "lines": 212, "description": "4 个 search 函数 + 3 个内部纯函数(extractTextFromJson/stripHtml/truncate)+ DEFAULT_SEARCH_PAGE_SIZE 常量。server-only。每个查询函数 try/catch 容错降级,单表失败返回空数组。", "usedBy": [ "app/api/search/route.ts" ] }, { "path": "types.ts", "lines": 31, "description": "SearchType/SearchResultItem/SearchResponse 类型 + isSearchType 类型守卫", "usedBy": [ "data-access.ts", "app/api/search/route.ts" ] } ] }, "dbTables": [ "questions", "textbooks", "exams", "announcements" ] } }, "dbTables": { "_meta": { "total": 63, "orm": "Drizzle ORM 0.45", "database": "MySQL", "idStrategy": "CUID2 (varchar length 128)", "source": "src/shared/db/schema.ts" }, "usersAndAuth": { "description": "用户与认证 (Auth.js v5 + RBAC)", "tables": { "users": { "owner": "users", "description": "用户主表(含未成年人信息保护字段)" }, "accounts": { "owner": "auth", "description": "OAuth 账户" }, "sessions": { "owner": "auth", "description": "Auth.js 会话" }, "verificationTokens": { "owner": "auth", "description": "验证令牌" }, "roles": { "owner": "auth", "description": "角色(admin/teacher/student/parent/grade_head/teaching_head)" }, "usersToRoles": { "owner": "auth", "description": "用户-角色多对多" }, "rolePermissions": { "owner": "auth", "description": "角色-权限(细粒度 RBAC)" }, "passwordSecurity": { "owner": "auth", "description": "密码安全策略(失败次数/锁定/必须改密)" }, "invitationCodes": { "owner": "invitation-codes", "description": "邀请码(audit-P2-3 新增:code/email/role/classId/notes/createdAt/createdBy/expiresAt/usedBy/usedAt)" } } }, "knowledgeStructure": { "description": "知识点树结构", "tables": { "knowledgePoints": { "owner": "textbooks", "description": "知识点(树结构,parentId 自引用)" }, "knowledgePointPrerequisites": { "owner": "textbooks", "description": "知识点前置依赖(复合主键 knowledgePointId+prerequisiteKpId,双外键 cascade 删除)" } } }, "questionBank": { "description": "题库核心", "tables": { "questions": { "owner": "questions", "description": "题目(content JSON,支持复合题 parentId 自引用)" }, "questionsToKnowledgePoints": { "owner": "questions", "description": "题目-知识点多对多" } } }, "academicStructure": { "description": "教学结构", "tables": { "subjects": { "owner": "school", "description": "科目" }, "textbooks": { "owner": "textbooks", "description": "教材" }, "chapters": { "owner": "textbooks", "description": "章节(支持嵌套 parentId)" } } }, "schoolManagement": { "description": "学校管理", "tables": { "departments": { "owner": "school", "description": "部门" }, "classrooms": { "owner": "school", "description": "教室" }, "academicYears": { "owner": "school", "description": "学年" }, "schools": { "owner": "school", "description": "学校" }, "grades": { "owner": "school", "description": "年级(含 gradeHeadId/teachingHeadId)" } } }, "classes": { "description": "班级/选课/课表/邀请码(v3 新增邀请码体系,对标 Google Classroom / 钉钉教育)", "tables": { "classes": { "owner": "classes", "description": "班级(invitationCode 字段保留作为 fallback,v3 迁移至 classInvitationCodes 表)" }, "classSubjectTeachers": { "owner": "classes", "description": "班级-科目-教师" }, "classEnrollments": { "owner": "classes", "description": "班级选课(active/inactive)" }, "classSchedule": { "owner": "scheduling", "description": "课表(注意:三处写入口,见 knownIssues P0-6)" }, "classInvitationCodes": { "owner": "classes", "description": "v3 新增:班级邀请码(独立表,支持有效期/次数限制/审计/多码并存,6 位字母数字剔除歧义字符)" } } }, "exams": { "description": "考试与提交", "tables": { "exams": { "owner": "exams", "description": "考试(含 examMode: homework/timed/proctored)" }, "examQuestions": { "owner": "exams", "description": "考试-题目多对多" }, "examSubmissions": { "owner": "exams", "description": "考试提交(started/submitted/graded)" }, "submissionAnswers": { "owner": "exams", "description": "提交答案" } } }, "homework": { "description": "作业", "tables": { "homeworkAssignments": { "owner": "homework", "description": "作业(关联 sourceExamId)" }, "homeworkAssignmentQuestions": { "owner": "homework", "description": "作业-题目" }, "homeworkAssignmentTargets": { "owner": "homework", "description": "作业目标学生" }, "homeworkSubmissions": { "owner": "homework", "description": "作业提交(含 attemptNo/isLate)" }, "homeworkAnswers": { "owner": "homework", "description": "作业答案" } } }, "ai": { "description": "AI 配置", "tables": { "aiProviders": { "owner": "settings", "description": "AI Provider(zhipu/openai/gemini/custom,apiKey 加密)" } } }, "announcements": { "description": "公告", "tables": { "announcements": { "owner": "announcements", "description": "公告(school/grade/class,draft/published/archived;V2-P2-13d 新增 isPinned 置顶字段)" }, "announcementReads": { "owner": "announcements", "description": "公告已读回执(V2-P2-13d 新增,唯一索引 announcementId+userId 保证幂等,cascade 删除)" } } }, "auditLogs": { "description": "审计与登录日志", "tables": { "auditLogs": { "owner": "audit", "description": "审计日志(success/failure)" }, "loginLogs": { "owner": "audit", "description": "登录日志(signin/signout/signup)" }, "dataChangeLogs": { "owner": "audit", "description": "数据变更日志(create/update/delete)" } } }, "gradeRecords": { "description": "成绩录入", "tables": { "gradeRecords": { "owner": "grades", "description": "成绩记录(exam/quiz/homework/other)" }, "gradeRecordAnswers": { "owner": "grades", "description": "成绩记录-题目得分明细(2026-06-24 新增:按试卷录入时存储每题得分,gradeRecordId/questionId/score/fullScore/feedback,外键 cascade 删除,迁移 0010_grade_record_answers.sql)" }, "gradeDrafts": { "owner": "grades", "description": "成绩录入草稿(v3-P2 新增:跨设备恢复未保存成绩,userId+classId+subjectId+type 唯一键,content JSON 存储 scores,24h 过期。P3-6 新增列:lockedBy/lockedAt/lockToken 协同录入锁机制,LOCK_TTL_MS=5分钟,classSubjectLockIdx 索引支持跨用户冲突检测)" }, "gradeAppeals": { "owner": "grades", "description": "成绩申诉记录(P3-7 新增:学生对成绩提出异议,教师复核后修改并留痕。状态机 pending→approved/rejected/withdrawn,originalScore 快照+adjustedScore+reviewComment 审计轨迹,pendingLookupIdx 索引应用层校验唯一性)" } } }, "files": { "description": "文件附件", "tables": { "fileAttachments": { "owner": "files", "description": "文件附件(含 storagePath/url)" } } }, "coursePlans": { "description": "课程计划", "tables": { "coursePlans": { "owner": "course-plans", "description": "课程计划(planning/active/completed/paused)" }, "coursePlanItems": { "owner": "course-plans", "description": "课程计划项(按周)" } } }, "messaging": { "description": "消息与通知", "tables": { "messages": { "owner": "messaging", "description": "站内消息(含回复链 parentMessageId;软删除 senderDeletedAt/receiverDeletedAt;V2-P2-13c 新增 isStarred 星标字段 + messages_receiver_starred_idx 复合索引;P2-4 新增 recalledAt 撤回时间戳;P2-2 新增 groupMessageId 群组消息聚合键)" }, "messageDrafts": { "owner": "messaging", "description": "V2-P2-13c 新增:消息草稿(userId/receiverId/subject/content/parentMessageId + updatedAt/createdAt;含 message_drafts_user_idx 和 message_drafts_user_updated_idx 索引;P2-10 新增 deviceId(跨设备标识) + version(乐观锁版本号) + message_drafts_user_parent_idx 复合索引)" }, "messageTemplates": { "owner": "messaging", "description": "P2-6 快捷回复模板(userId/title/content/sortOrder + createdAt/updatedAt)" }, "messageReports": { "owner": "messaging", "description": "P2-5 消息举报(reporterId/messageId/reportedUserId/reason: spam|harassment|inappropriate|other/description/status: pending|reviewing|resolved|dismissed + createdAt/updatedAt;4 个索引:reporter_idx/message_idx/reported_user_idx/status_idx)" }, "userBlocks": { "owner": "messaging", "description": "P2-5 用户屏蔽(blockerId/blockedId/createdAt;uniqueIndex 防重复屏蔽 (blockerId, blockedId);blockerIdx 索引)" }, "messageNotifications": { "owner": "notifications", "description": "消息通知(type/title/content/link/isRead/priority/isArchived/createdAt;V2-P2-13b 新增 priority 和 isArchived 字段)" }, "notificationLogs": { "owner": "notifications", "description": "通知发送日志(channel/status/messageId/error/sentAt)" }, "notificationPreferences": { "owner": "notifications", "description": "通知偏好(email/sms/push + 分类开关)" } } }, "parentStudent": { "description": "家长-子女关联", "tables": { "parentStudentRelations": { "owner": "parent", "description": "家长-子女关系" } } }, "attendance": { "description": "考勤 (含 L-1/L-2/L-3/L-4/L-6/L-9)", "tables": { "attendanceRecords": { "owner": "attendance", "description": "考勤记录(present/absent/late/early_leave/excused; L-6 新增 period 字段(节次: full_day/morning_reading/morning/afternoon/evening))" }, "attendanceRules": { "owner": "attendance", "description": "考勤规则(迟到/早退阈值)" } } }, "leave-requests": { "description": "在线请假 (L-5)", "tables": { "leaveRequests": { "owner": "leave-requests", "description": "请假申请(sick/personal/family/other;pending/approved/rejected/cancelled;含 attendanceSynced 标志位防止重复同步)" } } }, "scheduling": { "description": "排课", "tables": { "schedulingRules": { "owner": "scheduling", "description": "排课规则(每日最大课时/连续课时/午休等)" }, "scheduleChanges": { "owner": "scheduling", "description": "调课/代课申请(pending/approved/rejected/completed)" } } }, "elective": { "description": "选课管理 (P2)", "tables": { "electiveCourses": { "owner": "elective", "description": "选修课程(draft/open/closed/cancelled,fcfs/lottery)" }, "courseSelections": { "owner": "elective", "description": "选课记录(selected/enrolled/waitlist/dropped/rejected)" } } }, "proctoring": { "description": "考试监考 (P2)", "tables": { "examProctoringEvents": { "owner": "proctoring", "description": "监考事件(tab_switch/window_blur/copy_attempt 等)" } } }, "diagnostic": { "description": "学情诊断 (P2)", "tables": { "knowledgePointMastery": { "owner": "diagnostic", "description": "知识点掌握度(学生-知识点)" }, "learningDiagnosticReports": { "owner": "diagnostic", "description": "学情诊断报告(individual/class/grade;v4-P1-4 新增 classId 字段关联班级;v4-P2-3 新增 gradeId 字段关联年级)" } } }, "lessonPreparation": { "description": "教师备课", "tables": { "lessonPlans": { "owner": "lesson-preparation", "description": "课案主表(Block JSON 内容,15 列/4 索引/5 外键)", "columns": 15, "indexes": 4, "foreignKeys": 5 }, "lessonPlanVersions": { "owner": "lesson-preparation", "description": "课案版本(手动/自动快照,8 列/2 索引/2 外键)", "columns": 8, "indexes": 2, "foreignKeys": 2 }, "lessonPlanTemplates": { "owner": "lesson-preparation", "description": "课案模板(系统/个人,8 列/1 索引/1 外键)", "columns": 8, "indexes": 1, "foreignKeys": 1 }, "systemSettings": { "owner": "settings", "description": "系统设置(键值对存储:category + key + value + valueType,P0-3 新增)", "columns": 7, "indexes": 2, "foreignKeys": 0 }, "lessonPlanComments": { "owner": "lesson-preparation", "description": "M2 课案评论(Block 级评论,resolved 状态,2026-07-01 新增)", "columns": 8, "indexes": 2, "foreignKeys": 2 }, "lessonPlanReviewRecords": { "owner": "lesson-preparation", "description": "M3 审核工作流记录(draft→submitted→approved/rejected→published,2026-07-01 新增)", "columns": 6, "indexes": 2, "foreignKeys": 1 }, "lessonPlanFormativeItems": { "owner": "lesson-preparation", "description": "M5 形成性评价项(4 种互动类型,2026-07-01 新增)", "columns": 7, "indexes": 2, "foreignKeys": 1 }, "lessonPlanAttachments": { "owner": "lesson-preparation", "description": "M6 资源附件库(2026-07-01 新增)", "columns": 6, "indexes": 2, "foreignKeys": 1 }, "lessonPlanAiEvaluations": { "owner": "lesson-preparation", "description": "M8 AI 评估记录(2026-07-01 新增)", "columns": 7, "indexes": 2, "foreignKeys": 1 }, "lessonPlanSubstituteAssignments": { "owner": "lesson-preparation", "description": "M12 代课教师分配(2026-07-01 新增)", "columns": 6, "indexes": 2, "foreignKeys": 2 }, "lessonPlanSchedules": { "owner": "lesson-preparation", "description": "V5-7 W3/W4 课案绑定课时表(planId+classId+lessonDate 唯一约束,2026-07-03 新增)", "columns": 6, "indexes": 3, "foreignKeys": 2 } } }, "errorBook": { "description": "错题本(SM-2 间隔重复算法)", "tables": { "errorBookItems": { "owner": "error-book", "description": "错题条目主表(学生-题目-来源-状态-掌握度-SM2 字段-笔记-标签)", "columns": 18, "indexes": 4, "foreignKeys": 2 }, "errorBookReviews": { "owner": "error-book", "description": "复习记录(错题条目-学生-评级-新间隔-新掌握度-复习时间)", "columns": 8, "indexes": 2, "foreignKeys": 2 } } }, "adaptivePractice": { "description": "专项练习(错题变式/知识点专项/薄弱章节/AI 推荐,自动判分)", "tables": { "practiceSessions": { "owner": "adaptive-practice", "description": "练习会话表(学生 ID/类型/来源元数据/状态/题目统计/时间戳)", "columns": 12, "indexes": 3, "foreignKeys": 1 }, "practiceAnswers": { "owner": "adaptive-practice", "description": "练习答题记录表(会话-学生-题目-变式内容-顺序-状态-答案-判分-分数)", "columns": 13, "indexes": 4, "foreignKeys": 3 } } }, "onboarding": { "description": "首次登录引导(独立路由 /onboarding + Server Action + middleware 重定向,v3 对标 PowerSchool/Veracross/Auth0)", "tables": { "users": { "owner": "shared", "description": "读写 users.onboardedAt / name / phone / address / birthDate / phone(v3 家长绑定三因子验证)", "columns": "subset" }, "parentStudentRelations": { "owner": "shared", "description": "家长绑定子女(onboarding 中写入,支持多子女循环绑定)", "columns": "subset" }, "auditLogs": { "owner": "shared", "description": "v3 新增:onboarding 完成后写审计日志(含失败项明细)", "columns": "subset" } }, "exports": { "actions": [ { "name": "getOnboardingStatusAction", "file": "actions.ts", "purpose": "查询当前用户 onboarding 状态(Server Action)" }, { "name": "completeOnboardingAction", "file": "actions.ts", "purpose": "完成 onboarding(Server Action + requireAuth + Zod + db.transaction + logAudit,v3 新增幂等检查/局部错误收集/教师多科目循环绑定;audit-P1-8 新增家长绑定子女独立速率限制 onboarding:bind:{userId} 5次/小时,触发时写 onboarding.bind_rate_limited 审计日志)" } ], "dataAccess": [ { "name": "getOnboardingStatus", "file": "data-access.ts", "purpose": "读取 users.onboardedAt + usersToRoles" }, { "name": "updateUserProfile", "file": "data-access.ts", "purpose": "更新 users.name/phone/address" }, { "name": "bindParentToChild", "file": "data-access.ts", "purpose": "家长绑定子女(v3 三因子验证:邮箱+生日+手机号后4位,对标 PowerSchool Access ID+Password)" } ], "schema": [ { "name": "OnboardingSchema", "file": "schema.ts", "purpose": "Zod 校验:name/phone/address/classCodes/teacherSubjects/children[](v3 重构:children 数组替代单个 childEmail/childBindingCode,支持多子女)" } ], "types": [ { "name": "OnboardingRoleInfo / OnboardingStatus / OnboardingCompleteData", "file": "types.ts", "purpose": "类型定义" }, { "name": "OnboardingFailureItem", "file": "types.ts", "purpose": "v3 新增:局部失败项类型(班级码/子女绑定失败不回滚整个事务)" }, { "name": "BindParentToChildParams", "file": "types.ts", "purpose": "v3 新增:家长绑定子女输入参数(三因子验证)" }, { "name": "ChildRow / EMPTY_CHILD_ROW", "file": "types.ts", "purpose": "audit-P1-2 新增:家长 Onboarding 子女信息行类型与空行常量(从 onboarding-stepper.tsx 提取,便于 ParentChildrenForm 与 useOnboardingForm 复用)" }, { "name": "OnboardingStepId / OnboardingStepConfig / ONBOARDING_STEPS / getOnboardingStepsForRole", "file": "types.ts", "purpose": "audit-P1-13 新增:步骤配置驱动设计。OnboardingStepId 为 4 个步骤标识联合类型(roleConfirm/basicInfo/roleInfo/complete,同时作为 i18n 键后缀);OnboardingStepConfig 描述单步配置(id + applicableRoles 角色过滤 + canSkip 跳过按钮可见性函数);ONBOARDING_STEPS 为 4 步配置数组(roleInfo 仅对 teacher/student/parent 显示,admin 与 grade_head/teaching_head 跳过);getOnboardingStepsForRole(role) 纯函数按角色过滤步骤。消费方:useOnboardingForm 派生 stepConfigs/stepKeys/currentStepId/canSkip,onboarding-stepper 按 currentStepId 渲染" } ], "components": [ { "name": "OnboardingStepper", "file": "components/onboarding-stepper.tsx", "purpose": "客户端 stepper 容器(4 步,v3 新增:URL query 持久化步骤/家长多子女动态行/跳过机制明确化;audit-P1-2 重构:状态逻辑下沉至 useOnboardingForm Hook,本文件降至 259 行纯展示编排器;audit-P1-13 重构:按 currentStepId 渲染步骤内容替代数字下标 step===N,跳过按钮可见性由配置 canSkip 驱动)" }, { "name": "ParentChildrenForm", "file": "components/parent-children-form.tsx", "purpose": "audit-P1-2 新增:家长 Onboarding 第 2 步子女绑定子表单(纯受控组件,从原 onboarding-stepper.tsx 提取约 80 行,props: childRows/isSubmitting/onUpdate/onAdd/onRemove)" } ], "hooks": [ { "name": "useOnboardingForm", "file": "hooks/use-onboarding-form.ts", "purpose": "audit-P1-2 新增:Onboarding 表单状态与处理逻辑 Hook(从 onboarding-stepper.tsx 提取,含 step/name/phone/children 状态 + onNext/onBack/onSkip/onFinish 导航 + URL query 持久化 + completeOnboardingAction 调用;audit-P1-13 重构:步骤列表由 ONBOARDING_STEPS 配置驱动,派生 stepConfigs/stepKeys/currentStepId/canSkip 替代原 STEPS_KEYS 硬编码 + isAdmin 三元过滤;canNext/onNext 改用 currentStepId 判断步骤而非数字下标,避免角色过滤导致下标偏移)" }, { "name": "UseOnboardingFormReturn", "file": "hooks/use-onboarding-form.ts", "purpose": "audit-P1-2 新增:useOnboardingForm 返回值类型(便于组件 props 类型标注与测试)" } ] }, "routes": { "/onboarding": { "methods": [ "GET" ], "handler": "OnboardingPage(服务端组件,读 session.onboarded 决定渲染,v3 新增 Suspense 边界 + 骨架屏)", "auth": "required", "validation": "server-side redirect" } }, "securityNotes": [ "角色只读:不写 usersToRoles,角色由管理员预分配", "班级绑定:调用 modules/classes data-access 的 enrollStudentByInvitationCode / enrollTeacherByInvitationCode(含校验)", "事务化:completeOnboardingAction 用 db.transaction 包裹全部写入", "Zod 校验:OnboardingSchema 校验所有输入", "middleware 拦截:proxy.ts 读取 token.onboarded,未完成则重定向 /onboarding", "v3 P0-2 家长绑定三因子验证:邮箱+生日+手机号后4位(组合空间 3.65M,对标 PowerSchool)", "v3 P0-3 教师多科目循环绑定:修复 UI 多选但服务端只取第一个的 bug", "v3 P0-4 审计日志:onboarding 完成后写 audit_logs(含失败项明细)", "v3 P0-5 服务端幂等:开始时检查 users.onboardedAt,已完成直接返回成功", "v3 P1-1 URL query 持久化步骤:?step=N,刷新不丢步,支持浏览器前进后退", "v3 P1-2 局部错误收集:班级码/子女绑定失败不回滚整个事务,收集失败列表返回前端", "v3 P1-4 家长多子女绑定:动态多行 UI,支持一次绑定多个子女", "v3 P1-5 跳过机制明确化:parent 不可跳过子女绑定(核心功能)" ] }, "invitation-codes": { "path": "src/modules/invitation-codes", "description": "邀请码模块(audit-P2-3 新增):批量生成邀请码替代开放注册,支持预分配角色/班级/限定邮箱/过期时间;注册流程集成校验+消费(乐观锁);admin 管理页生成/列表/删除", "exports": { "actions": [ { "name": "generateInvitationCodesAction", "permission": "USER_MANAGE", "signature": "(input: unknown) => Promise>", "purpose": "批量生成邀请码(1-100 张/批),Zod 校验 + trackEvent invitation_codes.generated + revalidatePath,返回明文 codes 仅此一次", "deps": [ "data-access.generateInvitationCodes", "schema.GenerateInvitationCodesSchema" ] }, { "name": "listInvitationCodesAction", "permission": "USER_MANAGE", "signature": "() => Promise>", "purpose": "列出邀请码(含已使用+未使用,按 createdAt desc,最多 200 条)", "deps": [ "data-access.listInvitationCodes" ] }, { "name": "deleteInvitationCodeAction", "permission": "USER_MANAGE", "signature": "(codeId: string) => Promise>", "purpose": "删除邀请码(仅未使用,已使用保留作审计记录)+ trackEvent invitation_codes.deleted", "deps": [ "data-access.deleteInvitationCode" ] }, { "name": "validateInvitationCodeAction", "permission": "PUBLIC", "signature": "(code: string, email: string) => Promise>", "purpose": "校验邀请码(公开接口,注册流程预检用,不返回明文)", "deps": [ "data-access.validateInvitationCode" ] } ], "dataAccess": [ { "name": "generateInvitationCodes", "signature": "(input, createdBy) => Promise<{codes, batchId}>" }, { "name": "listInvitationCodes", "signature": "(limit?, includeUsed?) => Promise" }, { "name": "validateInvitationCode", "signature": "(code, email) => Promise" }, { "name": "consumeInvitationCode", "signature": "(input) => Promise", "notes": "乐观锁 UPDATE ... WHERE usedBy IS NULL + affectedRows 检查" }, { "name": "deleteInvitationCode", "signature": "(codeId) => Promise" }, { "name": "purgeExpiredInvitationCodes", "signature": "() => Promise", "notes": "cron 预留" } ], "lib": [ { "name": "generateInvitationCode", "signature": "() => string", "notes": "8 字符安全字母表(剔除 I/O/0/1)+ node:crypto.randomBytes" }, { "name": "generateUniqueInvitationCodes", "signature": "(count) => string[]", "notes": "Set 去重 + 最多 3× 重试" }, { "name": "generateBatchId", "signature": "() => string", "notes": "batch__" } ], "schema": [ { "name": "GenerateInvitationCodesSchema", "notes": "count 1-100, role enum, 可选 email/classId/expiresAt/notes" }, { "name": "InvitationCodeFieldSchema", "notes": "嵌入 RegisterSchema:8-64 字符大写字母数字" }, { "name": "INVITATION_ROLE_VALUES", "notes": "student/teacher/parent/admin const array" } ], "types": [ "InvitationRole", "InvitationCodeRecord", "GenerateInvitationCodesInput", "GenerateInvitationCodesResult", "ValidateInvitationCodeResult", "ConsumeInvitationCodeInput" ], "components": [ { "name": "InvitationCodesView", "type": "client", "purpose": "管理视图(统计卡片 + 表格 + 删除确认 + 生成对话框入口),接受 initialCodes + now props 避免客户端 Date.now()", "deps": [ "actions.deleteInvitationCodeAction", "GenerateInvitationCodesDialog" ] }, { "name": "GenerateInvitationCodesDialog", "type": "client", "purpose": "生成对话框(form/result 两态切换,result 视图仅此一次展示明文供复制)", "deps": [ "actions.generateInvitationCodesAction", "schema.INVITATION_ROLE_VALUES" ] } ] }, "dbTables": [ "invitationCodes" ], "i18n": { "namespace": "invitationCodes", "status": "implemented", "messageFiles": [ "shared/i18n/messages/zh-CN/invitation-codes.json", "shared/i18n/messages/en/invitation-codes.json" ] }, "routes": [ "/admin/invitation-codes" ], "securityNotes": [ "权限:admin 管理操作使用 USER_MANAGE 权限(复用用户管理权限,避免新增冗余权限点)", "validateInvitationCodeAction 为公开接口(注册流程预检用),不返回明文 code", "consumeInvitationCode 使用乐观锁 UPDATE ... WHERE usedBy IS NULL 防止并发消费", "已使用的邀请码不可删除(保留作审计记录),仅未使用或已过期可删除", "生成结果仅此一次返回明文 codes,列表/详情接口不返回明文", "代码生成使用 node:crypto.randomBytes(密码学安全),非 Math.random" ] }, "i18n": { "description": "v3 新增:项目国际化体系(next-intl 4.x,without i18n routing 模式,cookie 驱动)", "tables": {}, "exports": { "config": [ { "name": "getRequestConfig", "file": "src/i18n/request.ts", "purpose": "next-intl 请求配置:从 cookie 读取 locale,按命名空间加载字典" }, { "name": "setLocaleAction", "file": "src/i18n/actions.ts", "purpose": "Server Action:切换语言(写 cookie + revalidatePath)" } ], "shared": [ { "name": "LOCALES / Locale / DEFAULT_LOCALE / LOCALE_COOKIE", "file": "src/shared/i18n/locale.ts", "purpose": "locale 常量与工具函数" }, { "name": "LocaleSwitcher", "file": "src/shared/components/locale-switcher.tsx", "purpose": "语言切换组件(DropdownMenu + cookie 持久化)" } ], "messages": [ "src/shared/i18n/messages/zh-CN/{common,auth,onboarding,classes,errors,dashboard,examHomework,announcements,messages,notifications,settings,textbooks,grade,lessonPreparation,grades,diagnostic,attendance,elective,school,ai,practice,student,errorBook,audit,scheduling,coursePlans,files,users,leave,nav,rbac,questions,parent,invitationCodes}.json", "src/shared/i18n/messages/en/{common,auth,onboarding,classes,errors,dashboard,examHomework,announcements,messages,notifications,settings,textbooks,grade,lessonPreparation,grades,diagnostic,attendance,elective,school,ai,practice,student,errorBook,audit,scheduling,coursePlans,files,users,leave,nav,rbac,questions,parent,invitationCodes}.json" ] }, "routes": {}, "securityNotes": [ "不使用 URL 路由段,避免破坏现有 (auth)/(dashboard)/(onboarding) 路由组结构", "locale 通过 cookie 持久化,SSR 时由 getRequestConfig 读取", "不使用 Accept-Language 自动协商,避免 SSR 与客户端 hydration 不一致", "字典放在 shared/i18n/messages/,符合三层架构约束(shared 为被依赖方)", "NextIntlClientProvider 在根 layout 注入,服务端/客户端组件均可使用 useTranslations" ] } }, "dependencyMatrix": { "shared": { "dependsOn": [] }, "auth": { "dependsOn": [ "shared", "modules/auth", "modules/settings" ], "uses": { "shared": [ "db", "schema", "permissions", "lib.role-utils.normalizeRole", "lib.role-utils.resolvePrimaryRole", "lib.permissions.resolvePermissions", "lib.login-logger.logLoginEvent", "lib.password-policy", "lib.rate-limit", "lib.bcrypt-utils.normalizeBcryptHash", "lib.http-utils.resolveClientIp", "lib.password-security-service.getOrCreatePasswordSecurity", "lib.password-security-service.recordFailedLogin", "lib.password-security-service.resetFailedLogin", "lib.route-resolver.resolveDefaultPath (via proxy.ts)" ], "modules/auth": [ "services/two-factor-service.verifyTwoFactorForLogin (dynamic import)" ], "modules/settings": [ "data-access-two-factor.getTwoFactorEnabled", "data-access-two-factor.getTotpSecret", "data-access-two-factor.getBackupCodesHashed", "data-access-two-factor.setBackupCodesHashed", "lib/totp.verifyTotpCode", "lib/totp.verifyBackupCode", "lib/totp.consumeBackupCode" ] } }, "modules/auth": { "dependsOn": [ "shared", "modules/settings", "modules/invitation-codes" ], "uses": { "shared": [ "db", "db.schema.users", "lib.rate-limit", "lib.login-logger.logLoginEvent", "lib.bcrypt-utils", "lib.http-utils", "types.action-state" ], "modules/settings": [ "data-access-two-factor.getTwoFactorEnabled", "data-access-two-factor.getTotpSecret", "data-access-two-factor.getBackupCodesHashed", "data-access-two-factor.setBackupCodesHashed", "lib/totp.verifyTotpCode", "lib/totp.verifyBackupCode", "lib/totp.consumeBackupCode" ], "modules/invitation-codes": [ "data-access.validateInvitationCode", "data-access.consumeInvitationCode" ] } }, "exams": { "dependsOn": [ "shared", "auth", "questions", "classes" ], "uses": { "shared": [ "db", "auth-guard", "types", "ai" ], "auth": [ "auth" ], "questions": [ "data-access.createQuestionWithRelations" ], "classes": [ "data-access.getClassGradeIdsByClassIds" ] } }, "homework": { "dependsOn": [ "shared", "auth", "exams", "classes", "school", "users", "files", "notifications" ], "uses": { "shared": [ "db", "auth-guard", "types" ], "auth": [ "auth" ], "exams": [ "data-access.getExamIdsByGradeIds", "data-access.getExamSubjectIdMap", "data-access.getExamWithQuestionsForHomework" ], "classes": [ "data-access.getStudentIdsByClassId", "data-access.getStudentIdsByClassIds", "data-access.getActiveStudentIdsByClassId", "data-access.getTeacherSubjectIdsByClass", "data-access.getStudentActiveClassId", "data-access.getGradeIdsByClassIds", "data-access.getClassTeacherById" ], "school": [ "data-access.getSubjectOptions", "data-access.getSubjectNameMapByIds" ], "users": [ "data-access.getUserWithRole", "data-access.getUserNamesByIds" ], "files": [ "data-access.deleteFileAttachment" ], "notifications": [ "data-access.createNotification (动态 import, P3-2 催交提醒)" ] } }, "questions": { "dependsOn": [ "shared", "auth", "textbooks" ], "uses": { "shared": [ "db", "auth-guard", "types" ], "auth": [ "auth" ], "textbooks": [ "data-access.getTextbooks", "data-access.getChaptersByTextbookId", "data-access.getKnowledgePointsByChapterId", "data-access.getKnowledgePointsByTextbookId", "data-access.getKnowledgePointOptions" ] } }, "textbooks": { "dependsOn": [ "shared", "auth", "users", "classes" ], "uses": { "shared": [ "db", "auth-guard", "types" ], "auth": [ "auth" ], "users": [ "data-access.getCurrentStudentUser" ], "classes": [ "data-access-students.getClassStudents" ] } }, "classes": { "dependsOn": [ "shared", "auth", "homework", "scheduling", "school" ], "uses": { "shared": [ "db", "auth-guard", "types" ], "auth": [ "auth" ], "homework": [ "data-access-classes.getAssignmentIdsForStudents", "data-access-classes.getHomeworkAssignmentsWithSubject", "data-access-classes.getHomeworkAssignmentsByIds", "data-access-classes.getAssignmentMaxScoreById", "data-access-classes.getAssignmentTargetCounts", "data-access-classes.getHomeworkSubmissionsForStudents", "data-access-classes.getPublishedHomeworkAssignmentsWithSubject", "data-access-classes.getHomeworkSubmissionsForAssignments" ], "scheduling": [ "data-access-class-schedule.createClassScheduleItem", "data-access-class-schedule.updateClassScheduleItem", "data-access-class-schedule.deleteClassScheduleItem" ], "school": [ "data-access.isGradeHead", "data-access.isGradeManager", "data-access.findGradeIdByHeadAndName" ] } }, "school": { "dependsOn": [ "shared", "auth" ], "uses": { "shared": [ "db", "auth-guard", "types" ], "auth": [ "auth" ] } }, "dashboard": { "dependsOn": [ "shared", "auth", "homework", "classes" ], "uses": { "shared": [ "db", "types" ], "auth": [ "auth" ], "homework": [ "data-access.getTeacherGradeTrends", "data-access.getStudentDashboardGrades" ], "classes": [ "data-access.getTeacherClasses", "data-access.getStudentClasses", "data-access.getStudentSchedule" ] } }, "layout": { "dependsOn": [ "shared", "auth", "messaging" ], "uses": { "shared": [ "hooks.usePermission", "components.global-search.GlobalSearch" ], "auth": [ "useSession" ], "messaging": [ "components.notification-dropdown" ] } }, "settings": { "dependsOn": [ "shared", "auth", "classes", "homework", "dashboard", "users", "notifications", "files" ], "uses": { "shared": [ "db", "auth-guard", "ai", "types", "components.ui.switch", "components.ui.card", "components.ui.tabs", "components.ui.alert-dialog", "components.ui.badge", "components.form-fields", "components.locale-switcher" ], "auth": [ "auth" ], "classes": [ "data-access.getStudentClasses", "data-access.getStudentSchedule", "data-access.getTeacherClasses", "data-access.getTeacherTeachingSubjects" ], "homework": [ "data-access.getStudentHomeworkAssignments", "data-access.getStudentDashboardGrades" ], "dashboard": [ "components.student-dashboard.student-grades-card", "components.student-dashboard.student-stats-grid", "components.student-dashboard.student-today-schedule-card", "components.student-dashboard.student-upcoming-assignments-card" ], "users": [ "data-access.UserProfile", "data-access.UpdateUserProfileInput", "data-access.updateUserAvatar" ], "notifications": [ "types.NotificationPreferences", "types.UpdateNotificationPreferencesInput" ], "files": [ "data-access.getFileByUrl", "data-access.deleteFileAttachment" ] }, "note": "组件层通过 SettingsService 接口注入解耦,不直接 import messaging/actions;页面层 app/(dashboard)/settings/page.tsx 负责注入 users/actions + messaging/actions 实现。P0-3/P2-8/P2-9/P2-10/P2-11 已修复:AdminSettingsView 接入真实数据层(system_settings 表)、头像上传、2FA/登录历史、通知测试按钮、语言切换集成。v2 已增强:通知测试接入真实发送、头像上传清理旧文件、会话远程登出、AdminSettingsView/通知偏好表单 dirty 检测、currentDeviceLabel 标记当前会话、文件名长度校验、2FA 查询 N+1 优化。v3 已增强:完整 TOTP 2FA 流程(otplib v13 + qrcode)— 启用(QR码扫描+验证码校验+10个备份码 bcrypt 哈希存储)/关闭(需 TOTP 或备份码确认)/重新生成备份码;登录流程接入 2FA(preflightTwoFactorAction 预检 + auth.ts authorize 校验);新增 29 个 TOTP 单元测试(总计 59 个)。依赖新增:otplib、qrcode。" }, "users": { "dependsOn": [ "shared", "auth", "classes" ], "uses": { "shared": [ "db", "auth-guard.requireAuth", "auth-guard.requirePermission", "db.schema.users", "db.schema.roles", "db.schema.usersToRoles", "types.permissions", "types.action-state", "lib.excel" ], "auth": [ "auth" ], "classes": [ "data-access.enrollStudentByInvitationCode" ] } }, "audit": { "dependsOn": [ "shared", "auth" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "db.schema.auditLogs", "db.schema.loginLogs", "db.schema.dataChangeLogs", "types.permissions", "lib.excel" ], "auth": [ "auth" ] } }, "announcements": { "dependsOn": [ "shared", "auth", "school", "classes" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "auth-guard.requireAuth", "db.schema.announcements", "db.schema.announcementReads", "types.permissions" ], "auth": [ "auth" ], "school": [ "data-access.getGrades" ], "classes": [ "data-access.getClassGradeId", "data-access.getStudentActiveClassId", "data-access.getStudentActiveGradeId", "data-access.getStudentIdsByClassId", "data-access.getTeacherIdsByClassIds", "data-access.getAdminClasses" ] } }, "files": { "dependsOn": [ "shared", "auth" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "types.permissions", "types.action-state", "lib.file-storage", "lib.storage-provider", "lib.track-event.trackEvent", "lib.audit-logger.logAudit", "components.widget-boundary" ], "auth": [ "auth" ] }, "note": "2026-06-25 P0-P2 审计重构:新增 actions.ts 编排层(6 个 Server Action,权限+Zod+storageProvider+trackEvent+logAudit)、schema.ts(5 个 Zod schema)、hooks/ 目录(3 个自定义 hooks)。API 路由全部委托 actions.ts,不再直接调用 data-access。auth-guard.requireAuth 已替换为 requirePermission。新增依赖:lib.track-event(监控埋点)、lib.audit-logger(审计日志)、types.action-state(统一返回类型)、components.widget-boundary(错误边界+骨架屏)。被依赖方新增:settings(getFileByUrl + deleteFileAttachment 用于头像清理)。", "usedBy": [ "app/api/upload", "app/api/files/[id]", "app/api/files/batch-delete", "app/(dashboard)/admin/files", "homework", "settings" ] }, "course-plans": { "dependsOn": [ "shared", "auth", "school", "classes", "users" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "db.schema.coursePlans", "db.schema.coursePlanItems", "types.permissions", "types.action-state", "types.permissions.DataScope", "lib.action-utils.handleActionError", "lib.export-utils.toCSV", "lib.export-utils.downloadFile", "lib.export-utils.exportCSV", "components.section-error-boundary", "components.ui.button", "components.ui.card", "components.ui.input", "components.ui.label", "components.ui.textarea", "components.ui.select", "components.ui.checkbox", "components.ui.table", "components.ui.dialog", "components.ui.empty-state" ], "auth": [ "auth" ], "school": [ "data-access.getAcademicYears", "data-access.getSubjectOptions", "data-access.getSubjectNameMapByIds" ], "classes": [ "data-access.getAdminClasses", "data-access.getStaffOptions", "data-access.getClassNamesByIds" ], "users": [ "data-access.getUserNamesByIds" ] }, "note": "2026-06-25 P0-P2 审计重构完成(全部实施):\n[P0 安全] getCoursePlanById/getCoursePlans/getGradeCoursePlanProgress 增加 CoursePlanQueryScope 参数(按 admin/teacher/parent/student 角色过滤归属);resolveScope() 从 AuthContext.dataScope 解析为查询范围;admin/teacher 页面补充 requirePermission(COURSE_PLAN_READ);updateCoursePlanAction/deleteCoursePlanAction/reorderCoursePlanItemsAction/createCoursePlanItemAction 增加非 admin 归属二次校验。\n[P0 i18n] 提取全部硬编码文本至 zh-CN/en course-plans.json(~230 键,覆盖状态/表单/详情/列表/日历/导出/模板/错误边界)。\n[P1 架构] 跨模块 JOIN 移除:buildPlanSelect 不再 leftJoin classes/subjects/users,改由 enrichPlanRows 通过动态 import classes.getClassNamesByIds/school.getSubjectNameMapByIds/users.getUserNamesByIds 批量解析名称;getSubjectOptions 已迁移至 school 模块;所有 admin/teacher/parent/student 路由补充 loading.tsx + error.tsx;原生 替换为 ;as 断言替换为类型守卫 isCoursePlanStatus/isCoursePlanSemester;URL 路径推断改为 successHref prop 传入;getCoursePlansAction/getGradeCoursePlanProgressAction 增加 Zod 验证;reorderCoursePlanItems 死代码补充对应 Action + UI(DnD 拖拽排序)。\n[P1-7 拖拽] 新增 @dnd-kit/core + @dnd-kit/sortable + @dnd-kit/utilities 依赖;SortableWeekRow 组件(GripVertical 拖拽手柄,PointerSensor+KeyboardSensor a11y);course-plan-detail.tsx 集成 DndContext + SortableContext + handleReorder 乐观更新。\n[P2-1 错误边界] shared/components/section-error-boundary.tsx 重构为类组件 + 函数式包装器双层结构,支持 fallback 自定义降级 UI + onError 回调 + i18n 默认文案;course-plan-detail.tsx 各数据区块用 SectionErrorBoundary namespace='coursePlans' 包裹。\n[P2-2 多角色路由] 新增 parent/course-plans + student/course-plans 路由(只读,按孩子班级/学生班级 scope 过滤),全部含 loading.tsx + error.tsx。\n[P2-3 数据联动] SortableWeekRow 教材章节支持 textbooksHref 跳转(teacher→/teacher/textbooks,student→/student/learning/textbooks);作业跳转 homeworkHref(teacher→/teacher/homework)。\n[P2-4 批量操作] bulkToggleItemsAction + bulkUpdateItemCompleted(多选复选框批量标记完成)。\n[P2-5 模板复用] TemplatePickerDialog 组件(搜索筛选 + 列表选择 + copyCoursePlanAction 克隆);getTemplateCandidatesAction 返回按 subjectId 过滤的候选计划。\n[P2-6 导出] lib/export-utils.ts 纯函数 planToExportRows + exportCoursePlanReport;CSV 含 UTF-8 BOM 兼容 Excel;shared/lib/export-utils.ts 从 dashboard/lib 迁移供全模块复用。\n[P2-7 日历视图] lib/calendar-utils.ts 纯函数(planToCalendarEvents + buildMonthGrid 6×7 + 日期工具);CoursePlanCalendar 组件(上月/下月/今天导航,事件单元格,最多 2 个 + 溢出计数,事件列表)。\n[P2-8 监控埋点] trackCoursePlanEvent 接口预留。\n[配置驱动] ROLE_WIDGET_CONFIG 按角色配置仪表盘组件;CoursePlanWidgetId 类型联合。\n[文件清单] types.ts(扩展)/schema.ts(扩展 +5 Zod)/data-access.ts(重构)/actions.ts(扩展)/components/(新增 3 + 修改 5)/lib/(新增 2 文件)。所有文件行数符合规范(组件 ≤500,actions/data-access ≤800)。", "usedBy": [ "app/(dashboard)/admin/course-plans", "app/(dashboard)/admin/course-plans/[id]", "app/(dashboard)/admin/course-plans/create", "app/(dashboard)/admin/course-plans/[id]/edit", "app/(dashboard)/teacher/course-plans", "app/(dashboard)/teacher/course-plans/[id]", "app/(dashboard)/parent/course-plans", "app/(dashboard)/parent/course-plans/[id]", "app/(dashboard)/student/course-plans", "app/(dashboard)/student/course-plans/[id]", "app/(dashboard)/management/grade/dashboard", "dashboard" ] }, "grades": { "dependsOn": [ "shared", "auth", "classes", "school", "users", "exams" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "types.permissions", "types.action-state", "db.schema.gradeRecords", "db.schema.gradeRecordAnswers", "db.schema.gradeDrafts", "lib.excel" ], "auth": [ "auth" ], "classes": [ "data-access.getClassExists", "data-access.getClassNameById", "data-access.getClassNamesByIds", "data-access.getActiveStudentIdsByClassId", "data-access.getStudentActiveClassId", "data-access.getClassesByGradeId", "data-access.getClassGradeIdsByClassIds" ], "school": [ "data-access.getSubjectOptions", "data-access.getGradeOptions" ], "users": [ "data-access.getUserNamesByIds" ], "exams": [ "data-access.getExamsForGradeEntry", "data-access.getExamForGradeEntry" ] } }, "parent": { "dependsOn": [ "shared", "auth", "homework", "classes", "grades", "school", "users", "attendance" ], "uses": { "shared": [ "db", "auth-guard.requireAuth", "auth-guard.getAuthContext", "db.schema.parentStudentRelations", "types", "lib.utils.getSearchParam" ], "auth": [ "auth" ], "homework": [ "data-access.getStudentHomeworkAssignments", "data-access.getStudentDashboardGrades" ], "classes": [ "data-access.getStudentClasses", "data-access.getStudentSchedule", "data-access.getStudentActiveClass" ], "grades": [ "data-access.getStudentGradeSummary" ], "school": [ "data-access.getGradeNameById" ], "users": [ "data-access.getUserBasicInfo", "data-access.getUserNamesByIds" ], "attendance": [ "services.createAttendanceReadService (✅ 审计 P1-2:通过接口消费,不再直接 import data-access)", "services.types.AttendanceReadService", "services.types.AttendanceDataService" ] }, "auditNotes": [ "✅ 审计修复 P1-2:parent 模块不再直接 import attendance/components/student-attendance-view,改为通过 AttendanceReadService 接口消费 + 自有 ParentStudentAttendanceDetail 组件渲染", "✅ 审计修复 P1-3:parent-attendance-calendar.tsx 等组件改从 shared/constants/attendance-status 导入状态常量,不再依赖 attendance 模块", "✅ 审计修复 P2-8:parent-attendance-warning.tsx / parent-attendance-rate-card.tsx 改为 RSC(使用 getTranslations)" ] }, "messaging": { "dependsOn": [ "shared", "auth", "notifications", "classes" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "auth-guard.requireAuth", "db.schema.messages", "db.schema.users", "db.schema.classEnrollments", "db.schema.classes", "db.schema.grades", "types.permissions", "types.action-state" ], "auth": [ "auth" ], "notifications": [ "dispatcher.sendNotification", "data-access.createNotification (via re-export)", "data-access.getNotifications (via re-export)", "data-access.markNotificationAsRead (via re-export)", "data-access.markAllNotificationsAsRead (via re-export)", "data-access.getUnreadNotificationCount (via re-export)", "preferences.getNotificationPreferences (via re-export)", "preferences.upsertNotificationPreferences (via re-export)" ], "classes": [ "data-access.getTeacherIdsByClassIds", "data-access.getStudentActiveClassId" ] } }, "notifications": { "dependsOn": [ "shared", "auth", "classes" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "db.schema.messageNotifications", "db.schema.notificationPreferences", "types.permissions", "types.action-state" ], "auth": [ "auth" ], "classes": [ "data-access.getClassExists", "data-access.getStudentIdsByClassId" ] } }, "attendance": { "dependsOn": [ "shared", "auth", "classes", "users", "parent", "notifications", "grades" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.attendanceRecords", "db.schema.attendanceRules", "constants.attendance-status (✅ 审计 P1-3:状态类型与视觉常量下沉至 shared)", "lib.action-utils.safeParseDate", "lib.action-utils.handleActionError", "lib.action-utils.safeJsonParse", "lib.auth-guard.requirePermission", "lib.track-event.trackEvent", "lib.resolve-action-error", "types.permissions", "types.action-state", "types.DataScope", "hooks.use-permission" ], "auth": [ "auth" ], "classes": [ "data-access.getClassActiveStudentsWithInfo", "data-access.getClassNamesByIds", "data-access.getClassNameById", "data-access.verifyTeacherOwnsClass", "data-access.getTeacherClasses", "data-access.getAdminClasses" ], "users": [ "data-access.getUserNamesByIds" ], "parent": [ "data-access.getParentStudentMapByStudentIds (L-2 批量查询家长-学生映射,避免 N+1)" ], "notifications": [ "dispatcher.sendBatchNotifications (L-2 考勤记录后异步通知家长)", "notifyParentsOfAbsence (L-2 内部函数:批量构造通知负载)" ], "grades": [ "data-access.getGradeRecords (L-9 考勤与成绩关联分析:跨模块委托查询班级成绩数据,纯函数计算Pearson 相关系数与风险分级)" ] }, "auditNotes": [ "✅ 审计修复 P0-5:移除 db.schema.classEnrollments / db.schema.users / db.schema.classes 直查,改为委托 classes/users data-access", "✅ 审计新增 P1-12:services/ 目录暴露 AttendanceDataService 等接口,消费方依赖接口而非具体实现", "✅ 审计新增 P1-3:状态常量下沉至 shared/constants/attendance-status,parent 模块改从 shared 导入", "✅ L-2 新增:实时家长通知。批量点名保存成功后调用 notifications.notifyParentsOfAbsence,批量查询家长-学生映射后通过 dispatcher 发送通知;通知失败不阻断主流程", "✅ L-6 新增:节次考勤。attendanceRecords 表新增 period 字段(nullable,向后兼容:NULL 视为 full_day);buildPeriodFilter 对 full_day 使用 or(isNull, eq) 兼容旧数据;对外统一为 full_day", "✅ L-9 新增:考勤与成绩关联分析。data-access-correlation.getAttendanceGradeCorrelation 跨模块委托 classes/users/grades data-access 聚合数据;correlation-compute.ts 纯函数实现 Pearson 相关系数计算、风险分级(high/medium/low)与汇总,与 IO 解耦便于单测;AttendanceGradeCorrelationCard 散点图 + 风险表格展示" ] }, "leave-requests": { "dependsOn": [ "shared", "auth", "classes", "users", "parent", "attendance" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.leaveRequests (L-5 新增表)", "lib.track-event.trackEvent", "types.permissions", "types.action-state", "types.DataScope" ], "auth": [ "auth" ], "classes": [ "data-access.getStudentActiveClass (校验学生当前班级)", "data-access.getClassNamesByIds (列表展示班级名称)" ], "users": [ "data-access.getUserNamesByIds (列表展示学生/提交人/审批人名称)" ], "parent": [ "data-access.getParentStudentMapByStudentIds (校验家长-子女关系)" ], "attendance": [ "data-access.syncExcusedFromLeaveRequest (审批通过后同步 excused 考勤记录)" ] }, "auditNotes": [ "✅ L-5 新增:在线请假流程。状态机 pending → approved/rejected;pending/approved → cancelled。", "✅ L-5 跨模块联动:审批通过后调用 attendance/data-access.syncExcusedFromLeaveRequest 同步考勤,try/catch 不阻断审批;attendanceSynced 标志位防止重复同步。", "✅ L-5 幂等设计:syncExcusedFromLeaveRequest 遍历日期区间,仅插入不存在的 excused 记录;hasOverlappingLeave 防止重复请假。", "✅ L-5 DataScope 行级过滤:buildScopeFilter 支持 all/class_taught/children/class_members/owned。" ] }, "scheduling": { "dependsOn": [ "shared", "auth", "classes", "users" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.schedulingRules", "db.schema.scheduleChanges", "db.schema.classSchedule", "db.schema.classSubjectTeachers", "db.schema.subjects", "db.schema.classrooms", "types.permissions", "types.action-state" ], "auth": [ "auth" ], "classes": [ "data-access.verifyTeacherOwnsClass", "data-access.getTeacherIdForMutations" ], "users": [ "data-access.getUserNamesByIds" ] } }, "diagnostic": { "dependsOn": [ "shared", "auth", "classes", "exams", "questions", "users", "grades", "school", "notifications", "parent" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.knowledgePointMastery", "db.schema.learningDiagnosticReports", "db.schema.knowledgePoints", "types.permissions", "types.action-state", "hooks.usePermission", "components.ui.*", "components.widget-boundary" ], "auth": [ "auth" ], "classes": [ "data-access.getClassExists", "data-access.getClassNameById", "data-access.getActiveStudentIdsByClassId", "data-access.getStudentIdsByClassId" ], "exams": [ "data-access.getExamSubmissionWithAnswers" ], "questions": [ "data-access.getKnowledgePointsForQuestions" ], "users": [ "data-access.getUserNamesByIds", "data-access.getUserIdsByGradeId" ], "grades": [ "lib.grade-utils.toNumber" ], "school": [ "data-access.getGradeNameById" ], "notifications": [ "data-access.createNotification" ], "parent": [ "data-access.getParentIdsByStudentIds" ] } }, "elective": { "dependsOn": [ "shared", "auth", "classes", "school", "users", "notifications" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "db.schema.electiveCourses", "db.schema.courseSelections", "db.schema.systemSettings", "types.permissions", "types.action-state", "types.DataScope", "hooks.usePermission", "components.ui.*" ], "auth": [ "auth" ], "classes": [ "data-access.getStudentActiveGradeId" ], "school": [ "data-access.getSubjectOptions", "data-access.getGradeOptions" ], "users": [ "data-access.getUserNamesByIds" ], "notifications": [ "sendNotification" ] } }, "proctoring": { "dependsOn": [ "shared", "auth", "exams", "users" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "db.schema.examProctoringEvents", "types.permissions", "types.action-state", "hooks.usePermission", "components.ui.*", "next/cache.revalidatePath" ], "auth": [ "auth" ], "exams": [ "data-access.getExamForProctoringCrossModule", "data-access.getExamSubmissionForProctoringCrossModule", "data-access.getExamSubmissionsForExam", "data-access.getExamTitleById" ], "users": [ "data-access.getUserNamesByIds" ] } }, "lesson_preparation": { "dependsOn": [ "shared", "auth", "textbooks", "questions", "exams", "homework", "classes", "files" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "db.schema.lessonPlans", "db.schema.lessonPlanVersions", "db.schema.lessonPlanTemplates", "lib.ai.createAiChatCompletion", "types.permissions", "types.action-state", "hooks.usePermission", "components.ui.*", "next/cache.revalidatePath" ], "auth": [ "auth" ], "textbooks": [ "data-access.getChapters", "data-access.getKnowledgePoints" ], "questions": [ "data-access.getQuestions", "data-access.createQuestionWithRelations" ], "exams": [ "data-access.persistExamDraft" ], "homework": [ "data-access-write.createHomeworkAssignment" ], "classes": [ "data-access.getTeacherClasses" ], "files": [ "data-access.createFileAttachment", "data-access.getFileAttachmentsByTarget" ], "external": [ "@xyflow/react(React Flow 节点图编辑器:ReactFlow/Background/Controls/MiniMap/Handle/applyNodeChanges/applyEdgeChanges)", "@paralleldrive/cuid2(节点 ID 生成)", "zustand(编辑器状态管理)" ] } }, "onboarding": { "dependsOn": [ "shared", "auth", "classes" ], "uses": { "shared": [ "db", "auth-guard.requireAuth", "db.schema.users", "db.schema.usersToRoles", "db.schema.roles", "db.schema.parentStudentRelations", "lib.role-utils", "lib.route-resolver.resolveDefaultPath", "types.permissions", "types.action-state" ], "auth": [ "auth" ], "classes": [ "data-access.enrollStudentByInvitationCode", "data-access.enrollTeacherByInvitationCode" ] } }, "modules/invitation-codes": { "dependsOn": [ "shared" ], "uses": { "shared": [ "db", "db.schema.invitationCodes", "lib.auth-guard.requirePermission", "lib.track-event.trackEvent", "types.action-state", "types.permissions.Permissions.USER_MANAGE" ] }, "note": "audit-P2-3 新增。被 modules/auth 依赖(registerAction 调用 validateInvitationCode + consumeInvitationCode)。无对其他 modules/* 的依赖,仅依赖 shared 层。" }, "error-book": { "dependsOn": [ "shared", "classes", "questions" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "db.schema.errorBookItems", "db.schema.errorBookReviews", "db.schema.examSubmissions", "db.schema.submissionAnswers", "db.schema.homeworkSubmissions", "db.schema.homeworkAnswers", "db.schema.questions", "db.schema.questionsToKnowledgePoints", "db.schema.knowledgePoints", "db.schema.subjects", "db.schema.examQuestions", "db.schema.homeworkAssignmentQuestions", "db.schema.users", "types.permissions", "types.action-state", "lib.utils" ], "classes": [ "data-access.getStudentIdsByClassIds" ], "questions": [ "actions.getQuestionsAction" ] } }, "ai": { "dependsOn": [ "shared", "textbooks" ], "uses": { "shared": [ "db", "auth-guard.requirePermission", "lib.ai.createAiChatCompletion", "lib.ai.createAiChatCompletionStream", "types.permissions", "types.action-state", "lib.track-event.trackEvent", "i18n.messages" ], "textbooks": [ "data-access-graph.getKnowledgePointsWithRelations", "data-access-graph.getStudentKpMastery" ] }, "exports": { "actions": [ "aiChatAction", "suggestSimilarQuestionsAction", "suggestGradingAction", "generateLessonContentAction", "generateQuestionVariantAction", "analyzeWeaknessAction", "generateChildSummaryAction", "recommendStudyPathAction", "getAiUsageStatsAction", "explainErrorAction" ], "types": [ "AiService", "AiClientService", "AiChatMessage", "AiChatResult", "SimilarQuestionInput", "SimilarQuestionResult", "GradingInput", "GradingSuggestion", "LessonContentInput", "LessonContentResult", "QuestionVariantInput", "QuestionVariantResult", "WeaknessAnalysisInput", "WeaknessAnalysisResult", "ChildSummaryInput", "ChildSummaryResult", "StudyPathInput", "StudyPathResult", "AiUsageStats", "AiCapability", "ExplainErrorInput", "ExplainErrorResult" ], "components": [ "AiGradingAssist", "AiErrorBookAnalysis", "AiLessonContentGenerator", "AiQuestionVariantGenerator", "AiChatPanel", "AiErrorBoundary", "AiSuggestionSkeleton", "AiProviderSelector", "AiAssistantWidget", "AiAssistantWidgetInner", "AiMarkdownRenderer", "AiChartRenderer", "AiChildSummary", "AiUsageDashboard", "AiStudyPath" ], "hooks": [ "useAiClient", "useAiClientOptional", "useAiSuggestion", "useAiChatStream", "useFloatingBall", "usePositionPersistence", "useDragPosition" ], "providers": [ "AiClientProvider" ], "context": [ "createFullAiClientService", "createCoreAiClientService" ], "services": [ "DefaultAiService", "createAiService", "safeAiCall", "filterUserInput", "filterAiOutput", "checkDailyLimit", "incrementDailyUsage", "getDailyLimit", "tryConsumeDailyQuota", "refundDailyQuota", "validateSocraticOutput" ], "dataAccess": [ "recordAiEvent", "getAiUsageStats" ], "streamUtils": [ "consumeSseStream", "getStreamErrorKey", "extractErrorMessage", "removeTrailingEmptyAssistant", "appendTokenToLastAssistant" ], "promptTemplates": [ "CHILD_SUMMARY_SYSTEM_PROMPT", "STUDY_PATH_SYSTEM_PROMPT", "SOCRATIC_TUTOR_SYSTEM_PROMPT", "CHAT_SYSTEM_PROMPT", "EXPLAIN_ERROR_SYSTEM_PROMPT" ] }, "integrations": { "homework": { "component": "AiGradingAssist", "page": "teacher/homework/submissions/[submissionId]", "capability": "grading-assist" }, "error-book": { "component": "AiErrorBookAnalysis", "page": "student/error-book", "capability": "similar-question, weakness-analysis" }, "lesson-preparation": { "component": "AiLessonContentGenerator", "page": "teacher/lesson-plans/[planId]/edit", "capability": "lesson-content" }, "exams": { "component": "AiQuestionVariantGenerator", "page": "teacher/exams/[id]/build", "capability": "question-variant" }, "parent-dashboard": { "component": "AiChildSummary", "page": "parent/dashboard", "capability": "child-summary" }, "admin-dashboard": { "component": "AiUsageDashboard", "page": "admin/ai-settings", "capability": "usage-stats" }, "student-learning": { "component": "AiStudyPath", "page": "student/learning/study-path", "capability": "study-path" }, "global-assistant": { "component": "AiAssistantWidget", "page": "dashboard-layout (all pages)", "capability": "context-aware-chat" } }, "permissions": [ "ai:chat", "ai:configure" ], "safety": { "inputFilter": "filterUserInput — 阻断暴力/自残/色情/毒品/黑客/PII 索取等模式", "outputFilter": "filterAiOutput — 输出内容二次过滤,学生场景额外阻断直接答案模式", "dailyLimits": { "student": 50, "teacher": 200, "parent": 30, "admin": 500 }, "studentSocraticMode": "学生角色强制苏格拉底式引导,不直接给答案", "compliance": "COPPA/FERPA K12 合规设计" }, "streaming": { "endpoint": "/api/ai/chat/stream", "protocol": "SSE (Server-Sent Events)", "clientHook": "useAiChatStream", "features": [ "实时 token 流式渲染", "AbortController 支持停止生成", "错误事件流式回传", "过滤事件流式回传" ] }, "i18n": { "namespace": "ai", "files": [ "shared/i18n/messages/zh-CN/ai.json", "shared/i18n/messages/en/ai.json" ], "v2Keys": [ "chat.streaming", "chat.stopGeneration", "chat.copy", "chat.clearConfirm", "chat.suggestedPrompts", "chat.contextMessage.*", "grading.description", "grading.batch*", "lessonPrep.description", "lessonPrep.additionalContext", "lessonPrep.insertContent", "exam.variantType.*", "exam.targetDifficulty", "exam.addVariant", "parent.*", "admin.*", "studyPath.*", "widget.*", "safety.*", "chart.parseError", "error.statsFailed" ] } }, "adaptive-practice": { "dependsOn": [ "shared", "classes", "users", "questions", "error-book" ], "uses": { "shared": [ "db", "db.schema.practiceSessions", "db.schema.practiceAnswers", "db.schema.questions", "db.schema.questionsToKnowledgePoints", "db.schema.knowledgePoints", "auth-guard.requirePermission", "types.permissions", "types.action-state", "lib.action-utils.handleActionError", "i18n.messages" ], "classes": [ "data-access.getActiveStudentIdsByClassId", "data-access.getClassNameById", "data-access.getClassesByGradeId", "data-access.getClassIdsByGradeIds", "data-access.getStudentIdsByClassIds" ], "users": [ "data-access.getUserIdsByGradeId", "data-access.getUserNamesByIds" ], "questions": [ "data-access" ], "error-book": [ "data-access" ] }, "exports": { "actions": [ "getPracticeSessionsAction", "getPracticeSessionDetailAction", "getPracticeStatsAction", "createPracticeSessionAction", "submitPracticeAnswerAction", "completePracticeSessionAction", "abandonPracticeSessionAction" ], "dataAccess": [ "getPracticeSessions", "getPracticeSessionById", "getPracticeStats", "createPracticeSession", "submitPracticeAnswer", "completePracticeSession", "abandonPracticeSession", "autoGradeAnswer" ], "dataAccessStrategy": [ "selectQuestionsForPractice", "selectForErrorVariant", "selectForKnowledgePoint", "selectForWeakChapter", "selectForAiRecommended", "identifyWeakKnowledgePoints" ], "dataAccessAnalytics": [ "getClassPracticeStats", "getClassStudentPracticeSummaries", "getGradePracticeStats", "getPracticeTypeBreakdown", "getStudentsWithoutPractice", "getTeacherClassPracticeOverviews", "getClassKnowledgePointWeakness", "getGradeClassPracticeComparison", "getClassLearningProfile", "getStudentNameMap", "getGradeClassOverviews", "getGradePracticeOverview" ], "components": [ "PracticeStarter", "PracticeStarterWithNav", "PracticeSessionView", "PracticeHistory", "PracticeStatsCards", "PracticeOverviewStatsCards", "ClassPracticeComparisonTable", "PracticeTypeBreakdownChart", "ClassKnowledgePointWeaknessChart", "StudentPracticeRankingTable", "InactiveStudentsAlert" ], "services": [ "PracticeService (interface)", "PracticeAnalytics (interface)", "PracticeServiceProvider", "usePracticeService", "usePracticeAnalytics", "defaultPracticeService", "noopPracticeAnalytics" ], "lib": [ "grading.gradeAnswer", "grading.isCorrectForType", "source-meta.isErrorVariantSourceMeta", "source-meta.isKnowledgePointSourceMeta", "source-meta.isWeakChapterSourceMeta", "source-meta.isAiRecommendedSourceMeta", "source-meta.parseSourceMeta", "type-guards.isPracticeType", "type-guards.asPracticeType", "type-guards.asPracticeSourceMeta" ], "types": [ "PracticeType", "PracticeStatus", "PracticeAnswerStatus", "PracticeSourceMeta", "PracticeSessionSummary", "PracticeSessionDetail", "PracticeStats", "CreatePracticeSessionInput", "PracticeStarterProps", "PracticeHistoryProps", "PracticeService", "PracticeAnalytics" ] }, "integrations": { "error-book": { "component": "createPracticeSessionAction", "page": "student/error-book detail dialog (via onStartVariantPractice props 注入)", "capability": "error-variant-practice" }, "student": { "component": "PracticeStarterWithNav + PracticeSessionView", "page": "student/practice, student/practice/[sessionId]", "capability": "student-practice" }, "teacher": { "component": "PracticeOverviewStatsCards etc", "page": "teacher/practice", "capability": "teacher-analytics", "classFilter": "shared/components/class-filter (V4 提取,不再依赖 error-book)" }, "management": { "component": "PracticeOverviewStatsCards etc", "page": "management/grade/practice", "capability": "grade-analytics" }, "parent": { "component": "PracticeStatsCards + PracticeHistory (routePrefix=undefined)", "page": "parent/practice", "capability": "parent-practice-readonly" } }, "permissions": [ "adaptive_practice:read", "adaptive_practice:manage" ], "autoGrading": { "single_choice": "学生答案 ID 与正确答案 ID 完全匹配", "multiple_choice": "学生答案 ID 集合与正确答案 ID 集合完全匹配(集合比对)", "judgment": "学生布尔答案与正确布尔答案一致", "text": "不自动判分,返回 null,待教师批阅" }, "strategies": { "error_variant": "按难度升序选取原题(确保 AI 不可用时也能练习)", "knowledge_point": "按知识点+难度筛选,随机抽取", "weak_chapter": "排除已答题目,避免重复", "ai_recommended": "从 AI 推荐知识点抽题" }, "i18n": { "namespace": "practice", "files": [ "shared/i18n/messages/zh-CN/practice.json", "shared/i18n/messages/en/practice.json" ], "keys": [ "starter.*", "session.*", "result.*", "history.*", "stats.*", "types.*", "status.*", "teacher.*", "grade.*", "parent.*", "errors.*", "messages.*", "reasons.*", "classFilter.*" ] } }, "search": { "dependsOn": [ "shared" ] } }, "moduleDependencyGraph": { "nodes": [ "shared", "auth", "exams", "homework", "questions", "textbooks", "classes", "school", "dashboard", "layout", "settings", "users", "audit", "announcements", "files", "course-plans", "grades", "parent", "messaging", "notifications", "attendance", "scheduling", "proctoring", "diagnostic", "elective", "onboarding", "error-book", "ai", "adaptive-practice", "search" ], "edges": [ { "from": "exams", "to": "questions", "type": "data-access", "description": "引用题目(examQuestions 关联)" }, { "from": "exams", "to": "shared", "type": "normal", "description": "使用 db/auth-guard/ai" }, { "from": "homework", "to": "exams", "type": "data-access", "description": "引用试卷结构(sourceExamId)" }, { "from": "homework", "to": "questions", "type": "data-access", "description": "引用题目" }, { "from": "grades", "to": "classes", "type": "data-access", "description": "查询班级" }, { "from": "grades", "to": "exams", "type": "data-access", "description": "关联考试(examId)" }, { "from": "grades", "to": "subjects", "type": "data-access", "description": "查询科目" }, { "from": "dashboard", "to": "exams", "type": "data-access", "description": "调用 getExamsDashboardStats 获取考试统计(P0-4 已修复)" }, { "from": "dashboard", "to": "homework", "type": "data-access", "description": "调用 getHomeworkDashboardStats 获取作业统计(P0-4 已修复)" }, { "from": "dashboard", "to": "classes", "type": "data-access", "description": "调用 getClassesDashboardStats 获取班级统计(P0-4 已修复)" }, { "from": "dashboard", "to": "users", "type": "data-access", "description": "调用 getUsersDashboardStats 获取用户/会话/角色统计(P0-4 已修复)" }, { "from": "dashboard", "to": "textbooks", "type": "data-access", "description": "调用 getTextbooksDashboardStats 获取教材/章节统计(P0-4 已修复)" }, { "from": "dashboard", "to": "questions", "type": "data-access", "description": "调用 getQuestionsDashboardStats 获取题目统计(P0-4 已修复)" }, { "from": "messaging", "to": "notifications", "type": "data-access", "description": "messaging 通过 sendNotification dispatcher 发送新消息通知。V3 P0-3 修复:删除 data-access 层 getMessagesPageData 跨模块动态 import,消息首页编排迁回页面层通过 Promise.all 调用 messaging.data-access 和 notifications.data-access。V3 P1-3 修复:通知偏好 Action 已迁回 notifications 模块(原 messaging.getNotificationPreferencesAction/updateNotificationPreferencesAction 已删除)" }, { "from": "messaging", "to": "classes", "type": "data-access", "description": "getRecipients 通过 classes data-access.getTeacherIdsByClassIds / getStudentActiveClassId 获取班级教师 ID,支持 class_members(学生)和 children(家长)数据范围" }, { "from": "classes", "to": "homework", "type": "data-access", "description": "通过 homework/data-access-classes 获取作业数据(P0-7 已修复,原为 violation)" }, { "from": "classes", "to": "scheduling", "type": "resolved", "description": "✅ P0-5 已修复:classSchedule 写函数从 classes 迁移至 scheduling/data-access-class-schedule.ts,classes 仅保留读函数" }, { "from": "classes", "to": "grades", "type": "resolved", "description": "✅ P0-1 已修复:classes/data-access.ts 已拆分为 5 个文件,不再混入 grades 查询逻辑" }, { "from": "proctoring", "to": "exams", "type": "data-access", "description": "关联考试与提交(examSubmissions)" }, { "from": "diagnostic", "to": "questions", "type": "data-access", "description": "关联题目知识点(questionsToKnowledgePoints)" }, { "from": "diagnostic", "to": "exams", "type": "data-access", "description": "关联考试提交(examSubmissions/submissionAnswers)" }, { "from": "diagnostic", "to": "grades", "type": "lib-import", "description": "复用 toNumber 工具函数(P3-1 修复:从 grades/lib/grade-utils 导入)" }, { "from": "elective", "to": "school", "type": "data-access", "description": "关联年级(grades)/科目(subjects)(P2-8:通过 resolvers.ts CourseDisplayResolver 接口抽象,默认实现委托 school data-access)" }, { "from": "elective", "to": "users", "type": "data-access", "description": "查询教师/学生显示名(P2-8:通过 resolvers.ts CourseDisplayResolver 接口抽象,默认实现委托 users data-access.getUserNamesByIds)" }, { "from": "elective", "to": "classes", "type": "data-access", "description": "查询学生所在年级(P2-8:通过 resolvers.ts StudentGradeResolver 接口抽象,默认实现委托 classes data-access.getStudentActiveGradeId)" }, { "from": "attendance", "to": "classes", "type": "data-access", "description": "查询班级与课表" }, { "from": "parent", "to": "grades", "type": "data-access", "description": "查询子女成绩" }, { "from": "parent", "to": "attendance", "type": "data-access", "description": "查询子女考勤" }, { "from": "parent", "to": "homework", "type": "data-access", "description": "查询子女作业" }, { "from": "scheduling", "to": "classes", "type": "data-access", "description": "排课关联班级" }, { "from": "course-plans", "to": "classes", "type": "data-access", "description": "课程计划关联班级" }, { "from": "course-plans", "to": "school", "type": "data-access", "description": "关联科目" }, { "from": "announcements", "to": "school", "type": "data-access", "description": "关联年级/班级" }, { "from": "shared", "to": "auth", "type": "resolved", "description": "✅ 已修复:shared/lib 通过 session.ts 单一入口获取 session(dynamic import 打破静态循环)" }, { "from": "auth", "to": "shared", "type": "normal", "description": "auth.ts 依赖 shared/lib(合理,单向)" }, { "from": "layout", "to": "shared", "type": "normal", "description": "使用 shared 组件与配置" }, { "from": "settings", "to": "shared", "type": "normal", "description": "使用 shared/lib/ai" }, { "from": "settings", "to": "messaging", "type": "normal", "description": "通知偏好设置(v1.1 已重构:settings/actions-service.ts 直接调用 notifications/preferences.upsertNotificationPreferences,不再依赖 messaging/actions.updateNotificationPreferencesAction)" }, { "from": "settings", "to": "users", "type": "normal", "description": "v1.1 新增:settings/actions-service.ts.updateProfileAction 委托 users/actions.updateUserProfile" }, { "from": "settings", "to": "notifications", "type": "normal", "description": "v1.1 新增:settings/actions-service.ts.updateNotificationPreferencesAction 调用 notifications/preferences.upsertNotificationPreferences" }, { "from": "audit", "to": "shared", "type": "normal", "description": "使用 shared/lib/audit-logger" }, { "from": "files", "to": "shared", "type": "normal", "description": "使用 shared/db" }, { "from": "users", "to": "classes", "type": "data-access", "description": "✅ P1-4 已修复:通过 class-registration.ts 调用 classes/data-access.enrollStudentByInvitationCode,不再直写 classEnrollments" }, { "from": "error-book", "to": "shared", "type": "normal", "description": "使用 shared/db、auth-guard、types" }, { "from": "error-book", "to": "classes", "type": "data-access", "description": "通过 classes/data-access.getStudentIdsByClassIds 查询班级学生" }, { "from": "error-book", "to": "questions", "type": "data-access", "description": "✅ 2026-06-24 解耦:add-error-book-dialog 不再直接 import getQuestionsAction,改为通过 questionOptions/onLoadQuestions props 注入。依赖上移至 app/(dashboard)/student/error-book/page.tsx", "status": "decoupled" }, { "from": "ai", "to": "shared", "type": "normal", "description": "使用 lib/ai.createAiChatCompletion、auth-guard.requirePermission、types.permissions、types.action-state" }, { "from": "ai", "to": "textbooks", "type": "normal", "description": "V3 新增:使用 data-access-graph.getKnowledgePointsWithRelations/getStudentKpMastery 获取知识图谱与掌握度,注入学习路径推荐" }, { "from": "homework", "to": "ai", "type": "context", "description": "通过 AiClientProvider 注入 AiClientService,使用 AiGradingAssist 组件" }, { "from": "error-book", "to": "ai", "type": "context", "description": "✅ 2026-06-24 解耦:error-book-detail-dialog 不再直接 import AiErrorBookAnalysis,改为通过 aiAnalysisSlot props 注入。依赖上移至 app/(dashboard)/student/error-book/page.tsx", "status": "decoupled" }, { "from": "lesson-preparation", "to": "ai", "type": "context", "description": "通过 AiClientProvider 注入 AiClientService,使用 AiLessonContentGenerator 组件" }, { "from": "exams", "to": "ai", "type": "context", "description": "通过 AiClientProvider 注入 AiClientService,使用 AiQuestionVariantGenerator 组件" }, { "from": "adaptive-practice", "to": "shared", "type": "normal", "description": "使用 db.schema(practiceSessions/practiceAnswers/questions/knowledgePoints)、auth-guard.requirePermission、types.permissions、types.action-state" }, { "from": "adaptive-practice", "to": "classes", "type": "data-access", "description": "使用 getActiveStudentIdsByClassId/getClassNameById/getClassesByGradeId/getClassIdsByGradeIds/getStudentIdsByClassIds 获取班级与学生数据" }, { "from": "adaptive-practice", "to": "users", "type": "data-access", "description": "使用 getUserIdsByGradeId/getUserNamesByIds 获取年级用户与学生姓名" }, { "from": "adaptive-practice", "to": "questions", "type": "data-access", "description": "出题策略使用 questions data-access 查询题目" }, { "from": "adaptive-practice", "to": "error-book", "type": "data-access", "description": "错题变式策略使用 error-book data-access 获取错题数据" }, { "from": "error-book", "to": "adaptive-practice", "type": "action", "description": "错题详情弹窗调用 createPracticeSessionAction 发起变式练习" }, { "from": "search", "to": "shared", "type": "normal", "description": "使用 db/schema(questions/textbooks/exams/announcements 表)" } ] }, "knownIssues": [ { "id": "P0-1", "severity": "P0", "title": "classes/data-access.ts 超过 1000 行", "file": "src/modules/classes/data-access.ts", "lines": 2104, "problem": "混入 homework/scheduling/grades 逻辑,严重违反模块职责单一原则", "suggestion": "按职责拆分为 class-query/schedule/homework-insights/grade-query", "status": "resolved", "resolvedAt": "2026-06-17", "resolution": "拆分为 5 个文件:data-access.ts(548行,核心CRUD+邀请码+教师班级管理) + data-access-stats.ts(531行,作业统计) + data-access-schedule.ts(194行,课表) + data-access-students.ts(244行,学生查询) + data-access-admin.ts(406行,管理员班级管理),所有文件均 ≤800 行,data-access.ts 通过 re-export 保持向后兼容" }, { "id": "P0-2", "severity": "P0", "title": "homework/data-access.ts 超过 1000 行", "file": "src/modules/homework/data-access.ts", "lines": 1038, "problem": "混入排名计算业务逻辑", "suggestion": "分离排名逻辑到独立文件(如 data-access-ranking.ts)", "status": "resolved", "resolvedAt": "2026-06-17", "resolution": "拆分为 data-access.ts(598行) + stats-service.ts(425行),统计函数(getTeacherGradeTrends/getHomeworkAssignmentAnalytics/getStudentDashboardGrades)迁移至 stats-service.ts,data-access.ts 通过 re-export 保持向后兼容" }, { "id": "P0-3", "severity": "P0", "title": "shared/lib ↔ auth 循环依赖", "file": "src/shared/lib/{audit-logger,change-logger,auth-guard}.ts ↔ src/auth.ts", "problem": "shared/lib 依赖 @/auth(src/auth.ts),auth.ts 又依赖 shared/lib,形成循环依赖", "suggestion": "拆分 auth.ts,将 shared 依赖部分抽出为独立模块", "status": "resolved", "resolvedAt": "2026-06-18", "resolution": "新增 shared/lib/session.ts 单一入口封装 getSession()(server-only,内部 dynamic import @/auth 打破模块级静态循环)。audit-logger.ts/change-logger.ts/auth-guard.ts 改为 import { getSession } from '@/shared/lib/session',不再直接依赖 @/auth。运行时调用链保持不变,模块加载图无环。" }, { "id": "P0-4", "severity": "P0", "title": "dashboard 跨模块直接查询 11 张表", "file": "src/modules/dashboard/data-access.ts", "problem": "getAdminDashboardData 直查 sessions/users/classes/textbooks/chapters/questions/exams/homeworkAssignments/homeworkSubmissions/usersToRoles/roles,严重违反模块封装", "suggestion": "改为通过各模块 data-access 获取数据", "status": "fixed", "fixedBy": "新增 getUsersDashboardStats/getClassesDashboardStats/getTextbooksDashboardStats/getQuestionsDashboardStats/getExamsDashboardStats/getHomeworkDashboardStats,dashboard 改为并行调用各模块 stats 函数" }, { "id": "P0-5", "severity": "P0", "title": "messaging 绕过 notifications 直接写通知", "file": "src/modules/messaging/actions.ts", "lines": "66-72", "problem": "直接调用 createNotification,导致用户通知偏好失效、多渠道通知无效", "suggestion": "改为通过 notifications dispatcher 统一分发", "status": "resolved", "resolvedAt": "2026-06-17", "resolution": "将 messageNotifications 和 notificationPreferences 表的 CRUD 函数从 messaging 迁移到 notifications 模块;notifications/channels/in-app-channel.ts 改为静态导入本地 createNotification(不再动态 import messaging);notifications/dispatcher.ts 改为从本地 preferences.ts 导入偏好函数;messaging 模块通过 re-export 保持向后兼容;依赖方向变为单向:messaging → notifications(messaging 调用 sendNotification dispatcher 发送通知)" }, { "id": "P0-6", "severity": "P0", "title": "classSchedule 表三处写入口", "file": "src/modules/classes/data-access.ts, src/modules/scheduling/actions.ts, src/modules/scheduling/data-access.ts", "problem": "三处独立写入 classSchedule 表,数据完整性高风险", "suggestion": "统一写入口到 scheduling 模块", "status": "fixed", "fixedBy": "将 classSchedule 写函数(createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem)从 classes/data-access-schedule.ts 迁移到新文件 scheduling/data-access-class-schedule.ts;classes 模块仅保留 READ 函数(getStudentSchedule/getClassSchedule);新增 classes/data-access.verifyTeacherOwnsClass 供 scheduling 模块跨模块校验教师班级归属;classes/actions.ts 改为从 @/modules/scheduling/data-access-class-schedule 导入写函数;类型 CreateClassScheduleItemInput/UpdateClassScheduleItemInput 从 classes/types.ts 迁移到 scheduling/types.ts;所有 classSchedule DB 写入统一由 scheduling 模块管理" }, { "id": "P0-7", "severity": "P0", "title": "classes 模块直查 homework/exams 表违反三层架构", "file": "src/modules/classes/data-access-stats.ts, src/modules/classes/data-access-students.ts", "problem": "data-access-stats.ts 和 data-access-students.ts 直接导入并查询 homeworkAssignmentQuestions/homeworkAssignmentTargets/homeworkAssignments/homeworkSubmissions/exams 表,违反 modules/ 不直接查询其他模块 DB 表的规则", "suggestion": "在 homework 模块新增 data-access-classes.ts 暴露所需数据,classes 模块改为调用这些函数", "status": "resolved", "resolvedAt": "2026-06-18", "resolution": "新增 src/modules/homework/data-access-classes.ts(232行,7个函数:getAssignmentIdsForStudents/getHomeworkAssignmentsWithSubject/getHomeworkAssignmentsByIds/getAssignmentTargetCounts/getHomeworkSubmissionsForStudents/getPublishedHomeworkAssignmentsWithSubject/getHomeworkSubmissionsForAssignments,re-export getAssignmentMaxScoreById);classes/data-access-stats.ts 和 data-access-students.ts 改为调用这些函数,移除对 homework/exams 表的直接导入;同时修复 P1 问题:移除 4 处 `as string` 断言(data-access-stats.ts),将 `studentScores.get(s.studentId)!` 非空断言替换为显式 null 检查(data-access-students.ts)" }, { "id": "P0-8", "severity": "P0", "title": "exams/data-access.ts 超过 1000 行", "file": "src/modules/exams/data-access.ts", "lines": 1036, "problem": "data-access.ts 1036 行超过 1000 行硬上限,混合考试 CRUD 与跨模块查询接口", "suggestion": "按职责拆分,将跨模块接口迁移到独立文件", "status": "resolved", "resolvedAt": "2026-06-24", "resolution": "拆分为 data-access.ts(542行,考试 CRUD + 内部辅助函数) + data-access-cross-module.ts(462行,13 个跨模块查询/写接口:getExamsDashboardStats/getExamIdsByGradeIds/getExamWithQuestionsForHomework/getExamSubjectIdMap/getExamTitleById/getExamForProctoringCrossModule/getExamSubmissionForProctoringCrossModule/getExamSubmissionWithAnswers/getExamSubmissionsForExam/addExamQuestions/getExamsByGradeId/getExamsForGradeEntry/getExamForGradeEntry)。新文件标记 server-only,从 data-access.ts 导入 parseExamMeta/getString/getNumber/toExamStatus 复用;data-access.ts 文末 re-export 全部跨模块接口保持向后兼容。所有调用方无需修改 import 路径。" }, { "id": "P1-1", "severity": "P1", "title": "跨模块直接 DB 查询普遍存在", "problem": "classes(8+)/classEnrollments(6+)/users(6+)/subjects(6+)/exams(5+) 表被多个模块直接查询", "suggestion": "建立模块间数据访问规范,通过对方 data-access 或导出查询函数", "status": "resolved", "resolvedAt": "2026-06-18", "resolution": "所有跨模块直查已改为通过对方 data-access 接口:exams 通过 school data-access.getSubjectOptions/getGradeOptions;questions 通过 textbooks data-access.getKnowledgePointOptions;homework 通过 exams/classes/school/users data-access;grades 通过 classes/school/users data-access;classes 通过 school/homework data-access;attendance 通过 classes data-access;scheduling 通过 users data-access;notifications 通过 classes data-access;proctoring 通过 exams/users data-access;diagnostic 通过 exams/questions/classes/users data-access;dashboard 通过各模块 data-access;users updateUserProfile 已下沉到 data-access。各模块 data-access 暴露的查询接口:classes(getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId/verifyTeacherOwnsClass/getTeacherIdForMutations 等)、exams(getExamIdsByGradeIds/getExamSubjectIdMap/getExamWithQuestionsForHomework/getExamSubmissionWithAnswers/getExamForProctoringCrossModule 等)、school(getSubjectOptions/getGradeOptions)、users(getUserNamesByIds/getUserWithRole/getUserBasicInfo/getUserIdsByGradeId/getCurrentStudentUser)、textbooks(getKnowledgePointOptions)、questions(createQuestionWithRelations/getKnowledgePointsForQuestions)、homework/data-access-classes(7 个函数供 classes 模块跨模块调用)" }, { "id": "P1-2", "severity": "P1", "title": "actions 层混入数据访问逻辑", "file": "src/modules/{exams,homework,questions,announcements}/actions.ts", "problem": "actions.ts 中存在直接 db.insert/update/delete,应通过 data-access 层", "suggestion": "将 DB 操作下沉到 data-access 层", "status": "resolved", "resolvedAt": "2026-06-17", "resolution": "4 个模块的 actions 层 DB 操作全部下沉到 data-access:exams 新增 7 个 data-access 函数(actions.ts 831→691 行,data-access.ts 374→471 行);homework 新建 data-access-write.ts(285 行,10 个写函数,actions.ts 387→239 行);questions 新增 4 个 data-access 函数(actions.ts 294→149 行,data-access.ts 138→260 行);announcements 新增 5 个 data-access 函数(actions.ts 242→197 行,data-access.ts 120→171 行)。users/scheduling 待后续处理" }, { "id": "P1-3", "severity": "P1", "title": "auth.ts 混合 5 类职责", "file": "src/auth.ts", "problem": "NextAuth 配置 + 密码安全 DB 操作 + 角色规范化 + IP 解析 + 回调函数混合", "suggestion": "拆分为 auth-config/password-security/role-normalizer/ip-utils 等多文件", "status": "resolved", "resolution": "已拆分:密码安全DB操作→shared/lib/password-security-service.ts,角色规范化→shared/lib/role-utils.ts,bcrypt哈希规范化→shared/lib/bcrypt-utils.ts,IP解析→shared/lib/http-utils.ts。auth.ts 现 193 行仅保留 NextAuth 配置" }, { "id": "P1-4", "severity": "P1", "title": "users/import-export.ts 四重职责", "file": "src/modules/users/import-export.ts", "problem": "导入解析 + 导出 + 用户创建(含密码哈希) + 班级注册(跨模块写 classEnrollments)", "suggestion": "按职责拆分为 import-parser/exporter/user-creator/enrollment", "status": "resolved", "resolvedAt": "2026-06-17", "resolution": "已拆分:import-export.ts(157行,仅保留文件解析/生成) + user-service.ts(82行,batchImportUsers 用户创建+密码哈希+角色分配) + class-registration.ts(21行,registerStudentByInvitationCode 委托 classes/data-access.enrollStudentByInvitationCode)。batchImportUsers 不再直写 classEnrollments,改为调用 classes/data-access.enrollStudentByInvitationCode。import-export.ts 通过 re-export batchImportUsers/UserImportResult 保持向后兼容" }, { "id": "P1-5", "severity": "P1", "title": "proctoring 死代码", "file": "src/modules/proctoring/components/exam-mode-config.tsx", "problem": "组件已创建但未集成到考试表单,DB schema 有 examMode 字段但表单不收集;事件上报存在 Server Action 与 REST API 双通道重复", "suggestion": "集成 proctoring/exam-mode-config 到考试表单;删除重复的 REST API 路由", "status": "partially_fixed", "fixedBy": "删除 /api/proctoring/event REST 路由(移至 deletes/),Server Action recordProctoringEventAction 为唯一规范路径;exam-mode-config.tsx 集成属于功能新增,暂保留原位" }, { "id": "P2-1", "severity": "P2", "title": "schema.ts 54 张表混合(1111 行)", "file": "src/shared/db/schema.ts", "lines": 1111, "problem": "所有表定义混合在单文件,虽可接受但需分节", "suggestion": "按业务域分节(加注释分隔)或拆分为多文件" }, { "id": "P2-2", "severity": "P2", "title": "ai.ts 混合 5 类职责(218 行)", "file": "src/shared/lib/ai.ts", "lines": 218, "problem": "AI 调用 + Provider 配置 + API Key 加密 + 错误格式化 + 负载解析混合在单文件", "suggestion": "按职责拆分为 ai/ 目录多文件", "status": "resolved", "resolvedAt": "2026-06-17", "resolution": "已拆分为 src/shared/lib/ai/ 目录:payload-parser.ts(78 行,请求负载解析) + api-key-crypto.ts(28 行,API Key 加密/解密) + provider-config.ts(61 行,Provider 配置查询) + client.ts(58 行,AI 客户端创建与调用) + errors.ts(8 行,错误格式化) + index.ts(5 行,聚合导出)。原 ai.ts 保留为向后兼容的重导出文件(9 行)" }, { "id": "P1-6", "severity": "P1", "title": "三个 logger 重复实现 IP/Header 提取", "file": "src/shared/lib/{audit-logger,change-logger,login-logger}.ts, src/auth.ts", "problem": "audit-logger.ts / change-logger.ts / login-logger.ts / auth.ts 四处重复实现 IP/User-Agent 提取逻辑,且实现略有差异", "suggestion": "提取 shared/lib/http-utils.ts 统一 IP/UA 提取", "status": "resolved", "resolvedAt": "2026-06-18", "resolution": "shared/lib/http-utils.ts 新增 getUserAgent() 函数(与已有 resolveClientIp() 配套);audit-logger.ts / change-logger.ts / login-logger.ts 改为从 @/shared/lib/http-utils 导入 resolveClientIp 和 getUserAgent,删除本地重复实现;auth.ts 已在 P1-3 中改用 resolveClientIp。四处实现统一,消除不一致风险(resolveClientIp 取 x-forwarded-for 第一段,更准确)" }, { "id": "P2-20", "severity": "P2", "title": "homework/data-access.getDemoStudentUser 使用 auth() 而非 auth-guard", "file": "src/modules/homework/data-access.ts", "problem": "getDemoStudentUser 直接调用 auth() 获取学生身份,绕过 shared/lib/auth-guard 的统一认证上下文,且导致 student 页面虚假依赖 homework 模块", "suggestion": "迁移到 users 模块 getCurrentStudentUser,使用 auth-guard.getAuthContext", "status": "resolved", "resolvedAt": "2026-06-18", "resolution": "getDemoStudentUser 从 homework 模块迁移至 users 模块 getCurrentStudentUser(通过 session + JOIN users/usersToRoles/roles 校验 student 角色);6 个 student 页面(dashboard/assignments/assignments-[assignmentId]/courses/textbooks/textbooks-[id]/schedule)改用 users 模块,消除对 homework 的虚假依赖;student/elective 改用 getAuthContext();homework 保留 re-export 向后兼容" }, { "id": "v4-P2-6", "severity": "P2", "title": "年级管理模块改进:概览卡片视图 + 洞察筛选器 + assertClassInScope 迁移", "file": "src/modules/school/data-access.ts, src/modules/school/components/grade-insights-filters.tsx, src/modules/school/components/grades-view.tsx, src/modules/grades/lib/scope-check.ts, src/app/(dashboard)/admin/school/grades/page.tsx, src/app/(dashboard)/admin/school/grades/insights/page.tsx, src/app/(dashboard)/management/grade/insights/page.tsx", "problem": "(1) 年级管理页面缺少概览视图,管理员难以一目了然看到各年级规模;(2) 年级洞察页面使用原生 form get 切换年级导致整页刷新;(3) grades/actions.ts 中的 assertClassInScope 是同步函数但位于 \"use server\" 文件中,Next.js 要求 \"use server\" 文件所有 export 必须为 async,会导致构建错误", "suggestion": "新增 getGradeOverviewStats data-access 函数 + GradesClient 卡片视图;新增 GradeInsightsFilters 组件使用 ChipNav;将 assertClassInScope 迁移至独立 lib/scope-check.ts 文件", "status": "resolved", "resolvedAt": "2026-06-23", "resolution": "(1) 新增 getGradeOverviewStats() data-access 函数(返回 GradeOverviewStats[]:每个年级的 classCount/studentCount/teacherCount,动态导入 classes/data-access 避免循环依赖)+ GradeOverviewStats 接口;GradesClient 新增 gradeStats prop,渲染年级概览卡片视图(班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口);admin/school/grades/page.tsx 新增 getGradeOverviewStats() 查询并传入 GradesClient。(2) 新增 GradeInsightsFilters 组件(使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新);admin/school/grades/insights/page.tsx 与 management/grade/insights/page.tsx 重写,使用 GradeInsightsFilters 替代原生 form get,添加 i18n,表格添加 overflow-x-auto。(3) 将 assertClassInScope 从 actions.ts 迁移至独立文件 lib/scope-check.ts(含 import \"server-only\"),actions.ts 与 actions-analytics.ts 均从 ./lib/scope-check 导入。school.json(en + zh-CN)新增 grades.gradeInsights 和 grades.gradeOverview 两个章节 i18n 键" } ], "routes": { "pwa": { "/manifest.webmanifest": { "component": "manifest (MetadataRoute.Manifest)", "type": "metadata", "module": "app", "description": "M7 新增(2026-07-01):PWA Web App Manifest 元数据路由(src/app/manifest.ts)" } }, "auth": { "/login": { "component": "LoginForm", "type": "client", "module": "auth" }, "/register": { "component": "RegisterForm + registerAction", "type": "server", "module": "auth", "description": "注册页面(含未成年人信息保护、隐私政策/用户协议同意勾选)" }, "/privacy": { "component": "PrivacyPage", "type": "server", "module": "auth", "description": "隐私政策页面(信息收集/使用/保护、用户权利、Cookie、未成年人保护条款、联系方式)" }, "/terms": { "component": "TermsPage", "type": "server", "module": "auth", "description": "用户协议页面(服务说明、注册、行为规范、知识产权、免责、变更终止、法律适用)" } }, "onboarding": { "/onboarding": { "component": "OnboardingPage + OnboardingStepper", "type": "server", "module": "onboarding", "method": "GET", "actions": [ "getOnboardingStatusAction", "completeOnboardingAction" ], "dataAccess": [ "onboarding/data-access.getOnboardingStatus", "onboarding/data-access.updateUserProfile", "onboarding/data-access.bindParentToChild", "shared/lib/route-resolver.resolveDefaultPath", "classes/data-access.enrollStudentByInvitationCode", "classes/data-access.enrollTeacherByInvitationCode" ], "auth": "required", "description": "首次登录引导(独立路由 + middleware 重定向 + Server Action + Zod + 事务)" } }, "admin": { "_layout": { "file": "app/(dashboard)/admin/layout.tsx", "type": "server", "purpose": "admin 路由组统一权限守卫:调用 getAuthContext() 校验已登录,未登录抛 PermissionDeniedError;细粒度权限由各页面 requirePermission() 自行检查", "authGuard": "shared/lib/auth-guard.getAuthContext", "auth": "required" }, "/admin/dashboard": { "component": "AdminDashboardView", "type": "server", "dataAccess": [ "dashboard/data-access.getAdminDashboardData" ], "permission": "school:manage" }, "/admin/school": { "component": "重定向", "type": "server", "redirect": "/admin/school/classes", "permission": "school:manage" }, "/admin/school/schools": { "component": "SchoolsClient", "type": "client", "module": "school", "permission": "school:manage" }, "/admin/school/grades": { "component": "GradesClient", "type": "client", "module": "school", "dataAccess": [ "school/data-access.getGrades", "school/data-access.getSchools", "school/data-access.getStaffOptions", "school/data-access.getGradeOverviewStats" ], "permission": "school:manage", "description": "v4-P2-6 更新:新增 getGradeOverviewStats 查询,传入 gradeStats 到 GradesClient 渲染年级概览卡片视图" }, "/admin/school/grades/insights": { "component": "年级作业洞察 + SchoolWideSummaryCard + GradeInsightsFilters", "type": "server", "dataAccess": [ "classes/data-access.getGradeHomeworkInsights", "grades/data-access-analytics.getSchoolWideGradeSummary" ], "permission": "school:manage", "description": "v3-P2 更新:顶部新增 SchoolWideSummaryCard 全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格);v4-P2-6 更新:重写页面使用 GradeInsightsFilters 替代原生 form get(ChipNav 无整页刷新),添加 i18n,表格添加 overflow-x-auto" }, "/admin/school/departments": { "component": "DepartmentsClient", "type": "client", "module": "school", "permission": "school:manage" }, "/admin/school/classes": { "component": "AdminClassesClient", "type": "client", "module": "classes", "permission": "school:manage" }, "/admin/school/academic-year": { "component": "AcademicYearClient", "type": "client", "module": "school", "permission": "school:manage" }, "/admin/audit-logs": { "component": "AuditLogView", "type": "server", "module": "audit", "dataAccess": [ "audit/data-access.getAuditLogs", "audit/data-access.getAuditModuleOptions" ], "permission": "audit_log:read" }, "/admin/audit-logs/login-logs": { "component": "LoginLogView", "type": "server", "module": "audit", "dataAccess": [ "audit/data-access.getLoginLogs" ], "permission": "audit_log:read" }, "/admin/announcements": { "component": "AdminAnnouncementsView", "type": "server", "module": "announcements", "dataAccess": [ "announcements/data-access.getAnnouncements", "school/data-access.getGrades", "classes/data-access.getAdminClasses" ], "actions": [ "createAnnouncementAction" ], "permission": "announcement:manage" }, "/admin/announcements/[id]": { "component": "AnnouncementDetail (canManage=true)", "type": "server", "module": "announcements", "dataAccess": [ "announcements/data-access.getAdminAnnouncementDetailPageData" ], "actions": [ "publishAnnouncementAction", "archiveAnnouncementAction", "deleteAnnouncementAction", "toggleAnnouncementPinAction" ], "permission": "announcement:manage", "description": "P1-2 重构:管理端详情页(含发布/归档/删除/置顶操作)。原为编辑表单页面,现拆分为详情页 + /edit 子路由。使用 getAdminAnnouncementDetailPageData 编排函数(含 readCount 统计)。包裹 AnnouncementsServiceProvider" }, "/admin/announcements/[id]/edit": { "component": "AnnouncementForm (edit)", "type": "server", "module": "announcements", "dataAccess": [ "announcements/data-access.getAnnouncementById", "school/data-access.getGrades" ], "actions": [ "updateAnnouncementAction" ], "permission": "announcement:manage", "description": "P1-2 新增:从原 /admin/announcements/[id] 拆分出的编辑表单页面" }, "/admin/files": { "component": "AdminFilesView", "type": "server", "module": "files", "actions": [ "files/actions.getFileListAction", "files/actions.getFileStatsAction" ], "permission": "file:read" }, "/admin/course-plans": { "component": "CoursePlanList", "type": "client", "module": "course-plans", "dataAccess": [ "course-plans/data-access.getCoursePlans" ], "permission": "course_plan:manage" }, "/admin/course-plans/create": { "component": "CoursePlanForm (create)", "type": "client", "module": "course-plans", "actions": [ "createCoursePlanAction" ], "dataAccess": [ "classes/data-access.getAdminClasses", "course-plans/data-access.getSubjectOptions", "classes/data-access.getStaffOptions", "school/data-access.getAcademicYears" ], "permission": "course_plan:manage" }, "/admin/course-plans/[id]": { "component": "CoursePlanDetail", "type": "client", "module": "course-plans", "dataAccess": [ "course-plans/data-access.getCoursePlanById" ], "actions": [ "deleteCoursePlanAction", "createCoursePlanItemAction", "updateCoursePlanItemAction", "deleteCoursePlanItemAction", "toggleCoursePlanItemCompletedAction" ], "permission": "course_plan:manage" }, "/admin/course-plans/[id]/edit": { "component": "CoursePlanForm (edit)", "type": "client", "module": "course-plans", "actions": [ "updateCoursePlanAction" ], "dataAccess": [ "course-plans/data-access.getCoursePlanById", "classes/data-access.getAdminClasses", "course-plans/data-access.getSubjectOptions", "classes/data-access.getStaffOptions", "school/data-access.getAcademicYears" ], "permission": "course_plan:manage" }, "/admin/attendance": { "component": "AttendanceRecordList", "type": "server", "module": "attendance", "dataAccess": [ "attendance/data-access.getAttendanceRecords (scope=all)", "classes/data-access.getAdminClasses" ], "permission": "attendance:manage", "description": "管理员考勤总览(权限:requirePermission(ATTENDANCE_MANAGE))" }, "/admin/users/import": { "component": "UserImportPage (含 UserImportDialog)", "type": "server", "module": "users", "actions": [ "users/actions.downloadUserTemplateAction", "users/actions.importUsersAction" ], "permission": "user:manage", "description": "用户批量导入页面(说明卡片+字段文档表+导入对话框;权限:requirePermission(USER_MANAGE))" }, "/admin/users": { "component": "AdminUsersPage (含 AdminUsersView)", "type": "server", "module": "users", "dataAccess": [ "users/data-access.getAdminUsers", "users/data-access.getAdminUserRoles" ], "actions": [ "users/actions.updateUserRoleAction", "users/actions.deleteUserAction" ], "permission": "user:manage", "description": "管理员用户列表页面(搜索+角色筛选+分页表格+删除;权限:requirePermission(USER_MANAGE))" }, "/admin/invitation-codes": { "component": "AdminInvitationCodesPage (含 InvitationCodesView + GenerateInvitationCodesDialog)", "type": "server", "module": "invitation-codes", "dataAccess": [ "invitation-codes/data-access.listInvitationCodes" ], "actions": [ "invitation-codes/actions.generateInvitationCodesAction", "invitation-codes/actions.deleteInvitationCodeAction" ], "permission": "user:manage", "description": "管理员邀请码管理页(audit-P2-3 新增:统计卡片+表格+生成对话框+删除;权限:requirePermission(USER_MANAGE);Server Component 计算 now 传入 Client 避免客户端 Date.now() 调用)" }, "/admin/scheduling/rules": { "component": "SchedulingRulesForm", "type": "server", "module": "scheduling", "dataAccess": [ "scheduling/actions.getAdminClassesForScheduling", "scheduling/actions.getSchedulingRules" ], "actions": [ "saveSchedulingRulesAction" ], "permission": "schedule:adjust", "description": "排课规则配置页面(权限:requirePermission(SCHEDULE_ADJUST))" }, "/admin/scheduling/auto": { "component": "AutoSchedulePanel + AutoScheduleResultView", "type": "server", "module": "scheduling", "dataAccess": [ "scheduling/actions.getAdminClassesForScheduling" ], "actions": [ "autoScheduleAction", "applyAutoScheduleAction" ], "permission": "schedule:auto", "description": "自动排课页面(预览+应用;权限:requirePermission(SCHEDULE_AUTO))" }, "/admin/scheduling/changes": { "component": "ScheduleChangeList + ScheduleConflictsView", "type": "server", "module": "scheduling", "dataAccess": [ "scheduling/actions.getAdminClassesForScheduling", "scheduling/actions.getScheduleChanges" ], "actions": [ "approveScheduleChangeAction", "rejectScheduleChangeAction", "getClassConflictsAction" ], "permission": "schedule:adjust", "description": "调课申请审批+冲突检测页面(权限:requirePermission(SCHEDULE_ADJUST);审批操作需 SCHEDULE_AUTO)" }, "/admin/elective": { "component": "StatsCardsLoader (Suspense) + CourseListLoader (Suspense)", "type": "server", "module": "elective", "dataAccess": [ "elective/data-access.getElectiveCourses (scope=all)", "elective/data-access-stats.getElectiveOverviewStats" ], "actions": [ "deleteElectiveCourseAction", "openSelectionAction", "closeSelectionAction", "runLotteryAction" ], "permission": "elective:read", "description": "管理员选修课程列表 + 4 项统计概览(P0-3:入口 requirePermission(ELECTIVE_READ) 纵深防御;P1-13:新增统计卡片;P2-3:拆分为两个独立 Suspense 边界流式渲染)" }, "/admin/elective/create": { "component": "ElectiveCourseForm", "type": "client", "module": "elective", "actions": [ "createElectiveCourseAction" ], "dataAccess": [ "elective/data-access.getSubjectOptions" ], "permission": "elective:manage", "description": "创建选修课程(权限:requirePermission(ELECTIVE_MANAGE))" }, "/admin/elective/[id]/edit": { "component": "ElectiveCourseForm (edit)", "type": "client", "module": "elective", "actions": [ "updateElectiveCourseAction" ], "dataAccess": [ "elective/data-access.getElectiveCourseById", "elective/data-access.getSubjectOptions" ], "permission": "elective:manage", "description": "编辑选修课程(权限:requirePermission(ELECTIVE_MANAGE))" }, "/admin/settings": { "component": "AdminSettingsView", "type": "client", "module": "settings", "permission": "settings:admin", "description": "系统设置页面(学校信息/安全策略/文件上传/通知配置;权限:requirePermission(SETTINGS_ADMIN))" }, "/admin/ai-settings": { "component": "AiProviderSettingsCard (+ AiUsageDashboard for admin)", "type": "server", "module": "settings+ai", "permission": "ai:chat", "description": "AI 统一配置页(V3 新增:取代 /settings?tab=ai 和考试页面内嵌弹窗;V3.1 增强:AI_CHAT 用户均可访问管理自己的 private provider,AI_CONFIGURE 管理员可管理 public provider 与他人 private provider;权限:requirePermission(AI_CHAT),isAdmin=AI_CONFIGURE 控制公开选项)" }, "/admin/error-book": { "component": "AdminErrorBookPage + SubjectTabs + AnalyticsStatsCards + SubjectDistributionChart + KnowledgePointWeaknessChart + ChapterWeaknessChart + GroupedStudentErrorTable + TopWrongQuestions", "type": "server", "module": "error-book", "permission": "error_book:analytics_read", "description": "管理员错题分析页面(全校错题统计/薄弱知识点/学科分布/高频错题;权限:requirePermission(ERROR_BOOK_ANALYTICS_READ))" }, "/admin/lesson-plans": { "component": "LessonPlanList (viewMode=admin) + 统计卡片", "type": "server", "module": "lesson-preparation", "dataAccess": [ "lesson-preparation/data-access.getLessonPlans (scope=all)" ], "permission": "lesson_plan:read", "description": "V3 新增:管理员课案列表 + 统计卡片(全校课案总览;权限:requirePermission(LESSON_PLAN_READ);DataScope.all)" }, "/admin/lesson-plans/[planId]/view": { "component": "LessonPlanReadonlyView", "type": "server", "module": "lesson-preparation", "dataAccess": [ "lesson-preparation/data-access.getLessonPlanById" ], "permission": "lesson_plan:read", "description": "V3 新增:管理员查看课案详情(只读画布;权限:requirePermission(LESSON_PLAN_READ))" }, "/admin/curriculum-map": { "component": "CurriculumMapView", "type": "server", "module": "standards", "method": "GET", "description": "M4 课程地图热力图页面(2026-07-01 新增):学科×年级矩阵表格,颜色编码覆盖率(权限:requirePermission(STANDARD_READ))" } }, "teacher": { "/teacher/dashboard": { "component": "TeacherDashboardView", "type": "server", "dataAccess": [ "dashboard/data-access (teacher)", "homework/data-access.getTeacherGradeTrends", "classes/data-access.getTeacherClasses" ], "permission": "exam:read" }, "/teacher/exams/all": { "component": "ExamDataTable", "type": "server", "dataAccess": [ "exams/data-access.getExams" ], "permission": "exam:read" }, "/teacher/exams/create": { "component": "ExamForm", "type": "client", "actions": [ "createExamAction", "createAiExamAction", "previewAiExamAction" ], "permission": "exam:create" }, "/teacher/exams/new": { "component": "NewExamPage + ExamRichForm", "type": "server", "module": "exams", "actions": [ "autoMarkExamAction", "createExamFromRichEditorAction", "getSubjectsAction", "getGradesAction" ], "permission": "exam:create", "description": "2026-06-24 新增:富文本编辑器试卷创建页面。基于 Tiptap 的所见即所得试卷编辑器,支持 AI 自动标记(粘贴试卷文本 → AI 解析为 Tiptap doc)、手动编辑题目块/分组/填空/加点字/图片、左编辑右预览分栏布局(ResizablePanel)。保存时将 Tiptap JSONContent 转换为 questions + structure 后持久化为试卷草稿。" }, "/teacher/questions": { "component": "QuestionDataTable", "type": "server", "dataAccess": [ "questions/data-access.getQuestions" ], "permission": "question:read" }, "/teacher/textbooks": { "component": "TextbookList", "type": "server", "dataAccess": [ "textbooks/data-access.getTextbooks" ], "permission": "textbook:read" }, "/teacher/textbooks/[id]": { "component": "TextbookReader", "type": "client", "dataAccess": [ "textbooks/data-access.getTextbookById", "getChaptersByTextbookId", "getKnowledgePointsByTextbookId" ], "permission": "textbook:read" }, "/teacher/classes/my": { "component": "ClassList", "type": "server", "dataAccess": [ "classes/data-access.getTeacherClasses" ], "permission": "class:read", "generateMetadata": "✅ P2 修复新增:page.tsx 导出 generateMetadata,通过 next-intl getTranslations 生成页面标题", "errorFile": "error.tsx(✅ 2026-06-26 审计修复新增:使用 shared ErrorState + namespace=\"classes\")" }, "/teacher/classes/schedule": { "component": "ClassSchedule", "type": "server", "dataAccess": [ "classes/data-access.getClassSchedule" ], "permission": "class:read", "generateMetadata": "✅ P2 修复新增:page.tsx 导出 generateMetadata,通过 next-intl getTranslations 生成页面标题", "errorFile": "error.tsx(✅ 2026-06-26 审计修复新增:使用 shared ErrorState + namespace=\"classes\")" }, "/teacher/classes/students": { "component": "ClassStudents", "type": "server", "dataAccess": [ "classes/data-access.getClassStudents" ], "permission": "class:read", "generateMetadata": "✅ P2 修复新增:page.tsx 导出 generateMetadata,通过 next-intl getTranslations 生成页面标题", "errorFile": "error.tsx(✅ 2026-06-26 审计修复新增:使用 shared ErrorState + namespace=\"classes\")" }, "/teacher/classes": { "component": "重定向", "type": "server", "redirect": "/teacher/classes/my", "permission": "class:read" }, "/teacher/classes/my/[id]": { "component": "班级详情", "type": "client", "module": "classes", "dataAccess": [ "classes/data-access.getClassStudents", "classes/data-access.getClassSchedule", "classes/data-access.getClassHomeworkInsights" ], "permission": "class:read", "generateMetadata": "✅ P2 修复新增:page.tsx 导出 generateMetadata,通过 next-intl getTranslations 生成页面标题", "errorFile": "error.tsx(✅ 2026-06-26 审计修复新增:使用 shared ErrorState + namespace=\"classes\")", "loadingFile": "loading.tsx(✅ 2026-06-26 审计修复新增:班级详情加载骨架)" }, "/teacher/homework": { "component": "重定向", "type": "server", "redirect": "/teacher/homework/assignments", "permission": "homework:create" }, "/teacher/homework/assignments": { "component": "作业列表", "type": "server", "module": "homework", "dataAccess": [ "homework/data-access.getHomeworkAssignments" ], "permission": "homework:create" }, "/teacher/homework/assignments/create": { "component": "HomeworkAssignmentForm", "type": "client", "module": "homework", "actions": [ "createHomeworkAssignmentAction" ], "permission": "homework:create" }, "/teacher/homework/assignments/[id]": { "component": "作业详情+错误分析", "type": "client", "module": "homework", "dataAccess": [ "homework/data-access.getHomeworkAssignmentById", "homework/data-access.getHomeworkAssignmentAnalytics" ], "permission": "homework:create" }, "/teacher/homework/assignments/[id]/submissions": { "component": "作业提交列表", "type": "server", "module": "homework", "dataAccess": [ "homework/data-access.getHomeworkSubmissions" ], "permission": "homework:create" }, "/teacher/homework/submissions": { "component": "批改列表", "type": "server", "module": "homework", "dataAccess": [ "homework/data-access.getHomeworkAssignmentReviewList" ], "permission": "homework:grade" }, "/teacher/homework/submissions/[submissionId]": { "component": "HomeworkGradingView", "type": "client", "module": "homework", "actions": [ "gradeHomeworkSubmissionAction" ], "dataAccess": [ "homework/data-access.getHomeworkSubmissionDetails" ], "permission": "homework:grade" }, "/teacher/homework/submissions/[submissionId]/scan-grading": { "component": "HomeworkScanGradingPage + HomeworkScanGradingView", "type": "server", "module": "homework", "actions": [ "gradeHomeworkSubmissionAction", "getScansAction" ], "dataAccess": [ "homework/data-access.getHomeworkSubmissionDetails" ], "permission": "homework:grade", "description": "2026-06-24 新增:教师阅卷式批改页面。左侧显示题目+批改表单,右侧显示学生答题扫描图(ScanImageViewer,支持翻页/缩放/旋转/全屏)。使用 ResizablePanel 可拖拽分栏。支持上一份/下一份提交快速切换。适用于学生纸上作答后上传扫描图的场景。" }, "/teacher/exams": { "component": "重定向", "type": "server", "redirect": "/teacher/exams/all", "permission": "exam:read" }, "/teacher/exams/[id]/build": { "component": "ExamAssembly", "type": "client", "module": "exams", "permission": "exam:update" }, "/teacher/exams/[id]/analytics": { "component": "ExamAnalyticsDashboard", "type": "server", "module": "exams", "dataAccess": [ "exams/stats-service.getExamAnalytics" ], "permission": "exam:read", "description": "V3-8 新增:考试分析仪表盘页面(汇总卡片:应考人数/已批改份数/平均分/及格率 + 分数段分布 + 逐题分析表:错误率/难度等级);权限:requirePermission(EXAM_READ)" }, "/teacher/exams/grading": { "component": "重定向", "type": "server", "redirect": "/teacher/homework/submissions", "permission": "homework:grade" }, "/teacher/exams/grading/[submissionId]": { "component": "重定向", "type": "server", "redirect": "/teacher/homework/submissions", "permission": "homework:grade" }, "/teacher/grades": { "component": "成绩管理首页", "type": "server", "module": "grades", "dataAccess": [ "grades/actions.getGradeRecordsAction" ], "permission": "grade_record:read" }, "/teacher/grades/entry": { "component": "批量成绩录入(按试卷录入每题得分)", "type": "server", "module": "grades", "actions": [ "grades/actions.batchCreateGradeRecordsByExamAction", "grades/actions.undoBatchCreateGradeRecordsAction" ], "dataAccess": [ "exams/data-access.getExamsForGradeEntry", "exams/data-access.getExamForGradeEntry", "classes/data-access.getTeacherClasses", "classes/data-access.getClassGradeIdsByClassIds", "grades/data-access.getClassStudentsForEntry" ], "permission": "grade_record:manage" }, "/teacher/grades/stats": { "component": "成绩统计报表", "type": "server", "module": "grades", "dataAccess": [ "grades/actions.getClassGradeStatsAction", "grades/actions.getClassRankingAction" ], "permission": "grade_record:read" }, "/teacher/grades/analytics": { "component": "成绩分析", "type": "server", "module": "grades", "dataAccess": [ "grades/data-access-analytics.getGradeTrend", "grades/data-access-analytics.getClassComparison", "grades/data-access-analytics.getSubjectComparison", "grades/data-access-analytics.getGradeDistribution", "grades/data-access-analytics.getExamOptionsForGrades" ], "permission": "grade_record:read", "description": "v3-P2 更新:新增 semester/examId 搜索参数解析,调用 getExamOptionsForGrades 获取考试列表传给 AnalyticsFilters" }, "/teacher/course-plans": { "component": "CoursePlanList (teacher)", "type": "client", "module": "course-plans", "dataAccess": [ "course-plans/data-access.getCoursePlans (filtered by teacherId)" ], "permission": "course_plan:read" }, "/teacher/course-plans/[id]": { "component": "CoursePlanDetail (teacher, read-only)", "type": "client", "module": "course-plans", "dataAccess": [ "course-plans/data-access.getCoursePlanById" ], "permission": "course_plan:read" }, "/teacher/attendance": { "component": "AttendanceRecordList + AttendanceFilters", "type": "server", "module": "attendance", "dataAccess": [ "attendance/data-access.getAttendanceRecords", "classes/data-access.getTeacherClasses" ], "permission": "attendance:manage", "description": "教师考勤记录列表(权限:requirePermission(ATTENDANCE_MANAGE))" }, "/teacher/attendance/sheet": { "component": "AttendanceSheet", "type": "client", "module": "attendance", "actions": [ "batchRecordAttendanceAction", "getClassAttendanceForDateAction" ], "dataAccess": [ "attendance/data-access.getClassStudentsForAttendance" ], "permission": "attendance:manage", "description": "批量点名页面(权限:requirePermission(ATTENDANCE_MANAGE))" }, "/teacher/attendance/stats": { "component": "AttendanceStatsCard", "type": "server", "module": "attendance", "dataAccess": [ "attendance/data-access-stats.getClassAttendanceStats", "classes/data-access.getTeacherClasses" ], "permission": "attendance:read", "description": "班级考勤统计(权限:requirePermission(ATTENDANCE_READ))" }, "/teacher/leave": { "component": "LeaveReviewList", "type": "server", "module": "leave-requests", "dataAccess": [ "leave-requests/data-access.getLeaveRequests (scope=class_taught)", "leave-requests/data-access.countPendingForClasses" ], "permission": "leave_request:review", "description": "L-5 实装:教师审批请假页面。展示所辖班级的请假申请列表(按 dataScope=class_taught 过滤),pending 项显示审批按钮打开 LeaveReviewDialog。权限:requirePermission(LEAVE_REQUEST_REVIEW),DataScope.class_taught 仅查所辖班级" }, "/teacher/schedule-changes": { "component": "ScheduleChangeForm + ScheduleChangeList", "type": "server", "module": "scheduling", "dataAccess": [ "scheduling/actions.getAdminClassesForScheduling", "scheduling/actions.getTeachersForScheduling", "scheduling/actions.getScheduleChanges (requesterId=ctx.userId)" ], "actions": [ "requestScheduleChangeAction" ], "permission": "schedule:adjust", "description": "教师调课/代课申请页面(提交申请+查看本人申请列表;权限:requirePermission(SCHEDULE_ADJUST);admin 角色查看全部申请)" }, "/teacher/diagnostic": { "component": "ReportList", "type": "client", "module": "diagnostic", "dataAccess": [ "diagnostic/data-access-reports.getDiagnosticReports" ], "actions": [ "publishReportAction", "deleteReportAction" ], "permission": "diagnostic:read", "description": "学情诊断报告列表(reportType/status 过滤器;权限:requirePermission(DIAGNOSTIC_READ);DataScope.class_members 仅查看自己报告;发布/删除操作需 DIAGNOSTIC_MANAGE)" }, "/teacher/diagnostic/student/[studentId]": { "component": "StudentDiagnosticView", "type": "client", "module": "diagnostic", "dataAccess": [ "diagnostic/data-access.getStudentMasterySummary", "diagnostic/data-access.getKnowledgePointStats (班级平均对比)", "diagnostic/data-access-reports.getDiagnosticReports" ], "actions": [ "generateStudentReportAction" ], "permission": "diagnostic:read", "description": "学生学情诊断视图(概览卡片+雷达图+强项/弱项+生成报告[DIAGNOSTIC_MANAGE]+最新报告;权限:getAuthContext + DataScope 二次校验,class_members 仅自己,children 仅子女;v3-P2 更新:传入 practiceHrefBase=\"/teacher/questions\" 显示练习按钮)" }, "/teacher/diagnostic/class/[classId]": { "component": "ClassDiagnosticView", "type": "client", "module": "diagnostic", "dataAccess": [ "diagnostic/data-access.getClassMasterySummary" ], "actions": [ "generateClassReportAction" ], "permission": "diagnostic:read", "description": "班级学情诊断视图(概览+知识点热力图+排名表+需重点关注学生+生成班级报告[DIAGNOSTIC_MANAGE];权限:getAuthContext + DataScope 校验,class_taught 必须包含 classId,class_members/children notFound)" }, "/teacher/elective": { "component": "ElectiveCourseList (teacher)", "type": "server", "module": "elective", "dataAccess": [ "elective/data-access.getElectiveCourses (scope=class_taught/owned, currentUserId)" ], "actions": [ "deleteElectiveCourseAction", "openSelectionAction", "closeSelectionAction", "runLotteryAction" ], "permission": "elective:read", "description": "教师选修课程列表(权限:requirePermission(ELECTIVE_READ);DataScope.class_taught/owned 按 teacherId 过滤;P0-1:createHref/editBaseHref 指向 /teacher/elective/*)" }, "/teacher/elective/create": { "component": "ElectiveCourseForm", "type": "client", "module": "elective", "actions": [ "createElectiveCourseAction" ], "dataAccess": [ "school/data-access.getSubjectOptions", "school/data-access.getGrades", "school/data-access.getStaffOptions" ], "permission": "elective:manage", "description": "教师创建选修课程(P0-1 新增:权限 requirePermission(ELECTIVE_MANAGE);backHref=/teacher/elective)" }, "/teacher/elective/[id]/edit": { "component": "ElectiveCourseForm (edit)", "type": "client", "module": "elective", "actions": [ "updateElectiveCourseAction" ], "dataAccess": [ "elective/data-access.getElectiveCourseById", "school/data-access.getSubjectOptions", "school/data-access.getGrades", "school/data-access.getStaffOptions" ], "permission": "elective:manage", "description": "教师编辑选修课程(P0-1 新增:权限 requirePermission(ELECTIVE_MANAGE);教师只能编辑自己教授的课程,跨教师越权 notFound)" }, "/teacher/lesson-plans": { "component": "LessonPlanList", "type": "server", "module": "lesson-preparation", "method": "GET", "dataAccess": [ "lesson-preparation/data-access.getLessonPlans" ], "permission": "lesson_plan:read", "description": "教师课案列表(权限:requirePermission(LESSON_PLAN_READ))" }, "/teacher/lesson-plans/new": { "component": "LessonPlanEditor", "type": "client", "module": "lesson-preparation", "method": "GET", "actions": [ "createLessonPlanAction", "getLessonPlanTemplatesAction", "getKnowledgePointOptionsAction" ], "permission": "lesson_plan:create", "description": "新建课案页面(权限:requirePermission(LESSON_PLAN_CREATE))" }, "/teacher/lesson-plans/[planId]/edit": { "component": "LessonPlanEditor", "type": "client", "module": "lesson-preparation", "method": "GET", "dataAccess": [ "lesson-preparation/data-access.getLessonPlanById", "lesson-preparation/data-access-versions.getLessonPlanVersions" ], "actions": [ "updateLessonPlanAction", "saveLessonPlanVersionAction", "revertLessonPlanVersionAction", "deleteLessonPlanAction", "duplicateLessonPlanAction", "saveAsTemplateAction", "suggestKnowledgePointsAction", "publishLessonPlanHomeworkAction", "getLessonPlanSchedulesAction", "createLessonPlanScheduleAction", "deleteLessonPlanScheduleAction", "generateLessonPlanFeedbackAction", "generateDifferentiationSuggestionsAction", "checkCurriculumAlignmentAction", "generateExplainableAssessmentAction", "getKnowledgePointsForAlignmentAction" ], "permission": "lesson_plan:update", "permissionRead": "lesson_plan:read", "description": "编辑课案页面(权限:requirePermission(LESSON_PLAN_UPDATE);只读访问需 LESSON_PLAN_READ;V5-7 新增课时绑定 actions;V5-17/V5-21 新增 AI 反馈/差异化 actions)" }, "/teacher/lesson-plans/library": { "component": "LibraryPage", "type": "server", "module": "lesson-preparation", "method": "GET", "dataAccess": [ "lesson-preparation/data-access.getLessonPlans" ], "actions": [ "duplicateLessonPlanFormAction" ], "permission": "lesson_plan:read", "description": "V5-12 R4 新增:校内课案库页面,展示其他教师发布的课案,支持一键复制(权限:requirePermission(LESSON_PLAN_READ))" }, "/teacher/lesson-plans/heatmap": { "component": "HeatmapPage", "type": "server", "module": "lesson-preparation", "method": "GET", "dataAccess": [ "lesson-preparation/data-access.getLessonPlans", "textbooks/data-access.getKnowledgePointsByTextbookId" ], "permission": "lesson_plan:read", "description": "V5-20 T4 新增:课标覆盖热力图页面,统计教师所有课案对教材知识点的覆盖情况(权限:requirePermission(LESSON_PLAN_READ))" }, "/teacher/error-book": { "component": "TeacherErrorBookPage + SubjectTabs + ClassFilter + AnalyticsStatsCards + ClassErrorBarChart + KnowledgePointWeaknessChart + ChapterWeaknessChart + GroupedStudentErrorTable + TopWrongQuestions", "type": "server", "module": "error-book", "dataAccess": [ "error-book/data-access.getStudentErrorBookSummaries", "error-book/data-access.getKnowledgePointWeakness", "error-book/data-access.getSubjectErrorDistribution", "error-book/data-access.getTopWrongQuestionsByStudentIds", "error-book/data-access.getStudentNameMap", "classes/data-access.getStudentIdsByClassIds" ], "permission": "error_book:analytics_read", "description": "教师错题分析页面(所教班级学生的错题统计/薄弱知识点/学科分布/高频错题;权限:requirePermission(ERROR_BOOK_ANALYTICS_READ);DataScope.class_taught)" }, "/teacher/practice": { "component": "TeacherPracticePage + PracticeOverviewStatsCards + ClassPracticeComparisonTable + PracticeTypeBreakdownChart + ClassKnowledgePointWeaknessChart + StudentPracticeRankingTable + InactiveStudentsAlert", "type": "server", "module": "adaptive-practice", "dataAccess": [ "adaptive-practice/data-access-analytics.getTeacherClassPracticeOverviews", "adaptive-practice/data-access-analytics.getClassStudentPracticeSummaries", "adaptive-practice/data-access-analytics.getPracticeTypeBreakdown", "adaptive-practice/data-access-analytics.getClassKnowledgePointWeakness", "adaptive-practice/data-access-analytics.getStudentsWithoutPractice", "adaptive-practice/data-access-analytics.getStudentNameMap", "classes/data-access.getClassIdsByGradeIds", "classes/data-access.getStudentIdsByClassIds" ], "permission": "adaptive_practice:read", "description": "教师端专项练习分析页(班级对比/类型分布/知识点薄弱度/学生排名/未参与提醒;权限:requirePermission(ADAPTIVE_PRACTICE_READ);DataScope.class_taught/grade_managed)" }, "/teacher/lesson-plans/calendar": { "component": "LessonPlanCalendar", "type": "client", "module": "lesson-preparation", "method": "GET", "description": "M9 日历视图页面(2026-07-01 新增):周历/月历切换,6 种事件类型颜色映射(权限:requirePermission(LESSON_PLAN_READ))" } }, "student": { "/student/dashboard": { "component": "StudentDashboardView", "type": "server", "dataAccess": [ "users/data-access.getCurrentStudentUser", "classes/data-access.getStudentClasses", "classes/data-access.getStudentSchedule", "homework/data-access.getStudentHomeworkAssignments", "homework/data-access.getStudentDashboardGrades" ], "permission": "homework:submit" }, "/student/learning/assignments": { "component": "学生作业列表", "type": "server", "module": "homework", "dataAccess": [ "users/data-access.getCurrentStudentUser", "homework/data-access.getStudentHomeworkAssignments" ], "permission": "homework:submit" }, "/student/learning/assignments/[assignmentId]": { "component": "学生作答/复习", "type": "server", "module": "homework", "actions": [ "startHomeworkSubmissionAction", "saveHomeworkAnswerAction", "submitHomeworkAction" ], "dataAccess": [ "users/data-access.getCurrentStudentUser", "homework/data-access.getStudentHomeworkTakeData" ], "permission": "homework:submit" }, "/student/learning/assignments/[assignmentId]/result": { "component": "HomeworkSubmissionResult", "type": "server", "module": "homework", "dataAccess": [ "users/data-access.getCurrentStudentUser", "homework/data-access.getStudentSubmissionResult" ], "permission": "homework:submit", "description": "V3-9 新增:学生提交后即时反馈页(分数汇总+对错分布+错题预览);homework-take-view.tsx 提交后 router.push 跳转至此页;权限:requirePermission(HOMEWORK_SUBMIT)" }, "/student/learning": { "component": "StudentLearningPage(学习中心入口:课程/作业/教材 3 个卡片)", "type": "server", "dataAccess": [ "users/data-access.getCurrentStudentUser", "classes/data-access.getStudentClasses", "homework/data-access-student.getStudentHomeworkAssignments", "textbooks/data-access.getTextbooks" ], "permission": "class:read", "description": "✅ 2026-06-26 审计修复新增路由条目:学生学习中心入口页,权限点 class:read(CLASS_READ);新增 error.tsx + loading.tsx", "generateMetadata": "✅ P2 修复新增:page.tsx 导出 generateMetadata,通过 next-intl getTranslations 生成页面标题", "errorFile": "error.tsx(✅ 2026-06-26 审计修复新增:使用 shared ErrorState + namespace=\"classes\")", "loadingFile": "loading.tsx(✅ 2026-06-26 审计修复新增:学习中心加载骨架)" }, "/student/learning/courses": { "component": "StudentCoursesView", "type": "server", "dataAccess": [ "users/data-access.getCurrentStudentUser", "classes/data-access.getStudentClasses" ], "permission": "class:read", "description": "✅ 2026-06-26 审计修复:权限点从 homework:submit 改为 class:read(CLASS_READ),更准确反映页面语义", "generateMetadata": "✅ P2 修复新增:page.tsx 导出 generateMetadata,通过 next-intl getTranslations 生成页面标题", "errorFile": "error.tsx(✅ 2026-06-26 审计修复新增:使用 shared ErrorState + namespace=\"classes\")" }, "/student/learning/courses/[classId]": { "component": "学生课程详情", "type": "server", "module": "classes", "dataAccess": [ "users/data-access.getCurrentStudentUser", "classes/data-access.getStudentClasses" ], "permission": "class:read", "description": "✅ 2026-06-26 审计修复新增路由条目:学生课程详情页,权限点 class:read(CLASS_READ)", "generateMetadata": "✅ P2 修复新增:page.tsx 导出 generateMetadata,通过 next-intl getTranslations 生成页面标题", "errorFile": "error.tsx(✅ 2026-06-26 审计修复新增:使用 shared ErrorState + namespace=\"classes\")" }, "/student/learning/textbooks": { "component": "学生教材列表(只读)", "type": "server", "module": "textbooks", "dataAccess": [ "users/data-access.getCurrentStudentUser", "textbooks/data-access.getTextbooks" ], "permission": "textbook:read" }, "/student/learning/textbooks/[id]": { "component": "学生教材阅读(只读)", "type": "server", "module": "textbooks", "dataAccess": [ "users/data-access.getCurrentStudentUser", "textbooks/data-access.getTextbookById", "textbooks/data-access.getChaptersByTextbookId", "textbooks/data-access.getKnowledgePointsByTextbookId" ], "permission": "textbook:read" }, "/student/learning/study-path": { "component": "AiStudyPath", "type": "server", "module": "ai", "dataAccess": [ "users/data-access.getCurrentStudentUser" ], "permission": "ai:chat", "description": "学生学习路径推荐(V3 新增:基于学生学情与知识图谱生成个性化学习路径;权限:requirePermission(AI_CHAT),组件内通过 AiClientProvider 注入的 AiClientService.recommendStudyPath 调用 recommendStudyPathAction)" }, "/student/schedule": { "component": "学生课表", "type": "server", "module": "classes", "dataAccess": [ "users/data-access.getCurrentStudentUser", "classes/data-access.getStudentClasses", "classes/data-access.getStudentSchedule" ], "permission": "class:read", "description": "✅ 2026-06-26 审计修复:权限点从 homework:submit 改为 class:read(CLASS_READ),更准确反映页面语义", "generateMetadata": "✅ P2 修复新增:page.tsx 导出 generateMetadata,通过 next-intl getTranslations 生成页面标题", "errorFile": "error.tsx(✅ 2026-06-26 审计修复新增:使用 shared ErrorState + namespace=\"classes\")" }, "/student/grades": { "component": "我的成绩", "type": "server", "module": "grades", "dataAccess": [ "grades/data-access.getStudentGradeSummary" ], "permission": "grade_record:read" }, "/student/attendance": { "component": "StudentAttendanceView", "type": "server", "module": "attendance", "dataAccess": [ "attendance/data-access-stats.getStudentAttendanceSummary" ], "permission": "attendance:read", "description": "学生考勤视图(统计卡片 + 最近记录;权限:requirePermission(ATTENDANCE_READ),DataScope.class_members 仅查自己)" }, "/student/leave": { "component": "LeaveRequestForm (defaultStudentId/classId 模式) + LeaveRequestList", "type": "server", "module": "leave-requests", "dataAccess": [ "classes/data-access.getStudentActiveClass", "leave-requests/data-access.getLeaveRequests (scope=class_members, currentUserId=requesterId)" ], "permission": "leave_request:create + leave_request:read", "description": "L-5 实装:学生在线请假页面。顶部请假表单(hidden input 写入 studentId=ctx.userId 与 classId=当前活跃班级),底部学生本人提交的请假列表。权限:requirePermission(LEAVE_REQUEST_CREATE/READ);DataScope.class_members 由 currentUserId 过滤本人提交" }, "/student/diagnostic": { "component": "StudentDiagnosticView", "type": "server", "module": "diagnostic", "dataAccess": [ "diagnostic/data-access.getStudentMasterySummary (ctx.userId)", "diagnostic/data-access-reports.getDiagnosticReports (studentId=ctx.userId)" ], "permission": "diagnostic:read", "description": "学生本人学情诊断视图(概览+雷达图+强项/弱项+最新报告;权限:requirePermission(DIAGNOSTIC_READ),DataScope.class_members 仅查自己)" }, "/student/elective": { "component": "MySelectionsLoader (Suspense) + AvailableCoursesLoader (Suspense)", "type": "server", "module": "elective", "dataAccess": [ "elective/data-access-selections.getAvailableCoursesForStudent", "elective/data-access-selections.getStudentSelections (cache 去重)" ], "actions": [ "selectCourseAction", "dropCourseAction" ], "permission": "elective:read", "description": "学生选课页面(P2-3:拆分为\"我的选课\"与\"可选课程\"两个独立 Suspense 边界流式渲染;React cache() 自动去重 getStudentSelections;P0-3:入口 requirePermission(ELECTIVE_READ) 纵深防御;选/退课操作仍需 ELECTIVE_SELECT)" }, "/student/error-book": { "component": "StudentErrorBookPage + ErrorBookStatsCards + ErrorBookFilters + StudentErrorBookListClient + AddErrorBookDialogWithQuestions", "type": "server", "module": "error-book", "dataAccess": [ "error-book/data-access.getErrorBookStats (ctx.userId)", "error-book/data-access.getErrorBookItems (ctx.userId)" ], "actions": [ "createErrorBookItemAction", "updateErrorBookNoteAction", "reviewErrorBookItemAction", "deleteErrorBookItemAction", "archiveErrorBookItemAction", "getErrorBookItemDetailAction" ], "permission": "error_book:read", "description": "学生错题本页面(统计卡片/筛选/列表/手动添加/详情复习;权限:requirePermission(ERROR_BOOK_READ);DataScope.owned 仅查自己)。✅ 2026-06-24 解耦:app 层新增两个包装组件承接跨模块依赖——StudentErrorBookListClient 注入 AiErrorBookAnalysis(modules/ai)+createPracticeSessionAction(modules/adaptive-practice) 到 ErrorBookList 的 aiAnalysisSlot/onStartVariantPractice props;AddErrorBookDialogWithQuestions 注入 getQuestionsAction(modules/questions)+extractQuestionPreview(shared/lib) 到 AddErrorBookDialog 的 onLoadQuestions prop" }, "/student/practice": { "component": "StudentPracticePage + PracticeStarter + PracticeHistory + PracticeStatsCards", "type": "server", "module": "adaptive-practice", "dataAccess": [ "adaptive-practice/data-access.getPracticeSessions (ctx.userId)", "adaptive-practice/data-access.getPracticeStats (ctx.userId)" ], "actions": [ "createPracticeSessionAction" ], "permission": "adaptive_practice:read", "description": "学生端专项练习入口(发起四种模式练习/查看历史/统计;权限:requirePermission(ADAPTIVE_PRACTICE_READ);DataScope.owned 仅查自己)" }, "/student/practice/[sessionId]": { "component": "PracticeSessionView", "type": "server", "module": "adaptive-practice", "dataAccess": [ "adaptive-practice/data-access.getPracticeSessionById (sessionId, ctx.userId)" ], "actions": [ "submitPracticeAnswerAction", "completePracticeSessionAction", "abandonPracticeSessionAction" ], "permission": "adaptive_practice:manage", "description": "学生端答题页(逐题作答/自动判分/完成/放弃;权限:requirePermission(ADAPTIVE_PRACTICE_MANAGE))" }, "/student/lesson-plans": { "component": "LessonPlanList (viewMode=student)", "type": "server", "module": "lesson-preparation", "dataAccess": [ "lesson-preparation/data-access.getLessonPlans (status=published, scope=class_members)" ], "permission": "lesson_plan:read", "description": "V3 新增:学生课案列表(仅查看 status=published 的课案;权限:requirePermission(LESSON_PLAN_READ);DataScope.class_members 按 gradeIds 过滤)" }, "/student/lesson-plans/[planId]/view": { "component": "LessonPlanReadonlyView", "type": "server", "module": "lesson-preparation", "dataAccess": [ "lesson-preparation/data-access.getLessonPlanById" ], "permission": "lesson_plan:read", "description": "V3 新增:学生只读查看已发布课案(复用 React Flow,nodesDraggable=false, nodesConnectable=false;权限:requirePermission(LESSON_PLAN_READ))" } }, "management": { "/management/grade": { "component": "GradeManagementRedirect", "type": "server", "module": "grades", "redirect": "/management/grade/classes", "permission": "grade:manage" }, "/management/grade/classes": { "component": "GradeClassesClient", "type": "client", "module": "classes", "permission": "grade:manage" }, "/management/grade/insights": { "component": "年级作业洞察 + GradeInsightsFilters", "type": "server", "dataAccess": [ "classes/data-access.getGradeHomeworkInsights" ], "permission": "grade:manage", "description": "v4-P2-6 更新:重写页面使用 GradeInsightsFilters 替代原生 form get(ChipNav 无整页刷新),表格添加 overflow-x-auto 水平滚动" }, "/management/grade/dashboard": { "component": "GradeDashboardPage + 4 Tab 面板", "type": "server", "module": "school", "dataAccess": [ "grades/data-access-analytics.getGradeDistributionByGradeId", "classes/data-access.getGradeHomeworkInsights", "exams/data-access.getExamsByGradeId", "course-plans/data-access.getGradeCoursePlanProgress" ], "permission": "grade:read", "description": "v4-P2-7 新增:年级仪表盘,4 个维度 Tab(成绩分布/作业完成/考试/课本进度),ChipNav URL 切换 Tab,仅加载当前 Tab 数据" }, "/management/grade/practice": { "component": "GradePracticePage + GradeInsightsFilters + PracticeOverviewStatsCards + ClassPracticeComparisonTable + PracticeTypeBreakdownChart", "type": "server", "module": "adaptive-practice", "dataAccess": [ "adaptive-practice/data-access-analytics.getGradePracticeOverview", "adaptive-practice/data-access-analytics.getGradeClassPracticeComparison", "adaptive-practice/data-access-analytics.getPracticeTypeBreakdown", "users/data-access.getUserIdsByGradeId", "school/data-access.getGradesForStaff", "classes/data-access.getTeacherIdForMutations" ], "permission": "adaptive_practice:read", "description": "年级主任宏观数据分析页(年级整体统计/各班级练习对比/类型分布;权限:requirePermission(ADAPTIVE_PRACTICE_READ);按 getGradesForStaff 过滤可管理年级)" } }, "parent": { "/parent/dashboard": { "component": "ParentDashboard (+ AiChildSummary slot)", "type": "server", "module": "parent+ai", "dataAccess": [ "parent/data-access.getParentDashboardData" ], "permission": "auth_required + ai:chat", "description": "家长仪表盘首页(问候语 + 子女卡片网格 + AI 学情摘要区域;权限:requireAuth();V3 新增 aiSummarySlot 为每个子女渲染 AiChildSummary 客户端组件,由 AiClientProvider 提供 AiClientService.generateChildSummary 调用 generateChildSummaryAction,二次校验 AI_CHAT 权限)" }, "/parent/children/[studentId]": { "component": "ChildDetailHeader + ChildDetailPanel (Tab 布局 + SiblingSwitcher)", "type": "server", "module": "parent", "dataAccess": [ "parent/data-access.getChildDashboardData", "parent/data-access.getChildNameList" ], "permission": "auth_required", "description": "v4 升级:子女详情页改为 6-Tab 布局(overview/homework/grades/schedule/attendance/diagnostic),支持 ?tab= URL 参数与多子女切换;权限:requireAuth() + 二次校验 ctx.dataScope.childrenIds 包含 studentId" }, "/parent/grades": { "component": "子女成绩", "type": "server", "module": "grades", "dataAccess": [ "grades/data-access.getStudentGradeSummary", "grades/data-access-analytics.getClassAverageTrend" ], "permission": "grade_record:read", "description": "家长成绩视图(按 DataScope.children 过滤;v4 新增 ParentExportButton 占位;v3-P2 更新:为每个子女并行查询 getClassAverageTrend,渲染 GradeTrendCard;v3-P3-4 更新:GradeTrendCard 新增日期范围选择器,通过 nuqs trendRange URL 参数持久化)" }, "/parent/diagnostic": { "component": "子女学情诊断", "type": "server", "module": "diagnostic", "dataAccess": [ "diagnostic/data-access.getStudentMasterySummary", "diagnostic/data-access-reports.getDiagnosticReports" ], "permission": "diagnostic:read", "description": "P2-5 新增:家长查看多子女学情诊断(按 DataScope.children 遍历,复用 StudentDiagnosticView 组件;含 loading.tsx + error.tsx;v3-P2 更新:传入 practiceHrefBase={null} 隐藏练习按钮)" }, "/parent/attendance": { "component": "StudentAttendanceView (per child) + ParentAttendanceWarning", "type": "server", "module": "attendance", "dataAccess": [ "parent/data-access.getChildren", "attendance/data-access-stats.getStudentAttendanceSummary" ], "permission": "attendance:read", "description": "v4 升级:家长考勤视图新增 ParentAttendanceWarning 异常预警横幅(absent>=3 high、absent>=1 medium、late>=3 medium、presentRate<90 high);权限:requirePermission(ATTENDANCE_READ),DataScope.children 仅查子女" }, "/parent/elective": { "component": "ParentChildrenDataPage + ParentSelectionView (per child)", "type": "server", "module": "elective", "dataAccess": [ "parent/data-access.getChildren", "parent/data-access.getChildBasicInfo", "elective/data-access-selections.getStudentSelections" ], "permission": "elective:read", "description": "P0-2 新增:家长查看子女选课记录(只读,无选/退课操作);权限:requirePermission(ELECTIVE_READ);双重校验 parentId+studentId 防止跨家庭信息泄露;使用 ParentChildrenDataPage 模式 + Promise.allSettled 容错" }, "/parent/leave": { "component": "LeaveRequestForm (childOptions 模式) + LeaveRequestList", "type": "server", "module": "leave-requests", "dataAccess": [ "parent/data-access.getChildren", "parent/data-access.getChildBasicInfo", "leave-requests/data-access.getLeaveRequests (scope=children)" ], "permission": "leave_request:create + leave_request:read", "description": "L-5 实装:家长在线请假页面。顶部请假表单(下拉选择子女,自动写入 classId),底部该家长所有子女的请假列表(按 dataScope=children 过滤)。权限:requirePermission(LEAVE_REQUEST_CREATE/READ),DataScope.children 仅查子女" }, "/parent/error-book": { "component": "ParentErrorBookPage + ErrorBookStatsCards + TopWrongQuestions", "type": "server", "module": "error-book", "dataAccess": [ "error-book/data-access.getErrorBookStats (per child)", "error-book/data-access.getTopWrongQuestionsByStudentIds", "error-book/data-access.getKnowledgePointWeakness", "error-book/data-access.getStudentNameMap" ], "permission": "error_book:read", "description": "家长错题本页面(子女错题统计/薄弱知识点/高频错题;权限:requirePermission(ERROR_BOOK_READ);DataScope.children 仅查子女)" }, "/parent/lesson-plans": { "component": "LessonPlanList (viewMode=parent)", "type": "server", "module": "lesson-preparation", "dataAccess": [ "lesson-preparation/data-access.getLessonPlans (status=published, scope=children)" ], "permission": "lesson_plan:read", "description": "V3 新增:家长课案列表(仅查看子女所在班级 status=published 的课案;权限:requirePermission(LESSON_PLAN_READ);DataScope.children 按 gradeIds 过滤)" }, "/parent/lesson-plans/[planId]/view": { "component": "LessonPlanReadonlyView", "type": "server", "module": "lesson-preparation", "dataAccess": [ "lesson-preparation/data-access.getLessonPlanById" ], "permission": "lesson_plan:read", "description": "V3 新增:家长只读查看已发布课案(复用 React Flow,nodesDraggable=false, nodesConnectable=false;权限:requirePermission(LESSON_PLAN_READ))" }, "/parent/practice": { "component": "ParentPracticePage + PracticeStatsCards + PracticeHistory (routePrefix=undefined, 只读卡片)", "type": "server", "module": "adaptive-practice", "dataAccess": [ "adaptive-practice/data-access.getPracticeStats (per child)", "adaptive-practice/data-access.getPracticeSessions (per child, pageSize=20)", "users/data-access.getUserNamesByIds" ], "permission": "adaptive_practice:read", "description": "V4 新增(2026-06-25 P0-修复-11):家长查看子女练习统计与历史;权限:requirePermission(ADAPTIVE_PRACTICE_READ);DataScope.children 仅查子女,使用 Promise.allSettled 容错;PracticeServiceProvider 注入;家长无会话详情页,PracticeHistory 不传 routePrefix 仅渲染只读卡片;每个子女区块用 WidgetBoundary 隔离" } }, "root": { "/": { "component": "重定向", "type": "server", "redirect": "/dashboard" } }, "shared": { "/dashboard": { "component": "角色路由分发", "type": "server", "redirect": "按permissions判断→/admin|/teacher|/student|/parent" }, "/profile": { "component": "ProfilePage", "type": "server", "permission": "auth_required", "description": "个人资料页面(展示头像 Avatar + Personal Information + Account Information + 学生/教师概览;头像从 userProfile.image 获取,无头像时显示用户名首字母 fallback)" }, "/settings": { "component": "SettingsPage", "type": "server", "permission": "auth_required", "dataAccess": [ "messaging/notification-preferences.getNotificationPreferences (re-export from notifications/preferences.ts)" ], "description": "设置页面(按角色分发 AdminSettingsView/TeacherSettingsView/StudentSettingsView/ParentSettingsView;含 General/Notifications/Appearance/Security/AI tab,Tab 通过 URL ?tab= 参数持久化,AI tab 需 AI_CONFIGURE 权限,Notifications 渲染 NotificationPreferencesForm,登出按钮 AlertDialog 二次确认)" }, "/announcements": { "component": "AnnouncementList (published only, audience-filtered) + AnnouncementsServiceProvider", "type": "server", "module": "announcements", "dataAccess": [ "announcements/data-access.getUserAnnouncementsPageData (status=published, audience={gradeIds, classIds})" ], "permission": "announcement:read", "description": "P0-2 修复:用户端公告列表。audience 升级为数组 { gradeIds, classIds } 支持家长多孩子/教师多班级;使用 getUserAnnouncementsPageData 编排函数封装 resolveAudience 逻辑;包裹 AnnouncementsServiceProvider 供 AnnouncementList 调用 getReadStatus" }, "/announcements/[id]": { "component": "AnnouncementDetail (read-only, canManage=false) + AnnouncementsServiceProvider", "type": "server", "module": "announcements", "dataAccess": [ "announcements/data-access.getAnnouncementByIdForUser (status=published + 受众过滤 + isReadByCurrentUser + readCount)" ], "permission": "announcement:read", "description": "P0-1 修复:用户端详情页改用 getAnnouncementByIdForUser,结合 status=published + 受众过滤 + 已读状态一次返回,防止越权访问草稿/他人班级公告。包裹 AnnouncementsServiceProvider 供 markRead 调用" } }, "messages": { "/messages": { "component": "MessageList + NotificationList", "type": "server", "module": "messaging", "dataAccess": [ "messaging/data-access.getMessages", "notifications/data-access.getNotifications" ], "permission": "message:read", "description": "消息首页(收件箱/已发送/星标列表 + 通知列表;权限:requirePermission(MESSAGE_READ))。V3 P0-3 修复:编排从 data-access.getMessagesPageData 迁回页面层,通过 Promise.all 并行调用 messaging.data-access 和 notifications.data-access,保持模块独立性" }, "/messages/[id]": { "component": "MessageDetail", "type": "server", "module": "messaging", "dataAccess": [ "messaging/data-access.getMessageById", "messaging/data-access.getMessageThread" ], "actions": [ "markMessageAsReadAction (自动已读)" ], "permission": "message:read", "description": "消息详情(含回复线程;权限:requirePermission(MESSAGE_READ))" }, "/messages/compose": { "component": "MessageCompose", "type": "server", "module": "messaging", "dataAccess": [ "messaging/data-access.getRecipients", "messaging/data-access.getMessageDrafts" ], "components": [ "messaging/components/message-compose", "messaging/components/message-draft-list" ], "permission": "message:send", "description": "写消息页面(支持 reply 模式 via searchParams: receiverId, subject, parentMessageId;权限:requirePermission(MESSAGE_SEND))。V3 P1-1 新增:渲染 MessageDraftList 草稿列表组件(继续编辑 + 删除草稿,乐观更新 + 失败回滚)" } } }, "apiRoutes": { "/api/auth/[...nextauth]": { "methods": [ "GET", "POST" ], "handler": "auth.handlers", "auth": "public" }, "/api/ai/chat": { "methods": [ "POST" ], "handler": "createAiChatCompletion", "auth": "AI_CHAT", "validation": "parseAiChatPayload (Zod)", "errorHandling": "withApiErrorHandler HOF + errorToStatus 集中映射(PermissionDeniedError→403 / NotFoundError→404 / ValidationError→400 / BusinessError→400 / 其他→500)", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" }, "/api/ai/chat/stream": { "methods": [ "POST" ], "handler": "createAiChatCompletionStream (SSE AsyncGenerator → ReadableStream)", "auth": "AI_CHAT", "validation": "parseAiChatPayload + filterUserInput (per message)", "protocol": "Server-Sent Events", "events": [ "token — 流式 token 内容", "error — 错误信息", "filtered — 内容被安全过滤", "[DONE] — 流结束" ], "safety": [ "checkDailyLimit — 调用前校验每日上限", "filterUserInput — 输入过滤", "filterAiOutput — 输出过滤(学生场景阻断直接答案)", "incrementDailyUsage — 调用后计数", "trackEvent — 埋点统计" ], "studentMode": "强制苏格拉底式引导系统提示", "errorHandling": "createSseError(流建立前)+ formatSseError(流中)", "envelope": "✅ 2026-07-04 API 规范化重构:改用 shared/lib/sse.ts 共享 helpers(formatSseEvent/formatSseError/formatSseDone/createSseResponse/createSseError);补充 dynamic = 'force-dynamic'", "dynamic": "force-dynamic" }, "/api/notifications/stream": { "methods": [ "GET" ], "handler": "通知实时推送 SSE 端点(ReadableStream + setInterval 定时推送)", "auth": "MESSAGE_READ", "validation": "requirePermission 权限校验", "protocol": "Server-Sent Events", "events": [ "update — 未读数 + 最新通知列表(连接建立时立即推送,之后每 15 秒推送)", "error — 权限拒绝或内部错误", "[DONE] — 连接超时(5 分钟)自动关闭" ], "pushStrategy": "连接建立立即推送 + 15 秒间隔定时推送 + 5 分钟超时自动关闭", "module": "notifications", "deps": [ "requirePermission", "data-access.getUnreadNotificationCount", "data-access.getNotifications" ], "usedBy": [ "hooks/use-notification-stream.ts" ], "errorHandling": "PermissionDeniedError→403(已认证无权限),其他→401(未认证);改用 shared/lib/sse.ts 共享 helpers", "envelope": "✅ 2026-07-04 API 规范化重构:改用 shared/lib/sse.ts 共享 helpers;补充 dynamic = 'force-dynamic';鉴权失败处理修正(原字符串匹配 '权限不足' → 改用 instanceof PermissionDeniedError)", "dynamic": "force-dynamic" }, "/api/onboarding/complete": { "methods": [ "POST" ], "handler": "onboarding complete(已废弃,迁移至 completeOnboardingAction Server Action)", "auth": "required", "validation": "Zod schema", "deprecated": true, "deprecatedReason": "存在角色自选越权、教师覆盖任课、无事务等漏洞,已迁移至 modules/onboarding/actions.ts", "errorHandling": "apiError(..., 410, 'deprecated') 统一废弃响应", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" }, "/api/onboarding/status": { "methods": [ "GET" ], "handler": "onboarding status(已废弃,迁移至 getOnboardingStatusAction Server Action)", "auth": "required", "deprecated": true, "deprecatedReason": "引导流程改为独立路由 /onboarding + middleware 重定向,不再需要客户端拉取状态", "errorHandling": "apiError(..., 410, 'deprecated') 统一废弃响应", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" }, "/api/upload": { "methods": [ "POST" ], "handler": "文件上传 (multipart/form-data)", "auth": "requirePermission(FILE_UPLOAD)", "module": "files", "delegatesTo": "files/actions.uploadFileAction", "validation": "Zod UploadMetadataSchema + isAllowedMimeType + MAX_FILE_SIZE (10MB)", "description": "2026-06-25 重构:路由仅保留速率限制和 multipart 解析,委托 actions.uploadFileAction 完成权限校验+Zod 校验+storageProvider.save+createFileAttachment+trackEvent+logAudit。返回 FileUploadResult", "errorHandling": "apiFromAction(result, { headers }) 通过 errorCode 驱动状态码", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" }, "/api/files/[id]": { "methods": [ "GET", "DELETE" ], "handler": "文件元数据查询/删除", "auth": "GET: requirePermission(FILE_READ) + ownership 检查, DELETE: requirePermission(FILE_DELETE)", "module": "files", "delegatesTo": "GET: files/actions.getFileAction, DELETE: files/actions.deleteFileAction", "description": "2026-06-25 重构:路由委托对应 actions。✅ 2026-07-04 API 规范化重构:使用 apiFromAction 替代字符串匹配推导状态码,路由从 80+ 行简化到 38 行。GET 加入 ownership 检查(非 FILE_DELETE 持有者只能读自己上传的);DELETE 通过 storageProvider.delete 删除磁盘文件 + trackEvent + logAudit", "errorHandling": "apiFromAction(GET/DELETE)通过 errorCode 驱动状态码", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" }, "/api/files/batch-delete": { "methods": [ "POST" ], "handler": "批量删除文件", "auth": "requirePermission(FILE_DELETE)", "module": "files", "delegatesTo": "files/actions.batchDeleteFilesAction", "validation": "Zod BatchDeleteSchema(ids 数组 max 100 元素,每个为非空字符串),空数组返回 400", "description": "2026-06-25 重构:路由委托 actions.batchDeleteFilesAction。先通过 getFileAttachmentsByIds 查出文件记录,并行调用 storageProvider.delete 删除磁盘文件(静默失败),再调用 deleteFileAttachments 删除 DB 记录(失败时回退到逐条删除);trackEvent(file.batch_deleted) + logAudit;响应 { success, message, deletedCount, failedIds }", "errorHandling": "apiFromAction 通过 errorCode 驱动状态码", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" }, "/api/search": { "methods": [ "GET" ], "handler": "全局全文检索", "auth": "requireAuth", "module": "search", "validation": "query params: q (关键词), type=all|question|textbook|exam|announcement, page=1, pageSize=10 (上限 50)", "description": "并行查询 questions/textbooks/exams/announcements(公告仅 status=published),按 createdAt 降序排序后分页;question content 字段 CAST AS CHAR 模糊匹配;✅ 2026-07-04 重构:DB 查询下沉至 modules/search/data-access,消除 app/ 直查 DB 违规;统一响应信封 + withApiErrorHandler 异常处理", "delegatesTo": "modules/search/data-access.{searchQuestions,searchTextbooks,searchExams,searchAnnouncements}", "errorHandling": "withApiErrorHandler + apiSuccess", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" }, "/api/export": { "methods": [ "POST" ], "handler": "Excel 导出(grades/users/attendance/electiveCourses/courseSelections/audit/login/dataChange)", "auth": "requireAuth", "module": "shared.lib.excel + users/grades/attendance/elective/audit", "validation": "JSON body { type, params }", "description": "按 type 分发:grades→exportGradeRecordsToExcel, users→exportUsersToExcel, attendance→exportAttendanceRecordsToExcel(需 ATTENDANCE_READ), electiveCourses→exportElectiveCoursesToExcel(需 ELECTIVE_READ), courseSelections→exportCourseSelectionsToExcel(需 ELECTIVE_MANAGE), audit/login/dataChange→audit/actions 导出(需 AUDIT_LOG_READ)。✅ 2026-07-04 API 规范化重构:用 EXPORT_TYPES 映射表替代 4 处嵌套 try/catch,统一 requirePermission 调用。返回 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 二进制流", "errorHandling": "EXPORT_TYPES: Record 映射表 + requirePermission(无权限自动抛 PermissionDeniedError→403)", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }(成功响应仍为二进制流,失败响应为信封)" }, "/api/import": { "methods": [ "POST" ], "handler": "Excel 解析预览(不写 DB)", "auth": "requirePermission(USER_MANAGE)", "module": "shared.lib.excel", "validation": "multipart/form-data file,限 .xlsx/.xls,10MB 上限", "description": "接收 Excel 文件,调用 parseExcel 返回 sheets 预览数据(实际导入由 users/actions.importUsersAction 完成)", "errorHandling": "ValidationError + apiError;修复原 401→403(权限不足应为 403 而非 401)", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" } }, "devops": { "ci": { "configFile": ".gitea/workflows/ci.yml", "triggers": [ "push to main", "pull_request to main", "schedule cron 0 2 * * *" ], "jobs": { "build-deploy": { "runsOn": "CDCD", "container": "dockerreg.eazygame.cn/node-with-docker:22", "trigger": "push/PR to main", "steps": [ "checkout", "cache npm", "configure npm proxy", "npm ci", "lint", "typecheck", "install playwright chromium", "integration tests", "e2e tests", "cache next.js build", "build", "prepare standalone", "deploy to docker" ] }, "security-scan": { "runsOn": "ubuntu-latest", "trigger": "push/PR to main", "needs": "build-deploy", "continueOnError": true, "steps": [ "checkout", "setup node 20", "npm ci", "npm audit --audit-level=moderate + 生成 audit-report.json (continue-on-error)", "Snyk scan --severity-threshold=high --sarif-file-output=snyk.sarif (env SNYK_TOKEN, continue-on-error)", "Trivy fs scan json+table (continue-on-error)", "OWASP ZAP baseline scan target=NEXTAUTH_URL||localhost:8015 cmd_options='-a -j' (continue-on-error)", "upload security-reports artifact (audit-report.json, trivy-fs-report.json, snyk.sarif)" ] }, "scheduled-backup": { "runsOn": "ubuntu-latest", "trigger": "schedule cron 0 2 * * *", "condition": "github.event_name == 'schedule'", "steps": [ "checkout", "run scripts/backup-db.sh (env DATABASE_URL, BACKUP_DIR)", "run scripts/backup-verify.sh (校验备份完整性)", "run scripts/backup-offsite-sync.sh (异地同步, env BACKUP_OFFSITE_*, 失败不阻塞)", "upload backups/ artifact (retention 30 days)" ] }, "backup-verify": { "runsOn": "ubuntu-latest", "trigger": "schedule", "condition": "github.event_name == 'schedule'", "needs": "scheduled-backup", "steps": [ "checkout", "download db-backup artifact", "run scripts/backup-verify.sh (独立校验)", "run scripts/health-check.sh > health-report.json", "upload backup-verify-report artifact (backups/, health-report.json, retention 7 days)" ] }, "weekly-dr-drill": { "runsOn": "ubuntu-latest", "trigger": "schedule (每周触发, github.run_attempt % 7 == 0)", "condition": "github.event_name == 'schedule' && github.run_attempt % 7 == 0", "needs": "backup-verify", "steps": [ "checkout", "run scripts/dr-drill.sh (env DATABASE_URL, DR_DRILL_TEST_DB=next_edu_dr_drill)", "upload dr-drill-report artifact (docs/dr/reports/, retention 90 days)" ] } } }, "drDrillWorkflow": { "configFile": ".gitea/workflows/dr-drill.yml", "triggers": [ "schedule cron 0 4 * * 1 (每周一凌晨 4 点)", "workflow_dispatch (inputs: backup_file, no_cleanup)" ], "job": "dr-drill", "runsOn": "ubuntu-latest", "timeoutMinutes": 30, "steps": [ "checkout", "install mysql-client", "prepare backup directory (mkdir backups docs/dr/reports)", "download db-backup artifact (continue-on-error) 或现场执行 backup-db.sh", "run scripts/dr-drill.sh (支持 --backup/--no-cleanup 参数)", "upload dr-drill-report-${{ github.run_id }} artifact (docs/dr/reports/, retention 90 days)", "on failure: webhook 通知运维团队 (DR_NOTIFICATION_WEBHOOK)" ] }, "securityWorkflow": { "configFile": ".gitea/workflows/security.yml", "triggers": [ "schedule cron 0 3 * * 1 (每周一凌晨 3 点)", "workflow_dispatch (inputs: target_url, skip_dast)" ], "job": "deep-security-scan", "runsOn": "ubuntu-latest", "continueOnError": true, "steps": [ "checkout", "setup node 20", "npm ci", "npm audit + 生成 audit-report.json (依赖扫描)", "Snyk scan --severity-threshold=medium --sarif-file-output=snyk.sarif (env SNYK_TOKEN, 深度依赖+静态分析)", "Trivy fs scan json+table (文件系统扫描, trivy-fs-report.json)", "Build Next.js standalone + docker build nextjs-app:scan + Trivy image scan (容器镜像扫描, trivy-image-report.json)", "OWASP ZAP baseline scan (DAST, target=inputs.target_url||NEXTAUTH_URL||localhost:8015, 可通过 skip_dast 跳过)", "Generate security-summary.md (jq 汇总各报告漏洞计数)", "upload security-reports-full artifact (audit-report.json, trivy-fs-report.json, trivy-image-report.json, snyk.sarif, security-summary.md)" ], "configFiles": { "suppressions": ".gitea/suppressions.json (Snyk 漏洞抑制, 每条含 id/package/severity/reason/expires/owner)", "trivyignore": ".trivyignore (Trivy CVE 忽略列表, 每行一个 CVE 带注释)" } }, "scripts": { "scripts/audit.sh": { "type": "bash", "purpose": "依赖安全审计,运行 npm audit --audit-level=moderate,失败时生成 audit-report.json", "platform": "Linux/macOS" }, "scripts/audit.ps1": { "type": "powershell", "purpose": "依赖安全审计(Windows 版本)", "platform": "Windows" }, "scripts/backup-db.sh": { "type": "bash", "purpose": "MySQL 数据库备份,从 DATABASE_URL 解析连接信息,gzip 压缩,保留 RETENTION_DAYS 天(默认 30)", "env": [ "DATABASE_URL", "BACKUP_DIR", "RETENTION_DAYS" ], "output": "${BACKUP_DIR}/db_backup_${TIMESTAMP}.sql.gz" }, "scripts/restore-db.sh": { "type": "bash", "purpose": "MySQL 数据库恢复,从指定备份文件恢复", "env": [ "DATABASE_URL" ], "usage": "./restore-db.sh " }, "scripts/test-backup.sh": { "type": "bash", "purpose": "备份流程测试,执行一次备份并验证最新备份文件" }, "scripts/backup-verify.sh": { "type": "bash", "purpose": "备份完整性校验:检查文件存在/大小/gzip 完整性/SQL 内容结构/SQL 语法(可选,需 DATABASE_URL)", "env": [ "BACKUP_DIR", "DATABASE_URL", "BACKUP_VERIFY_MIN_SIZE" ], "exitCodes": { "0": "校验通过", "1": "校验失败" }, "options": [ "--min-size BYTES", "--no-sql-check", "--help" ] }, "scripts/backup-offsite-sync.sh": { "type": "bash", "purpose": "异地备份同步:支持 S3/OSS/NFS 后端,同步后校验文件数量,清理远程过期备份(保留 90 天)", "env": [ "BACKUP_DIR", "BACKUP_OFFSITE_BACKEND", "BACKUP_OFFSITE_REMOTE", "BACKUP_OFFSITE_BUCKET", "BACKUP_OFFSITE_ACCESS_KEY", "BACKUP_OFFSITE_SECRET_KEY", "BACKUP_OFFSITE_REGION", "BACKUP_OFFSITE_RETENTION_DAYS" ], "tools": [ "aws-cli (s3)", "rclone (s3/oss)", "ossutil (oss)", "rsync (nfs)" ], "exitCodes": { "0": "同步成功", "1": "同步失败" }, "options": [ "--backend TYPE", "--no-cleanup", "--no-verify", "--help" ] }, "scripts/dr-drill.sh": { "type": "bash", "purpose": "灾备演练:创建测试库→从备份恢复→数据完整性检查→冒烟测试→清理→生成报告到 docs/dr/reports/", "env": [ "DATABASE_URL", "BACKUP_DIR", "DR_DRILL_TEST_DB", "DR_DRILL_REPORT_DIR" ], "exitCodes": { "0": "演练成功", "1": "演练失败" }, "options": [ "--backup FILE", "--test-db NAME", "--no-cleanup", "--report-dir DIR", "--help" ] }, "scripts/dr-drill.ps1": { "type": "powershell", "purpose": "灾备演练(Windows PowerShell 5.1+ 版本),功能同 Bash 版本", "env": [ "DATABASE_URL", "BACKUP_DIR", "DR_DRILL_TEST_DB", "DR_DRILL_REPORT_DIR" ], "platform": "Windows", "options": [ "-BackupFile FILE", "-TestDb NAME", "-NoCleanup", "-ReportDir DIR", "-Help" ] }, "scripts/failover.sh": { "type": "bash", "purpose": "故障切换:检测主库健康→提升备库→更新应用配置→重启应用→验证切换", "env": [ "DATABASE_URL", "DATABASE_URL_STANDBY", "FAILOVER_APP_URL", "FAILOVER_APP_NAME", "FAILOVER_CONFIG_FILE", "FAILOVER_LOG_FILE" ], "exitCodes": { "0": "切换成功", "1": "切换失败" }, "options": [ "--auto", "--primary URL", "--standby URL", "--app-url URL", "--no-restart", "--dry-run", "--help" ] }, "scripts/health-check.sh": { "type": "bash", "purpose": "健康检查:检查应用 HTTP/数据库连接/磁盘空间/备份新鲜度,输出 JSON 报告", "env": [ "DATABASE_URL", "HEALTH_CHECK_URL", "BACKUP_DIR", "HEALTH_CHECK_DISK_THRESHOLD", "HEALTH_CHECK_BACKUP_MAX_AGE" ], "exitCodes": { "0": "健康", "1": "异常" }, "options": [ "--app-url URL", "--no-app", "--no-db", "--no-disk", "--no-backup", "--disk-threshold PCT", "--backup-max-age HRS", "--help" ] } }, "packageJsonScripts": { "audit": "npm audit --audit-level=moderate", "audit:report": "npm audit --json > audit-report.json", "security:audit": "npm audit --audit-level=moderate", "security:scan": "bash scripts/security-scan.sh", "backup": "bash scripts/backup-db.sh", "restore": "bash scripts/restore-db.sh", "dr:backup-verify": "bash scripts/backup-verify.sh", "dr:offsite-sync": "bash scripts/backup-offsite-sync.sh", "dr:drill": "bash scripts/dr-drill.sh", "dr:drill:ps1": "powershell -ExecutionPolicy Bypass -File scripts/dr-drill.ps1", "dr:health-check": "bash scripts/health-check.sh", "dr:failover": "bash scripts/failover.sh" }, "drDocs": { "docs/dr/dr-plan.md": "灾备计划文档:RTO/RPO 定义(4h/24h)、备份策略、故障切换流程、联系人列表、恢复步骤", "docs/dr/dr-runbook.md": "灾备操作手册:数据库故障/应用故障/备份失败/异地同步失败/演练失败/磁盘不足场景的诊断与处理", "docs/dr/reports/": "灾备演练报告存档目录(Markdown 格式,由 dr-drill.sh 生成)", "docs/dr/logs/": "故障切换日志目录(由 failover.sh 生成)" }, "drEnvVars": { "BACKUP_OFFSITE_BACKEND": "异地备份后端类型: s3|oss|nfs|none", "BACKUP_OFFSITE_REMOTE": "远程存储路径", "BACKUP_OFFSITE_BUCKET": "存储桶名称(仅 s3/oss)", "BACKUP_OFFSITE_ACCESS_KEY": "访问密钥", "BACKUP_OFFSITE_SECRET_KEY": "秘密密钥", "BACKUP_OFFSITE_REGION": "区域(默认 us-east-1)", "BACKUP_OFFSITE_RETENTION_DAYS": "远程备份保留天数(默认 90)", "DR_DRILL_TEST_DB": "演练测试数据库名(默认 next_edu_dr_drill)", "HEALTH_CHECK_URL": "应用健康检查 URL(默认 http://localhost:8015)", "DATABASE_URL_STANDBY": "备库连接 URL(故障切换时使用)", "FAILOVER_APP_NAME": "应用容器名(默认 nextjs-app)" }, "gitignore": { "added": [ "/backups/", "/audit-report.json", "/trivy-fs-report.json", "/trivy-image-report.json", "/snyk.sarif", "/security-summary.md", "/playwright-report/", "/test-results/", "/tests/visual/.auth/" ], "exceptions": [ ".env.example (灾备环境变量示例,允许提交)" ] } }, "testing": { "e2e": { "configFile": "playwright.config.ts", "testDir": "./tests", "baseURL": "http://127.0.0.1:3000", "webServer": { "command": "npm run dev", "port": 3000, "timeout": 180000, "reuseExistingServer": "!process.env.CI", "env": { "SKIP_ENV_VALIDATION": "1", "NEXTAUTH_SECRET": "test-nextauth-secret", "NEXTAUTH_URL": "http://127.0.0.1:3000", "DATABASE_URL": "mysql://test:test@127.0.0.1:3306/test_db" } }, "projects": [ { "name": "chromium", "testDir": "./tests/e2e", "channel": "CI: undefined, local: chrome" }, { "name": "visual-chromium", "testDir": "./tests/visual", "channel": "CI: undefined, local: chrome" } ], "snapshotPathTemplate": "{testDir}/__screenshots__/{testFilePath}/{arg}{ext}", "expect": { "toHaveScreenshot": { "maxDiffPixelRatio": 0.01, "animations": "disabled", "caret": "hide" }, "toMatchSnapshot": { "maxDiffPixelRatio": 0.01 } }, "retries": "CI: 2, local: 0", "workers": "CI: 2, local: default", "testFiles": { "smoke-auth.spec.ts": { "coverage": "登录/注册页面控件渲染冒烟测试", "requiresDb": false }, "auth-business-flow.spec.ts": { "coverage": "注册→登录→访问受保护区域完整流程", "requiresDb": true }, "full-route-regression.spec.ts": { "coverage": "全路由清单完整性 + 公开/受保护路由守卫", "requiresDb": false }, "auth.spec.ts": { "coverage": "认证页面(登录/注册/隐私/协议)渲染 + 未认证重定向", "requiresDb": false }, "navigation.spec.ts": { "coverage": "admin/teacher/student 导航链接无 404", "requiresDb": true, "envVars": [ "E2E_ADMIN_EMAIL", "E2E_TEACHER_EMAIL", "E2E_STUDENT_EMAIL" ] }, "announcements.spec.ts": { "coverage": "公告页面未认证重定向 + 登录后渲染", "requiresDb": "partial" }, "grades.spec.ts": { "coverage": "成绩页面未认证重定向 + 登录后渲染", "requiresDb": "partial" } } }, "visual": { "configFile": "tests/visual/visual.config.ts", "snapshotDir": "tests/visual/__screenshots__", "storageStateDir": "tests/visual/.auth/", "viewports": { "desktop": { "width": 1920, "height": 1080 }, "tablet": { "width": 768, "height": 1024 }, "mobile": { "width": 375, "height": 812 } }, "themes": [ "light", "dark" ], "defaultMaxDiffPixelRatio": 0.01, "testAccounts": { "admin": { "email": "admin@xiaoxue.edu.cn", "envVars": [ "VISUAL_ADMIN_EMAIL", "VISUAL_ADMIN_PASSWORD" ] }, "teacher": { "email": "admin@xiaoxue.edu.cn", "envVars": [ "VISUAL_TEACHER_EMAIL", "VISUAL_TEACHER_PASSWORD" ] }, "student": { "email": "admin@xiaoxue.edu.cn", "envVars": [ "VISUAL_STUDENT_EMAIL", "VISUAL_STUDENT_PASSWORD" ] } }, "testFiles": { "homepage.spec.ts": { "coverage": "登录页在 desktop/tablet/mobile × light/dark 下的快照", "requiresDb": false }, "admin-dashboard.spec.ts": { "coverage": "管理员仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "admin" }, "teacher-dashboard.spec.ts": { "coverage": "教师仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "teacher" }, "student-dashboard.spec.ts": { "coverage": "学生仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "student" } }, "helpers": { "auth.ts": [ "setupAuthState(role)", "loginByUI(page, role)", "storageStatePath(role)" ], "visual-helpers.ts": [ "setViewport(page, size)", "setTheme(page, theme)", "waitForPageReady(page)", "maskDynamicElements(page, selectors)", "buildMaskOption(masks)" ] }, "scripts": { "test:visual": "playwright test --project=visual-chromium", "test:visual:update": "playwright test --project=visual-chromium --update-snapshots" } } } }