feat(school,classes): 实现 P2 长期问题全量改进项

P2-2: 新增 OrgTreeNav 组件(学校→年级→班级三级树形导航,支持搜索过滤/选中高亮/展开折叠)

P2-3: 新增 promoteGradesAction 年级升级功能(中文数字/阿拉伯数字识别,按 order 降序避免冲突)

P2-4: 新增 bulkEnrollStudentsAction(CSV 批量导入学生)+ bulkAssignSubjectTeachersAction(CSV 批量分配教师)

P2-5: 为 department/academicYear/grade 的 9 个 CRUD Action 补充 logAudit 审计日志

同步更新架构图文档 004/005
This commit is contained in:
SpecialX
2026-06-23 08:55:21 +08:00
parent 4da9194a5e
commit c766951374
11 changed files with 761 additions and 81 deletions

View File

@@ -4625,11 +4625,29 @@
"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(6参数)",
"purpose": "知识点操作门面 Hook(组合 useKpDialogState + useKpCrud对外保持原有 API 不变)",
"usedBy": [
"textbook-reader.tsx"
]
@@ -4637,8 +4655,8 @@
{
"name": "useGraphData",
"file": "hooks/use-graph-data.ts",
"signature": "(textbookId: string, viewMode: GraphViewMode) => { data: KnowledgeGraphData | null, isLoading: boolean, error: string | null, reload: () => void }",
"purpose": "Task 11 新增:知识图谱数据加载 Hook按 textbookId + viewMode 加载,使用派生值模式避免 effect 中 setState",
"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"
]
@@ -4975,42 +4993,51 @@
"name": "TextbookReader",
"purpose": "教材阅读器"
},
{
"name": "TeacherTextbookReader",
"purpose": "教师端 TextbookReader 客户端包装v1 测试修复:解决 Server→Client 函数 prop 序列化问题)"
},
{
"name": "TextbookSettingsDialog",
"purpose": "教材设置对话框"
}
],
"uiDeps": [],
"uiDepsNote": "已通过 render prop 解耦,不再直接 import questions 模块组件",
"uiDepsNote": "已通过 render prop 解耦,TeacherTextbookReader 已移至 app 层src/app/(dashboard)/teacher/textbooks/[id]/_components/textbooks 模块不再直接 import questions 模块组件",
"knownIssues": [
"i18n 覆盖率约 95%chapter-sidebar-list 已接入,actions.ts 已接入,section-error-boundary 默认值已改英文",
"i18n 覆盖率约 98%chapter-sidebar-list/actions.ts/section-error-boundary/8 处硬编码英文 toast 已全部接入Zod refine 消息改为英文,错误分支不再复用成功消息",
"类型断言残留 3 处 as string",
"P2 图谱方向键导航未实现",
"v1 测试已修复textbook-reader.tsx SheetTrigger 越界、Server→Client 函数 prop 序列化、seed 数据 i18n key 不匹配"
"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 层解耦跨模块依赖"
],
"files": {
"actions.ts": 502,
"data-access.ts": 586,
"data-access-graph.ts": 184,
"types.ts": 94,
"schema.ts": 62,
"constants.ts": 99,
"utils.ts": 203,
"graph-layout.ts": 105,
"actions.ts": 515,
"data-access.ts": 662,
"data-access-graph.ts": 207,
"types.ts": 106,
"schema.ts": 81,
"constants.ts": 96,
"utils.ts": 225,
"graph-layout.ts": 121,
"analytics.tsx": 43,
"hooks/use-knowledge-point-actions.ts": 121,
"hooks/use-knowledge-point-actions.ts": 49,
"hooks/use-kp-dialog-state.ts": 38,
"hooks/use-kp-crud.ts": 122,
"hooks/use-text-selection.ts": 57,
"hooks/use-graph-data.ts": 58,
"components/teacher-textbook-reader.tsx": 41,
"components/knowledge-graph.tsx": 249,
"components/graph-kp-node.tsx": 80,
"components/graph-prerequisite-edge.tsx": 40,
"components/graph-toolbar.tsx": 77,
"components/graph-node-detail-panel.tsx": 171
"hooks/use-graph-data.ts": 76,
"components/chapter-sidebar-list.tsx": 342,
"components/create-chapter-dialog.tsx": 111,
"components/graph-kp-node.tsx": 92,
"components/graph-node-detail-panel.tsx": 181,
"components/graph-prerequisite-edge.tsx": 48,
"components/graph-toolbar.tsx": 93,
"components/knowledge-graph.tsx": 376,
"components/knowledge-point-dialogs.tsx": 175,
"components/knowledge-point-list.tsx": 122,
"components/section-error-boundary.tsx": 71,
"components/textbook-card.tsx": 181,
"components/textbook-content-panel.tsx": 189,
"components/textbook-filters.tsx": 71,
"components/textbook-form-dialog.tsx": 139,
"components/textbook-reader.tsx": 446,
"components/textbook-settings-dialog.tsx": 203
},
"auditReport": "audit/textbooks-audit-report.md"
}
@@ -5963,8 +5990,8 @@
},
{
"path": "actions-invitations.ts",
"lines": 280,
"description": "邀请码与注册8 个 ActionP0-3 修复)"
"lines": 502,
"description": "邀请码与注册 + 批量操作10 个 ActionP0-3 修复P2-4 新增批量导入学生/批量分配教师"
},
{
"path": "actions-schedule.ts",
@@ -6022,6 +6049,13 @@
"signature": "(gradeId) => Promise<ActionState<string>>",
"purpose": "删除年级"
},
{
"name": "promoteGradesAction",
"permission": "GRADE_MANAGE",
"signature": "(prevState, formData) => Promise<ActionState<string>>",
"purpose": "年级升级order +1 + 名称升级)",
"auditLog": "grade.promote"
},
{
"name": "createDepartmentAction",
"permission": "SCHOOL_MANAGE",
@@ -6401,6 +6435,15 @@
"usedBy": [
"updateAcademicYear"
]
},
{
"name": "OrgTreeNode",
"type": "type",
"definition": "组织架构树节点(学校/年级/班级三级P2-2 修复)",
"usedBy": [
"getOrgTree",
"school/components/org-tree-nav.tsx"
]
}
],
"components": [
@@ -6455,6 +6498,12 @@
"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": [
@@ -8622,7 +8671,7 @@
"deps": [
"shared.db",
"shared.db.schema.gradeRecords",
"grades/lib/grade-utils.buildScopeClassFilter"
"grades/lib/scope-filter.buildScopeClassFilter"
],
"usedBy": [
"grades/data-access.getClassGradeStatsWithMeta"
@@ -8664,7 +8713,7 @@
"shared.db",
"shared.db.schema.gradeRecords",
"shared.db.schema.users",
"grades/lib/grade-utils.buildScopeClassFilter",
"grades/lib/scope-filter.buildScopeClassFilter",
"users/data-access.getUserNamesByIds"
],
"usedBy": [
@@ -9368,11 +9417,12 @@
"name": "exportGradeRecordsToExcel",
"signature": "(params: { classId: string; subjectId?: string; examId?: string; scope: DataScope; currentUserId?: string }) => Promise<Buffer>",
"file": "export.ts",
"purpose": "导出成绩单Sheet1 成绩明细Sheet2 统计汇总:均分/中位数/最高分/最低分/标准差/及格率/优秀率/参考人数P3 更新:传递 scope/currentUserId 到 data-access",
"purpose": "导出成绩单Sheet1 成绩明细Sheet2 统计汇总:均分/中位数/最高分/最低分/标准差/及格率/优秀率/参考人数P3 更新:传递 scope/currentUserId 到 data-accessP3-7硬编码中文改用 next-intl getTranslations",
"deps": [
"shared.lib.excel.exportToExcel",
"data-access.getGradeRecords",
"data-access.getClassGradeStats"
"data-access.getClassGradeStats",
"next-intl/server.getTranslations"
],
"usedBy": [
"actions.exportGradesAction",
@@ -9383,7 +9433,7 @@
"name": "exportClassGradeReportToExcel",
"signature": "(params: { classId: string; scope: DataScope; currentUserId?: string }) => Promise<Buffer>",
"file": "export.ts",
"purpose": "导出班级成绩总表(多科目横向对比,含总分/平均分/排名列P3 更新:适配 PaginatedGradeRecords 结构 + 传递 scope/currentUserId",
"purpose": "导出班级成绩总表(多科目横向对比,含总分/平均分/排名列P3 更新:适配 PaginatedGradeRecords 结构 + 传递 scope/currentUserIdP3-6复用 stats-service.computeAverageScore 替代局部 avgP3-7硬编码中文改用 next-intl getTranslations",
"deps": [
"shared.db",
"shared.db.schema.classes",
@@ -9391,7 +9441,9 @@
"shared.db.schema.gradeRecords",
"shared.db.schema.users",
"shared.lib.excel.exportToExcel",
"data-access.getGradeRecords"
"data-access.getGradeRecords",
"stats-service.computeAverageScore",
"next-intl/server.getTranslations"
],
"usedBy": [
"actions.exportGradesAction"
@@ -9440,8 +9492,8 @@
{
"name": "buildScopeClassFilter",
"signature": "(scope: DataScope, currentUserId?: string) => SQL | null",
"file": "lib/grade-utils.ts",
"purpose": "根据 DataScope 构建 gradeRecords 表的行级权限过滤条件P1-2 新增:从 data-access/data-access-analytics 抽取P3 更新class_members scope 内置 studentId 过滤,需传入 currentUserId 参数)",
"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",
@@ -9469,16 +9521,17 @@
"name": "computeAverageScore",
"signature": "(scores: number[]) => number",
"file": "stats-service.ts",
"purpose": "计算分数平均值(空数组返回 0P1-1 新增:从 data-access.getStudentGradeSummary 抽取为纯函数)",
"purpose": "计算分数平均值(空数组返回 0P1-1 新增:从 data-access.getStudentGradeSummary 抽取为纯函数P3-6export.ts 复用此函数替代局部 avg",
"usedBy": [
"data-access.getStudentGradeSummary"
"data-access.getStudentGradeSummary",
"export.exportClassGradeReportToExcel"
]
},
{
"name": "buildGradeTrendPoints",
"signature": "(rows: RawScoreRow[]) => GradeTrendPoint[]",
"file": "stats-service.ts",
"purpose": "构建成绩趋势数据点(按考试标题分组,归一化分数 0-100P1-1 新增:从 data-access-analytics.getGradeTrend 抽取为纯函数)",
"purpose": "构建成绩趋势数据点(按考试标题分组,归一化分数 0-100P1-1 新增:从 data-access-analytics.getGradeTrend 抽取为纯函数P3-24使用 isGradeTrendType 类型守卫替代 as 断言",
"usedBy": [
"data-access-analytics.getGradeTrend"
]
@@ -11040,19 +11093,23 @@
},
{
"name": "logNotificationSend",
"signature": "(result: ChannelSendResult) => void",
"signature": "(result: ChannelSendResult, payload?: { userId: string; title: string }) => Promise<void>",
"file": "data-access.ts",
"purpose": "记录单条发送日志(当前使用 console.info未来可扩展 notification_logs 表",
"deps": [],
"purpose": "记录单条发送日志到 notification_logs 表DB 写入失败时降级 console不阻塞通知流程",
"deps": [
"shared.db",
"shared.db.schema.notificationLogs",
"@paralleldrive/cuid2"
],
"usedBy": [
"logNotificationSendBatch"
]
},
{
"name": "logNotificationSendBatch",
"signature": "(results: ChannelSendResult[]) => void",
"signature": "(results: ChannelSendResult[], payload?: { userId: string; title: string }) => Promise<void>",
"file": "data-access.ts",
"purpose": "批量记录发送日志",
"purpose": "批量记录发送日志(并行调用 logNotificationSend",
"deps": [
"logNotificationSend"
],
@@ -11221,6 +11278,13 @@
"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",
@@ -14339,7 +14403,7 @@
},
"dbTables": {
"_meta": {
"total": 58,
"total": 59,
"orm": "Drizzle ORM 0.45",
"database": "MySQL",
"idStrategy": "CUID2 (varchar length 128)",
@@ -14598,6 +14662,10 @@
"owner": "notifications",
"description": "消息通知"
},
"notificationLogs": {
"owner": "notifications",
"description": "通知发送日志(channel/status/messageId/error/sentAt)"
},
"notificationPreferences": {
"owner": "notifications",
"description": "通知偏好(email/sms/push + 分类开关)"
@@ -14980,7 +15048,9 @@
"textbooks": {
"dependsOn": [
"shared",
"auth"
"auth",
"users",
"classes"
],
"uses": {
"shared": [
@@ -14990,6 +15060,12 @@
],
"auth": [
"auth"
],
"users": [
"data-access.getCurrentStudentUser"
],
"classes": [
"data-access-students.getClassStudents"
]
}
},