refactor(dashboard): V2 审计重构 — i18n 补齐 + 共享抽象 + 单测 + a11y

V2 审计报告(docs/architecture/audit/dashboard-audit-report-v2.md)发现并修复:

- P0 i18n:10 个子组件硬编码字符串全部接入 next-intl(teacher-quick-actions /
  teacher-classes-card / teacher-homework-card / teacher-schedule /
  recent-submissions / teacher-grade-trends / student-grades-card /
  student-today-schedule-card / student-upcoming-assignments-card /
  admin-dashboard),新增 ~50 个翻译键
- P1 共享抽象:新增 DashboardGreetingHeader 组件,消除 teacher/student
  头部 90% 重复代码,两个 Header 改为薄包装
- P2 单测:为 6 个纯函数添加 31 个单元测试
  (tests/integration/dashboard/dashboard-utils.test.ts)
- P2 a11y:admin 表格 caption、teacher/student 视图语义化标签
  (header / section aria-label / aside aria-label)
- 同步架构图 004/005
This commit is contained in:
SpecialX
2026-06-22 17:01:00 +08:00
parent 10c668f36a
commit e997abaf5e
41 changed files with 1811 additions and 516 deletions

View File

@@ -948,7 +948,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- Actions`getAdminDashboardAction` / `getTeacherDashboardAction` / `getStudentDashboardAction` / `getParentDashboardAction`(均调用 `requirePermission()` 校验对应 `DASHBOARD_*_READ` 权限)
- Data-access`getAdminDashboardData`(并行调用 6 个模块 stats 函数)
- Lib 纯函数:`toWeekday` / `countStudentAssignments` / `sortUpcomingAssignments` / `filterTodaySchedule` / `computeTeacherMetrics` / `getGreetingKey`
- Components`AdminDashboardView` / `TeacherDashboardView` / `StudentDashboard` / `UserGrowthChart`(均接入 next-intl i18n
- Components`AdminDashboardView` / `TeacherDashboardView` / `StudentDashboard` / `UserGrowthChart` / `DashboardGreetingHeader`共享问候头部V2 抽象)/ `DashboardSection`(分区 Error Boundary + Suspense + 骨架屏)(均接入 next-intl i18n
**依赖关系**
- 依赖:`shared/*``@/auth``classes`(通过 data-access`homework`(通过 data-access`users`(通过 data-access`parent`(通过 data-access.getParentDashboardData`textbooks`/`questions`/`exams`(通过各模块 dashboard stats 函数)、`recharts``next-intl`
@@ -969,6 +969,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ P2 已修复2026-06-22新增 `components/dashboard-section.tsx`,每个独立数据区块用 Error Boundary + Suspense + 骨架屏包裹,单区块崩溃/加载不波及整页5 种骨架变体stats/card/chart/table/list
- V1 新增:`AdminDashboardData` 类型含 `userGrowth`/`homeworkTrend` 字段,`data-access.ts` 当前返回空数组占位,待后续接入真实统计
- parent 仪表盘组件仍位于 `modules/parent/components/parent-dashboard.tsx`,通过 `dashboard/actions.getParentDashboardAction` 调用(架构决策:保留在 parent 模块以避免移动文件破坏其他 import
- ✅ P0 已修复2026-06-22 V210 个子组件 i18n 遗漏全部补齐teacher-quick-actions / teacher-classes-card / teacher-homework-card / teacher-schedule / recent-submissions / teacher-grade-trends / student-grades-card / student-today-schedule-card / student-upcoming-assignments-card / admin-dashboard新增 ~50 个翻译键
- ✅ P1 已修复2026-06-22 V2抽象共享组件 `DashboardGreetingHeader`,消除 teacher/student 头部 90% 重复代码
- ✅ P2 已修复2026-06-22 V2为 6 个纯函数添加 31 个单元测试(`tests/integration/dashboard/dashboard-utils.test.ts`
- ✅ P2 已修复2026-06-22 V2a11y 增强 — admin 表格 `<caption>`、teacher/student 视图语义化标签(`<header>` / `<section aria-label>` / `<aside aria-label>`
**文件清单**
| 文件 | 行数 | 职责 |
@@ -978,10 +982,12 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `lib/dashboard-utils.ts` | 170 | 纯逻辑工具函数weekday / 统计 / 排序 / 指标计算 / 问候语) |
| `types.ts` | 74 | Admin / Teacher / Student 类型定义 |
| `components/dashboard-section.tsx` | 165 | 分区 Error Boundary + Suspense + 骨架屏5 种变体stats/card/chart/table/list |
| `components/admin-dashboard/admin-dashboard.tsx` | 267 | 管理员仪表盘视图i18n |
| `components/dashboard-greeting-header.tsx` | 36 | 共享问候头部组件V2 抽象,消除 teacher/student 头部重复 |
| `components/admin-dashboard/admin-dashboard.tsx` | 267 | 管理员仪表盘视图i18n + a11y 表格 caption |
| `components/admin-dashboard/user-growth-chart.tsx` | 50 | recharts 折线图i18n |
| `components/teacher-dashboard/*.tsx` | 9 文件 | 教师仪表盘组件i18n |
| `components/student-dashboard/*.tsx` | 6 文件 | 学生仪表盘组件i18n |
| `components/teacher-dashboard/*.tsx` | 9 文件 | 教师仪表盘组件i18n + a11y 语义化标签 |
| `components/student-dashboard/*.tsx` | 6 文件 | 学生仪表盘组件i18n + a11y 语义化标签 |
| `tests/integration/dashboard/dashboard-utils.test.ts` | 408 | 6 个纯函数的 31 个单元测试V2 新增) |
---
@@ -990,57 +996,78 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**职责**站内私信messages 表 CRUD
**导出函数**
- Actions`sendMessageAction` / `getMessagesAction` / `getMessageAction` / `deleteMessageAction` / `getNotificationsAction` / `markNotificationReadAction` / `markAllNotificationsReadAction` / `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`
- Data-access`getMessages` / `getMessageById` / `getMessageThread` / `createMessage` / `markMessageAsRead` / `deleteMessage` / `getUnreadMessageCount` / `getRecipients`(按 DataScope 过滤可发送对象class_taught 教师→学生、grade_managed 年级管理员→教师/学生、all 管理员、class_members 学生→自己班级的任课教师/班主任、children 家长→孩子的班主任/任课教师;通过 classes data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID(通知 CRUD 通过 re-export 从 notifications 模块重导出,保持向后兼容
- Actions`sendMessageAction` / `markMessageAsReadAction` / `deleteMessageAction` / `getMessagesAction` / `getMessageDetailAction` / `getRecipientsAction` / `getUnreadMessageCountAction` / `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`(✅ P1-4 已修复:通知 CRUD Action 已迁移至 notifications 模块messaging 仅保留私信和通知偏好 Action
- Data-access`getMessages` / `getMessageById` / `getMessageThread` / `createMessage` / `markMessageAsRead` / `deleteMessage` / `getUnreadMessageCount` / `getRecipients`(按 DataScope 过滤可发送对象class_taught 教师→学生、grade_managed 年级管理员→教师/学生、all 管理员、class_members 学生→自己班级的任课教师/班主任、children 家长→孩子的班主任/任课教师;通过 classes data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID/ `getMessagesPageData`(✅ P1-5 新增:消息首页编排函数,一次性获取消息列表和通知列表
- Hooks`useMessageSearch`(✅ P1-7 新增:消息搜索 hook含防抖和请求竞态取消
- Notification-preferences~~re-export shim实际逻辑在 `notifications/preferences.ts`~~ ✅ P0-b 已修复:`notification-preferences.ts` 文件已删除(通知模块去重),消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`
**依赖关系**
- 依赖:`shared/*``@/auth``notifications`(✅ P0-4 / P1-5 已修复:通过 `sendNotification` dispatcher 发送通知,通知 CRUD 和偏好已迁移至 notifications 模块)、`classes`(通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID支持学生 class_members 和家长 children 数据范围)、`users`(通过 data-access.getUserNamesByIds 获取用户显示名称)
- 被依赖:`notifications`(✅ 已消除反向依赖)、`settings`(通知偏好表单)、`layout`通知下拉
- 依赖:`shared/*``@/auth``notifications`(✅ P0-4 / P1-5 已修复:通过 `sendNotification` dispatcher 发送通知,通知 CRUD 和偏好已迁移至 notifications 模块;✅ P1-4 已修复:通知 UI 组件已迁移至 notifications 模块)、`classes`(通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID支持学生 class_members 和家长 children 数据范围)、`users`(通过 data-access.getUserNamesByIds 获取用户显示名称)
- 被依赖:`notifications`(✅ 已消除反向依赖)、`settings`(通知偏好表单)、`layout`✅ P1-4 已修复:通知下拉组件改为从 notifications 模块导入
**已知问题**
- ✅ P0-4 已修复:~~`sendMessageAction` 绕过 notifications dispatcher 直接调用 `createNotification`~~ 改为调用 `notifications.sendNotification`,通知 CRUD 已迁移至 notifications 模块
- ✅ P0 已修复:~~与 notifications 双向依赖 + 职责重叠~~ 通知相关表messageNotifications / notificationPreferences所有权已移交 notifications 模块messaging 仅保留 messages 表
- ✅ P1-5 已修复:~~同时管理 3 类数据messages + messageNotifications + notificationPreferences~~ 仅管理 messages 表,通知相关数据由 notifications 模块管理
- ✅ P1 已修复:~~通知相关 Action 使用 `requireAuth()` 而非 `requirePermission()`~~ 5 个通知 ActiongetNotifications / markNotificationAsRead / markAllNotificationsAsRead / getNotificationPreferences / updateNotificationPreferences改为 `requirePermission(Permissions.MESSAGE_READ)`
- ✅ P1 已修复:~~通知相关 Action 使用 `requireAuth()` 而非 `requirePermission()`~~ 通知 Action 已迁移至 notifications 模块并改为 `requirePermission(Permissions.MESSAGE_READ)`
- ✅ P1 已修复:~~`markMessageAsReadAction` / `deleteMessageAction` / `getMessageDetailAction` 缺少 Zod 校验~~ 已添加 `MessageIdSchema` 校验 messageId 参数
- ✅ P1 已修复:~~`updateNotificationPreferencesAction` 缺少 Zod 校验~~ 已添加 `UpdateNotificationPreferencesSchema` 校验 8 个布尔字段
- ✅ P2 已修复:`data-access.ts` 中 3 处 `or(...)!` 非空断言清理为安全守卫(条件 push
- ✅ P0-b 已修复:~~`notification-preferences.ts` re-export shim 文件~~ 已删除通知模块去重8 个消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`,消除 messaging 模块对通知偏好的冗余 re-export 层
- ✅ P1 已修复:~~全模块零 i18n中英文案硬编码~~ 所有组件接入 next-intl`useTranslations("messages")`),新增 `src/shared/i18n/messages/{zh-CN,en}/messages.json` 翻译字典title/description/tabs/actions/form/status/meta/notificationType/search/empty/messages/error 共 13 个命名空间);所有页面 `page.tsx` 使用 `generateMetadata` + `getTranslations` 替代硬编码 metadata
- ✅ P1 已修复:~~缺 Error Boundary~~ 新增 3 个 `error.tsx` 错误边界(`/messages``/messages/[id]``/messages/compose`),统一使用 `EmptyState` + i18n 错误文案 + 重试按钮
- ✅ P2 已修复a11y 改进,`message-list.tsx` / `notification-dropdown.tsx` 添加 `aria-label` / `aria-hidden`
- ✅ P2 已修复a11y 改进,`message-list.tsx` 添加 `aria-label` / `aria-hidden`
- ✅ P1-4 已修复:~~通知组件notification-list.tsx / notification-dropdown.tsx放在 messaging/components 下,直接 import notifications 模块类型和 messaging/actions~~ 两个组件已迁移至 `notifications/components/`,通知 CRUD Action 已迁移至 `notifications/actions.ts`messaging 模块仅保留私信组件
- ✅ P1-5 已修复:~~页面层 `Promise.all` 编排 messaging 和 notifications 两个模块的 data-access~~ 新增 `getMessagesPageData` 编排函数,页面层仅调用单一函数
- ✅ P1-7 已修复:~~消息列表客户端 `useEffect` + `setTimeout` 防抖搜索未取消已发出的请求~~ 搜索逻辑抽离为 `useMessageSearch` hook含防抖 + 请求竞态取消);~~无分页 UI~~ 新增客户端分页 UIPAGE_SIZE=20ChevronLeft/ChevronRight 按钮)
- ✅ P1-9 已修复:~~`deleteMessage` 两个独立 UPDATE 无事务~~ 改为 `db.transaction` 包裹 senderDeletedAt 和 receiverDeletedAt 更新,保证原子性
- ✅ P2-11 已修复:~~发送/删除消息无埋点~~ `sendMessageAction` / `markMessageAsReadAction` / `deleteMessageAction` 新增 `trackEvent` 埋点message.sent / message.marked_read / message.deleted
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 276 | 9 个 Server Action(通知相关 Action 委托 notifications 模块) |
| `data-access.ts` | 199 | 私信 CRUD + re-export 通知 CRUD向后兼容 |
| ~~`notification-preferences.ts`~~ | ~~11~~ | ~~re-export shim~~ ✅ P0-b 已删除(消费方改为直接从 notifications/preferences 导入) |
| `schema.ts` | 41 | 私信发送校验 + messageId 校验 + 通知偏好更新校验 |
| `types.ts` | 72 | 私信类型 + re-export 通知类型(向后兼容 |
| `actions.ts` | ~260 | 7私信 Server Action + 2 个通知偏好 Action✅ P1-4通知 CRUD Action 已迁移至 notifications 模块) |
| `data-access.ts` | ~270 | 私信 CRUD + `getMessagesPageData` 编排函数(✅ P1-5 新增 |
| `schema.ts` | 44 | 私信发送校验 + messageId 校验 + 通知偏好更新校验 |
| `types.ts` | 52 | 私信类型 + re-export 通知类型(向后兼容) |
| `hooks/use-message-search.ts` | ~60 | ✅ P1-7 新增:消息搜索 hook防抖 + 请求竞态取消 |
**组件清单**
| 组件 | 职责 |
|------|------|
| `components/message-list.tsx` | 消息列表(✅ P1-7使用 `useMessageSearch` hook + 客户端分页 UIPAGE_SIZE=20 |
| `components/message-detail.tsx` | 消息详情(含回复) |
| `components/message-compose.tsx` | 撰写新消息 |
| `components/unread-message-badge.tsx` | 未读消息计数徽章(侧边栏,每 60 秒轮询 `getUnreadMessageCountAction` |
**客户端行为**
- `message-list.tsx`:客户端调用 `getMessagesAction` 搜索消息useMessageSearch hook400ms 防抖,请求竞态取消)
- `unread-message-badge.tsx`:每 60 秒轮询 `getUnreadMessageCountAction` 刷新未读计数
---
## 2.14 notifications通知分发模块
**职责**多渠道通知分发SMS/Email/WeChat/InApp+ 站内通知 CRUD + 通知偏好管理。
**职责**多渠道通知分发SMS/Email/WeChat/InApp+ 站内通知 CRUD + 通知偏好管理 + 通知 UI 组件
**导出函数**
- Actions`sendNotificationAction` / `sendClassNotificationAction`
- Actions`sendNotificationAction` / `sendClassNotificationAction` / `getNotificationsAction` / `getUnreadNotificationCountAction` / `markNotificationAsReadAction` / `markAllNotificationsAsReadAction`(✅ P1-4 新增:后 4 个通知 CRUD Action 从 messaging 模块迁移)
- Dispatcher`sendNotification(payload)` / `sendBatchNotifications(payloads)`
- Data-access`createNotification` / `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount` / `getUserContactInfo` / `logNotificationSend` / `logNotificationSendBatch`(✅ P0-4 / P1-5 修复后从 messaging 迁移)
- Preferences`getNotificationPreferences` / `upsertNotificationPreferences`(✅ P0-4 / P1-5 修复后从 messaging 迁移)
- Channels`InAppChannelSender` / `SmsChannelSender` / `EmailChannelSender` / `WeChatChannelSender`
- Components`NotificationList` / `NotificationDropdown`(✅ P1-4 新增:从 messaging/components 迁移)
**依赖关系**
- 依赖:`shared/*``@/auth``classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getStudentIdsByClassId
- 被依赖:`messaging`(✅ P0-4 / P1-5 已修复messaging 通过 `sendNotification` dispatcher 发送通知,通知 CRUD 和偏好通过 re-export 保持向后兼容
- 被依赖:`messaging`(✅ P0-4 / P1-5 已修复messaging 通过 `sendNotification` dispatcher 发送通知;✅ P1-4 已修复:通知 UI 组件由 notifications 模块自持messaging 不再反向依赖)、`layout`(✅ P1-4 已修复:通知下拉组件直接从 notifications 模块导入)、`app/(dashboard)/messages`(✅ P1-4 已修复:通知列表组件直接从 notifications 模块导入
**已知问题**
- ✅ P0-4 已修复:~~不拥有任何数据,全部依赖 messaging 模块~~ messageNotifications 和 notificationPreferences 表所有权已从 messaging 迁移至 notifications 模块
- ✅ P0 已修复:~~与 messaging 双向依赖~~ notifications 不再反向依赖 messagingin-app-channel 改为静态导入本地 createNotification
- ✅ P1-1 已修复:~~`sendClassNotificationAction` 直查 `classes`/`classEnrollments`~~ 改为调用 `classes/data-access.getClassExists` / `getStudentIdsByClassId`
- ✅ P1-4 已修复:~~通知 UI 组件放在 messaging/components 下,直接 import notifications 类型和 messaging/actions~~ `notification-list.tsx``notification-dropdown.tsx` 已迁移至 `notifications/components/`,通知 CRUD Action 已从 messaging 迁移至 `notifications/actions.ts`,消除 UI 层跨模块耦合
- ✅ P2-11 已修复:~~通知标记已读无埋点~~ `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` 新增 `trackEvent` 埋点notification.marked_read / notification.marked_all_read
- ⚠️ P1发送日志仅 console`notification_logs`
- ✅ 渠道抽象优秀(接口 + 工厂 + Mock 实现)
@@ -1050,10 +1077,21 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `dispatcher.ts` | 152 | 渠道选择 + 并行分发 |
| `data-access.ts` | 177 | 站内通知 CRUD + 用户联系方式 + 日志P0-4 / P1-5 修复后新增通知 CRUD |
| `preferences.ts` | 166 | 通知偏好 CRUDP0-4 / P1-5 修复后从 messaging 迁移) |
| `actions.ts` | 119 | 2 个 Server Action |
| `actions.ts` | ~260 | 6 个 Server Action(✅ P1-4新增 4 个通知 CRUD Action |
| `types.ts` | 120 | 通知负载 + 渠道配置 + 通知记录 + 偏好类型P0-4 / P1-5 修复后扩充) |
| `index.ts` | 61 | 对外导出入口 |
| `index.ts` | ~75 | 对外导出入口(✅ P1-4新增组件和 CRUD Action 导出) |
| `channels/*` | 5 文件 | 4 个渠道实现 |
| `components/notification-list.tsx` | ~140 | ✅ P1-4 新增(从 messaging 迁移):通知列表组件 |
| `components/notification-dropdown.tsx` | ~180 | ✅ P1-4 新增(从 messaging 迁移):通知下拉菜单组件 |
**组件清单**
| 组件 | 职责 |
|------|------|
| `components/notification-list.tsx` | 通知列表(消息页底部,展示所有通知,支持标记已读) |
| `components/notification-dropdown.tsx` | 通知下拉菜单(站点头部,每 30 秒轮询 `getNotificationsAction` + `getUnreadNotificationCountAction` |
**客户端行为**
- `notification-dropdown.tsx`:每 30 秒轮询 `getNotificationsAction`pageSize=10`getUnreadNotificationCountAction` 刷新通知和未读计数
---
@@ -1090,7 +1128,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**导出函数**
- Actions`getAnnouncementsAction` / `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction`(✅ P1-2 已修复actions 层不再直接访问 DB全部下沉到 data-access✅ 发布公告时触发通知模块 `sendBatchNotifications`
- Data-access`getAnnouncements`(支持 `audience` 受众过滤)/ `getAnnouncementById` / `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`(后 5 个为 P1-2 新增)
- Data-access`getAnnouncements`(支持 `audience` 受众过滤)/ `getAnnouncementById` / `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`(后 5 个为 P1-2 新增)/ `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData`(✅ P1-5 新增:管理端列表页和编辑页编排函数,页面层仅调用单一函数)
**依赖关系**
- 依赖:`shared/*``@/auth``school`(获取年级列表)、`classes`(获取班级列表 + 解析受众)、`users`(获取目标用户 ID 列表)、`notifications`(发布公告时发送通知)
@@ -1107,14 +1145,26 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ P1 已修复:~~全模块零 i18n中英文案硬编码~~ 所有组件接入 next-intl`useTranslations("announcements")`),新增 `src/shared/i18n/messages/{zh-CN,en}/announcements.json` 翻译字典title/description/filter/status/type/form/actions/messages/meta/empty/error 共 11 个命名空间);所有页面 `page.tsx` 使用 `generateMetadata` + `getTranslations` 替代硬编码 metadata
- ✅ P1 已修复:~~缺 Error Boundary~~ 新增 4 个 `error.tsx` 错误边界(`/announcements``/announcements/[id]``/admin/announcements``/admin/announcements/[id]`),统一使用 `EmptyState` + i18n 错误文案 + 重试按钮
- ✅ P2 已修复a11y 改进,`announcement-card.tsx` / `announcement-detail.tsx` 添加 `aria-label`
- ✅ P1-5 已修复:~~页面层 `Promise.all` 编排 announcements/school/classes 三个模块的 data-access~~ 新增 `getAdminAnnouncementsPageData``getEditAnnouncementPageData` 编排函数,页面层仅调用单一函数
- ✅ P1-6 已修复:~~`targetGradeId` / `targetClassId` 为 optional未根据 `type` 做条件必填校验~~ `CreateAnnouncementSchema``UpdateAnnouncementSchema` 添加 `superRefine(refineAudience)`,年级公告强制 `targetGradeId`,班级公告强制 `targetClassId`
- ✅ P2-11 已修复:~~发布/归档/删除公告无埋点~~ `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction` 新增 `trackEvent` 埋点announcement.created / announcement.updated / announcement.published / announcement.deleted / announcement.archived
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~270 | 6 个 Server Action + 通知触发逻辑P1-2 已修复,无直接 DB 操作) |
| `data-access.ts` | ~190 | 公告 CRUD + 发布/归档 + 受众过滤(含 P1-2 新增 5 个写函数 |
| `schema.ts` | - | Zod 校验 |
| `types.ts` | - | 类型定义(`GetAnnouncementsParams` 新增 `audience` 字段) |
| `actions.ts` | ~330 | 6 个 Server Action + 通知触发逻辑 + trackEvent 埋点P1-2 已修复,无直接 DB 操作) |
| `data-access.ts` | ~230 | 公告 CRUD + 发布/归档 + 受众过滤 + `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData` 编排函数(✅ P1-5 新增) |
| `schema.ts` | ~70 | Zod 校验 + `refineAudience` 条件校验(✅ P1-6 新增 superRefine |
| `types.ts` | ~65 | 类型定义(`GetAnnouncementsParams` 新增 `audience` 字段) |
**组件清单**
| 组件 | 职责 |
|------|------|
| `components/announcement-list.tsx` | 公告列表(用户端,支持状态筛选) |
| `components/announcement-card.tsx` | 公告卡片(列表项) |
| `components/announcement-detail.tsx` | 公告详情(只读) |
| `components/announcement-form.tsx` | 公告表单(创建/编辑,✅ P1-6条件校验由 schema superRefine 保证) |
| `components/admin-announcements-view.tsx` | 管理端公告视图(列表 + 筛选) |
---
@@ -1392,8 +1442,13 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**导出函数**
- Actions`getAiProvidersAction` / `createAiProviderAction` / `updateAiProviderAction` / `deleteAiProviderAction` / `testAiProviderAction`
- Actions-password`changePasswordAction`(✅ P1 已修复:使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod 校验 + DB 操作下沉到 data-access
- Actions-avatar`updateUserAvatarAction` / `removeUserAvatarAction`(✅ P2-8 新增:头像上传/删除,复用 `/api/upload` 路由)
- Actions-notifications`sendTestNotificationAction`(✅ P2-10 新增:发送测试通知,占位实现待接入真实通知服务)
- Actions-system-settings`getAdminSystemSettingsAction` / `saveAdminSystemSettingsAction`(✅ P0-3 新增:管理员系统设置 CRUD4 分类 Zod 校验)
- Actions-security`getSecurityCenterAction` / `toggleTwoFactorAction`(✅ P2-9 新增2FA 状态查询/切换 + 最近登录历史)
- Data-access`getAiProviderSummaries` / `countDefaultAiProviders` / `getAiProviderForUpdate` / `updateAiProvider` / `createAiProvider` / `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`P1 新增,从 actions 下沉)
- Components`SettingsView`统一设置页布局5 标签页 General/Notifications/Appearance/Security/AI角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入Tab URL 持久化;每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏AI 标签页条件渲染需 `AI_CONFIGURE` 权限)、`SettingsServiceProvider` / `useSettingsService`Context 注入 `SettingsService` 接口,解耦组件对 users/messaging actions 的直接依赖)、`SettingsSectionErrorBoundary`(分区 Error Boundary局部失败不影响整页`QuickLinksCard`快捷链接卡片i18n 键驱动)、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`(学生概览异步 Server Component + 骨架屏)、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`(教师概览异步 Server Component + 骨架屏)、`AdminSettingsView`系统设置视图4 个 Card
- Data-access-system-settings`getSystemSettingsByCategory` / `getAllSystemSettings` / `getSystemSetting` / `upsertSystemSetting` / `upsertSystemSettings`(✅ P0-3 新增system_settings 表 CRUD键值对存储模式
- Components`SettingsView`统一设置页布局5 标签页 General/Notifications/Appearance/Security/AI角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入Tab URL 持久化;每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏AI 标签页条件渲染需 `AI_CONFIGURE` 权限)、`SettingsServiceProvider` / `useSettingsService`Context 注入 `SettingsService` 接口,解耦组件对 users/messaging actions 的直接依赖)、`SettingsSectionErrorBoundary`(分区 Error Boundary局部失败不影响整页`QuickLinksCard`快捷链接卡片i18n 键驱动)、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`(学生概览异步 Server Component + 骨架屏)、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`(教师概览异步 Server Component + 骨架屏)、`AdminSettingsView`(✅ P0-3 已修复:从 mock 改为真实数据层,通过 Server Actions 加载/保存到 system_settings 表)、`AvatarUpload`(✅ P2-8 新增:头像上传/预览/删除客户端组件,文件验证 + i18n`SecurityCenterCard`(✅ P2-9 新增2FA 开关 + 最近登录历史卡片)、`ThemePreferencesCard`(✅ P2-11 已增强:集成 `LocaleSwitcher` 语言切换)
- Config`ROLE_SETTINGS_CONFIG` / `resolveRoleSettingsConfig`(配置驱动角色 → 设置视图映射,新增角色只需添加条目)
- Lib`buildStudentOverviewData` / `computeStudentStats` / `sortUpcomingAssignments` / `filterTodaySchedule` / `toWeekday`(纯数据计算函数,与 UI 分离,便于单元测试)
- Types`AiProviderSummary` / `AiProviderName` / `AiProviderExisting` / `SettingsService` / `ProfileService` / `NotificationPreferenceService`(服务接口定义,用于依赖注入解耦)
@@ -1421,27 +1476,39 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ P1 已修复:~~profile/page.tsx 业务逻辑与 UI 混合~~ 抽取 `buildStudentOverviewData` 等纯函数到 `lib/student-overview-data.ts`;拆分 `ProfileStudentOverview` / `ProfileTeacherOverview` 异步组件
- ✅ 密码修改有速率限制
- ✅ AI Provider 操作有 `AI_CONFIGURE` 权限校验
- ✅ P0-3 已修复:~~AdminSettingsView 为 mock 实现,无数据持久化~~ 新增 `system_settings` 表(键值对存储)+ `data-access-system-settings.ts` + `actions-system-settings.ts`AdminSettingsView 改为真实数据层
- ✅ P2-8 已修复:~~无头像上传~~ 新增 `updateUserAvatar` data-access + `actions-avatar.ts` + `AvatarUpload` 组件,复用 `/api/upload` 路由
- ✅ P2-9 已修复:~~无 2FA / 会话管理~~ 新增 `actions-security.ts` + `SecurityCenterCard` 组件2FA 开关占位 + 最近登录历史来自 login_logs 表)
- ✅ P2-10 已修复:~~通知偏好表单无测试通知按钮~~ 新增 `sendTestNotificationAction`,每个已启用渠道旁显示测试按钮
- ✅ P2-11 已修复:~~语言切换未集成到设置页~~ `ThemePreferencesCard` 集成 `LocaleSwitcher` 到 Appearance 标签页
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 160 | AI Provider CRUD + 测试P1 已修复,无直接 DB 操作) |
| `actions-password.ts` | 87 | 修改密码P1 已修复requirePermission + Zod + data-access |
| `actions-avatar.ts` | 56 | 头像上传/删除P2-8 新增requirePermission + revalidatePath |
| `actions-notifications.ts` | 46 | 发送测试通知P2-10 新增:占位实现待接入真实通知服务) |
| `actions-system-settings.ts` | 186 | 管理员系统设置 CRUDP0-3 新增4 分类 Zod 校验 + upsert |
| `actions-security.ts` | 165 | 2FA 状态查询/切换 + 最近登录历史P2-9 新增) |
| `data-access.ts` | 158 | AI Provider CRUD + 密码修改 DB 操作P1 新增) |
| `data-access-system-settings.ts` | 119 | system_settings 表 CRUDP0-3 新增:键值对存储模式) |
| `types.ts` | 60 | 类型定义AiProviderSummary + SettingsService/ProfileService/NotificationPreferenceService 接口) |
| `config/role-settings-config.tsx` | 85 | 角色设置页配置驱动映射ROLE_SETTINGS_CONFIG + resolveRoleSettingsConfig |
| `lib/student-overview-data.ts` | 150 | 学生概览纯数据计算buildStudentOverviewData + computeStudentStats 等,便于单测) |
| `components/settings-view.tsx` | 236 | SettingsView 统一设置页布局5 标签页 + Error Boundary + Suspense + i18n |
| `components/settings-view.tsx` | 236 | SettingsView 统一设置页布局5 标签页 + Error Boundary + Suspense + i18n + SecurityCenterCard 集成 |
| `components/settings-service-context.tsx` | 39 | SettingsServiceProvider + useSettingsServiceContext 注入服务接口) |
| `components/settings-section-error-boundary.tsx` | 64 | 分区 Error Boundary局部失败不影响整页 |
| `components/quick-links-card.tsx` | 42 | 快捷链接卡片i18n 键驱动) |
| `components/profile-student-overview.tsx` | 91 | 学生概览异步 Server Component + 骨架屏 |
| `components/profile-teacher-overview.tsx` | 115 | 教师概览异步 Server Component + 骨架屏 |
| `components/admin-settings-view.tsx` | 195 | AdminSettingsView 系统设置视图4 个 Cardi18n |
| `components/admin-settings-view.tsx` | 425 | AdminSettingsView 系统设置视图(P0-3 已修复:真实数据层 + 4 个 Card + i18n |
| `components/avatar-upload.tsx` | ~150 | 头像上传/预览/删除客户端组件P2-8 新增:文件验证 + i18n |
| `components/security-center-card.tsx` | ~240 | 安全中心卡片P2-9 新增2FA 开关 + 最近登录历史) |
| `components/profile-settings-form.tsx` | 158 | 个人资料表单(通过 SettingsService 注入i18n |
| `components/notification-preferences-form.tsx` | ~140 | 通知偏好表单(通过 SettingsService 注入i18n |
| `components/notification-preferences-form.tsx` | ~160 | 通知偏好表单(通过 SettingsService 注入i18n + P2-10 测试按钮 |
| `components/password-change-form.tsx` | ~130 | 密码修改表单i18n + a11y |
| `components/theme-preferences-card.tsx` | ~60 | 主题偏好卡片i18n |
| `components/theme-preferences-card.tsx` | ~80 | 主题偏好卡片i18n + P2-11 LocaleSwitcher 集成 |
| `components/ai-provider-settings-card.tsx` | ~200 | AI 服务商配置卡片i18n |
---
@@ -1568,10 +1635,16 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
> 架构变更2026-06-22本次审计修复
> - **P0-1 跨模块直查修复**`publish-service.ts` 不再直接 `db.insert(examQuestions)` 和本地实现 `getStudentIdsByClassIds`,改为调用 `exams/data-access.addExamQuestions` 和 `classes/data-access.getStudentIdsByClassIds`,恢复三层架构约束
> - **P0-2 i18n 接入**:新增 `shared/i18n/messages/zh-CN/lesson-preparation.json` 和 `shared/i18n/messages/en/lesson-preparation.json`,注册 `lessonPreparation` 命名空间到 `src/i18n/request.ts`17 个组件改造为 `useTranslations`/`getTranslations`
> - **P0-3 DataScope 过滤修复**`data-access.ts` 的 `buildScopeCondition` 按 scope 类型精确过滤——`class_taught` 增加 `subjectId IN teacher.subjects AND gradeId IN teacher.grades``grade_managed` 限制 `gradeId IN managedGrades``class_members`/`children` 仅允许查看 `published` 课案
> - **P1-1 类型安全修复**`as never` 断言全部替换为类型守卫函数(`isQuestionType`/`isV1Document`/`isV2Document`+ `validTypes` 数组;`block-renderer.tsx` 使用 `as ExerciseBlockData`/`as TextStudyBlockData` 精确断言;`constants.ts` 的 `BLOCK_TYPE_LABELS`/`LESSON_PLAN_STATUS_LABELS` 改为 i18n 键(`BLOCK_TYPE_KEYS`/`LESSON_PLAN_STATUS_KEYS`
> - **P1 纯函数抽取**:新增 `lib/document-migration.ts`migrateV1ToV2/normalizeDocument/buildInitialContent使用类型守卫替代 as 断言)、`lib/node-summary.ts`getNodeSummary + NODE_COLORS + getNodeColor接受翻译函数注入、`lib/rf-mappers.ts`toRfNodes/toRfEdges/fromRfEdgesdata-access.ts 改为从 lib/ 导入并 re-export 保持向后兼容
> - **P1 错误边界 + 骨架屏**:新增 `components/lesson-plan-error-boundary.tsx`LessonPlanErrorBoundary 类组件)和 `components/lesson-plan-skeleton.tsx`VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton
> - **P1 Block 注册表**:新增 `config/block-registry.tsx`BLOCK_REGISTRY 配置表 + getBlockComponent/isRichTextBlock`node-edit-panel.tsx` 重构为配置驱动渲染,移除 if/else 链
> - **P1 window.location.reload 修复**`exercise-block.tsx` 改用 `router.refresh()` 精确刷新缓存
> - **P1-2/P1-3 错误边界 + 骨架屏**:新增 `components/lesson-plan-error-boundary.tsx`LessonPlanErrorBoundary 类组件)和 `components/lesson-plan-skeleton.tsx`VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton
> - **P1-4 阻塞式 UI 修复**`alert()` 全部替换为 `sonner` toast`confirm()` 全部替换为 `AlertDialog`shadcn`window.location.reload` 替换为 `router.refresh()`;涉及 lesson-plan-card/version-history-drawer/inline-question-editor/text-study-block/exercise-block
> - **P1-5/P1-7 多实例 + 角色配置驱动**:新增 `providers/lesson-plan-provider.tsx`LessonPlanProvider + Context + 4 个角色配置 TEACHER/ADMIN/STUDENT/PARENT + LessonPlanDataService 接口 + LessonPlanTracker 埋点接口 + useLessonPlanContextSafe/useRoleConfig 等 hooks和 `services/default-data-service.ts`(包装 Server Actions 为 DataService 实现lesson-plan-list/lesson-plan-card 通过 Context 注入数据服务useRoleConfig 控制按钮可见性
> - **P1-8 Block 注册表**:新增 `config/block-registry.tsx`BLOCK_REGISTRY 配置表 + getBlockComponent/isRichTextBlock`node-edit-panel.tsx` 重构为配置驱动渲染,移除 if/else 链
> - **P1-4 window.location.reload 修复**`exercise-block.tsx` 改用 `router.refresh()` 精确刷新缓存
> - **P2-1 a11y 修复**5 个组件question-bank-picker/publish-homework-dialog/knowledge-point-picker/exercise-block/text-study-block添加 `role="dialog"`/`aria-modal`/`aria-label`inline-question-editor 添加 `role="dialog"`/`aria-modal`/`aria-label`
> - **P2-4 监控埋点预留**`providers/lesson-plan-provider.tsx` 定义 `LessonPlanTracker` 接口 + `noopTracker` 默认空实现,生产环境可替换为真实埋点
**文件清单**
| 文件 | 职责 |
@@ -1583,7 +1656,9 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `lib/node-summary.ts` | **纯函数**getNodeSummary接受翻译函数注入支持 i18n+ NODE_COLORS + getNodeColor |
| `lib/rf-mappers.ts` | **纯函数**toRfNodes/toRfEdges/fromRfEdgesLessonPlanNode/Edge ↔ React Flow Node/Edge 映射) |
| `config/block-registry.tsx` | **配置驱动**BLOCK_REGISTRY 注册表 + getBlockComponent/isRichTextBlocknode-edit-panel 通过配置渲染 Block |
| `data-access.ts` | 课案 CRUD + 模板查询migrateV1ToV2/normalizeDocument/buildInitialContent 从 lib/ 导入并 re-export 保持向后兼容) |
| `providers/lesson-plan-provider.tsx` | **Provider + ContextP1-5/P1-7/P2-4**LessonPlanProvider 注入数据服务/角色配置/埋点;定义 LessonPlanDataService 接口、4 个角色配置TEACHER/ADMIN/STUDENT/PARENT、ROLE_CONFIGS 注册表、LessonPlanTracker 接口 + noopTrackerhooksuseLessonPlanContextSafe返回 null 不抛错)/useLessonPlanContext/useRoleConfig/useLessonPlanService/useLessonPlanTracker |
| `services/default-data-service.ts` | **默认数据服务实现**createDefaultDataService() 包装 Server Actions 为 LessonPlanDataService 实现,测试可替换为 mock |
| `data-access.ts` | 课案 CRUD + 模板查询migrateV1ToV2/normalizeDocument/buildInitialContent 从 lib/ 导入并 re-export 保持向后兼容buildScopeCondition 按 scope 类型精确过滤 P0-3 |
| `data-access-versions.ts` | 版本管理(创建/查询/回滚/清理) |
| `data-access-templates.ts` | 个人模板 CRUD |
| `data-access-knowledge.ts` | 按知识点/题目反查课案 |

View File

@@ -6055,6 +6055,11 @@
"name": "RecentSubmissions",
"file": "teacher-dashboard/RecentSubmissions",
"purpose": "最近提交"
},
{
"name": "DashboardGreetingHeader",
"file": "dashboard-greeting-header",
"purpose": "共享问候头部组件V2 抽象,消除 teacher/student 头部 90% 重复代码,接收 userName 和可选 actions slot"
}
]
}
@@ -8329,6 +8334,80 @@
]
}
],
"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": "计算分数平均值(空数组返回 0P1-1 新增:从 data-access.getStudentGradeSummary 抽取为纯函数)",
"usedBy": [
"data-access.getStudentGradeSummary"
]
},
{
"name": "buildGradeTrendPoints",
"signature": "(rows: RawScoreRow[]) => GradeTrendPoint[]",
"file": "stats-service.ts",
"purpose": "构建成绩趋势数据点(按考试标题分组,归一化分数 0-100P1-1 新增:从 data-access-analytics.getGradeTrend 抽取为纯函数)",
"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<ClassComparisonItem, 'averageScore' | 'passRate' | 'excellentRate' | 'studentCount'>",
"file": "stats-service.ts",
"purpose": "计算班级对比统计均分、及格率、优秀率、参考人数P1-1 新增:从 data-access-analytics.getClassComparison 抽取为纯函数)",
"usedBy": [
"data-access-analytics.getClassComparison"
]
},
{
"name": "computeSubjectComparisonStats",
"signature": "(scores: number[]) => Pick<SubjectComparisonItem, 'averageScore' | 'passRate' | 'excellentRate' | 'maxScore' | 'minScore'>",
"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<string, RankingTrendEntry>, targetStudentId: string) => RankingTrendPoint[]",
"file": "stats-service.ts",
"purpose": "构建排名趋势数据点按考试标题排序、计算每次考试学生排名P1-1 新增:从 data-access-ranking.getRankingTrend 抽取为纯函数)",
"usedBy": [
"data-access-ranking.getRankingTrend"
]
}
],
"components": [
{
"name": "GradeRecordForm",
@@ -8433,6 +8512,15 @@
"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-labelP1-5 新增)",
"deps": [
"shared/components/ui/skeleton",
"shared/components/ui/button"
]
}
]
}
@@ -9109,19 +9197,20 @@
},
"messaging": {
"path": "src/modules/messaging",
"description": "站内消息系统:用户间私信收发(支持回复链)、站内通知多态类型message/announcement/homework/gradeSiteHeader 通知下拉菜单展示未读数",
"description": "站内私信系统:用户间私信收发(支持回复链)。通知相关 UI 组件和 CRUD Action 已迁移至 notifications 模块P1-4 修复)",
"exports": {
"actions": [
{
"name": "sendMessageAction",
"permission": "MESSAGE_SEND",
"signature": "(prevState: ActionState<string> | null, formData: FormData) => Promise<ActionState<string>>",
"purpose": "发送消息(同时通过 notifications dispatcher 为收件人创建多渠道通知;支持 parentMessageId 回复)",
"purpose": "发送消息(同时通过 notifications dispatcher 为收件人创建多渠道通知;支持 parentMessageId 回复P2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"shared/db",
"data-access.createMessage",
"notifications.dispatcher.sendNotification",
"trackEvent",
"revalidatePath"
],
"usedBy": [
@@ -9132,11 +9221,12 @@
"name": "markMessageAsReadAction",
"permission": "MESSAGE_READ",
"signature": "(id: string) => Promise<ActionState<void>>",
"purpose": "标记消息已读(设置 readAt",
"purpose": "标记消息已读(设置 readAtP2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"schema.MessageIdSchema",
"data-access.markMessageAsRead",
"trackEvent",
"revalidatePath"
],
"usedBy": [
@@ -9148,11 +9238,12 @@
"name": "deleteMessageAction",
"permission": "MESSAGE_DELETE",
"signature": "(id: string) => Promise<ActionState<void>>",
"purpose": "删除消息(仅发送者或接收者可删)",
"purpose": "删除消息(仅发送者或接收者可删P2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"schema.MessageIdSchema",
"data-access.deleteMessage",
"trackEvent",
"revalidatePath"
],
"usedBy": [
@@ -9162,14 +9253,14 @@
{
"name": "getMessagesAction",
"permission": "MESSAGE_READ",
"signature": "(params?: { type?, page?, pageSize? }) => Promise<ActionState<PaginatedResult<MessageListItem>>>",
"purpose": "获取消息列表(收件箱/已发送,分页)",
"signature": "(params?: { type?, page?, pageSize?, keyword? }) => Promise<ActionState<PaginatedResult<MessageListItem>>>",
"purpose": "获取消息列表(收件箱/已发送,分页,关键词搜索;客户端通过 useMessageSearch hook 调用",
"deps": [
"requirePermission",
"data-access.getMessages"
],
"usedBy": [
"message-list.tsx"
"message-list.tsx (via useMessageSearch hook)"
]
},
{
@@ -9201,47 +9292,16 @@
]
},
{
"name": "getNotificationsAction",
"name": "getUnreadMessageCountAction",
"permission": "MESSAGE_READ",
"signature": "(params?: { page?, pageSize? }) => Promise<ActionState<PaginatedResult<NotificationListItem>>>",
"purpose": "获取当前用户通知列表(分页",
"signature": "() => Promise<ActionState<number>>",
"purpose": "获取当前用户未读私信计数unread-message-badge 组件每 60 秒轮询",
"deps": [
"requirePermission",
"data-access.getNotifications"
"data-access.getUnreadMessageCount"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
},
{
"name": "markNotificationAsReadAction",
"permission": "MESSAGE_READ",
"signature": "(id: string) => Promise<ActionState<void>>",
"purpose": "标记单条通知已读",
"deps": [
"requirePermission",
"data-access.markNotificationAsRead",
"revalidatePath"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
},
{
"name": "markAllNotificationsAsReadAction",
"permission": "MESSAGE_READ",
"signature": "() => Promise<ActionState<void>>",
"purpose": "标记所有通知已读",
"deps": [
"requirePermission",
"data-access.markAllNotificationsAsRead",
"revalidatePath"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
"unread-message-badge.tsx"
]
},
{
@@ -9251,7 +9311,7 @@
"purpose": "获取当前用户的通知偏好设置(首次访问自动创建默认记录)",
"deps": [
"requirePermission",
"notification-preferences.getNotificationPreferences"
"notifications.preferences.getNotificationPreferences"
],
"usedBy": [
"settings/page.tsx",
@@ -9266,7 +9326,7 @@
"deps": [
"requirePermission",
"schema.UpdateNotificationPreferencesSchema",
"notification-preferences.upsertNotificationPreferences",
"notifications.preferences.upsertNotificationPreferences",
"revalidatePath"
],
"usedBy": [
@@ -9362,70 +9422,23 @@
"shared.db.schema.messages"
],
"usedBy": [
"待扩展"
"getUnreadMessageCountAction",
"unread-message-badge.tsx"
]
},
{
"name": "getNotifications",
"signature": "(userId: string, params?: { page?, pageSize? }) => Promise<PaginatedResult<NotificationListItem>>",
"name": "getMessagesPageData",
"signature": "(userId: string) => Promise<{ messages: PaginatedResult<Message>, notifications: PaginatedResult<Notification> }>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移",
"purpose": "P1-5 新增:消息首页编排函数,一次性获取消息列表和通知列表(通知通过动态 import notifications/data-access",
"deps": [
"data-access.getMessages",
"notifications.data-access.getNotifications"
],
"usedBy": [
"getNotificationsAction",
"messages/page.tsx"
]
},
{
"name": "createNotification",
"signature": "(input: CreateNotificationInput) => Promise<string>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移)",
"deps": [
"notifications.data-access.createNotification"
],
"usedBy": [
"notifications.dispatcher (via in-app-channel)"
]
},
{
"name": "markNotificationAsRead",
"signature": "(id: string, userId: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移)",
"deps": [
"notifications.data-access.markNotificationAsRead"
],
"usedBy": [
"markNotificationAsReadAction"
]
},
{
"name": "markAllNotificationsAsRead",
"signature": "(userId: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移)",
"deps": [
"notifications.data-access.markAllNotificationsAsRead"
],
"usedBy": [
"markAllNotificationsAsReadAction"
]
},
{
"name": "getUnreadNotificationCount",
"signature": "(userId: string) => Promise<number>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移)",
"deps": [
"notifications.data-access.getUnreadNotificationCount"
],
"usedBy": [
"待扩展"
]
},
{
"name": "getRecipients",
"signature": "(ctx: AuthContext) => Promise<RecipientOption[]>",
@@ -9629,21 +9642,26 @@
"purpose": "写消息表单(收件人 Select、主题 Input、内容 Textarea支持回复模式"
},
{
"name": "NotificationDropdown",
"file": "components/notification-dropdown.tsx",
"purpose": "SiteHeader 通知下拉菜单Bell 图标 + 未读数 Badge滚动列表标记已读查看全部链接"
},
"name": "UnreadMessageBadge",
"file": "components/unread-message-badge.tsx",
"purpose": "未读消息计数徽章(侧边栏 Messages 导航项旁显示,每 60 秒轮询 getUnreadMessageCountAction"
}
],
"hooks": [
{
"name": "NotificationList",
"file": "components/notification-list.tsx",
"purpose": "通知完整列表(全部标记已读、单条标记已读、查看链接)"
"name": "useMessageSearch",
"file": "hooks/use-message-search.ts",
"purpose": "P1-7 新增:消息搜索 hook400ms 防抖 + 请求竞态取消,通过 requestIdRef 匹配最新请求)",
"usedBy": [
"message-list.tsx"
]
}
]
}
},
"notifications": {
"path": "src/modules/notifications",
"description": "通知渠道集成层基于用户通知偏好notification_preferences将通知分发到站内消息/SMS/微信公众号/邮件多渠道。所有渠道实现统一 NotificationChannelSender 接口dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务)。",
"description": "通知渠道集成层基于用户通知偏好notification_preferences将通知分发到站内消息/SMS/微信公众号/邮件多渠道。所有渠道实现统一 NotificationChannelSender 接口dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务)。P1-4 修复后新增通知 UI 组件和通知 CRUD Server Action。",
"exports": {
"actions": [
{
@@ -9673,6 +9691,66 @@
"usedBy": [
"待扩展"
]
},
{
"name": "getNotificationsAction",
"permission": "MESSAGE_READ",
"signature": "(params?: { page?, pageSize?, unreadOnly? }) => Promise<ActionState<PaginatedResult<Notification>>>",
"purpose": "P1-4 新增(从 messaging 迁移):获取当前用户通知列表(分页)",
"deps": [
"requirePermission",
"data-access.getNotifications"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
},
{
"name": "getUnreadNotificationCountAction",
"permission": "MESSAGE_READ",
"signature": "() => Promise<ActionState<number>>",
"purpose": "P1-4 新增(从 messaging 迁移):获取当前用户未读通知计数",
"deps": [
"requirePermission",
"data-access.getUnreadNotificationCount"
],
"usedBy": [
"notification-dropdown.tsx"
]
},
{
"name": "markNotificationAsReadAction",
"permission": "MESSAGE_READ",
"signature": "(notificationId: string) => Promise<ActionState<string>>",
"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<ActionState<string>>",
"purpose": "P1-4 新增(从 messaging 迁移标记所有通知已读P2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"data-access.markAllNotificationsAsRead",
"trackEvent",
"revalidatePath"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
}
],
"dispatcher": [
@@ -10029,6 +10107,18 @@
"messaging (via re-export)"
]
}
],
"components": [
{
"name": "NotificationList",
"file": "components/notification-list.tsx",
"purpose": "P1-4 新增(从 messaging 迁移):通知完整列表(全部标记已读、单条标记已读、查看链接)"
},
{
"name": "NotificationDropdown",
"file": "components/notification-dropdown.tsx",
"purpose": "P1-4 新增(从 messaging 迁移SiteHeader 通知下拉菜单Bell 图标 + 未读数 Badge每 30 秒轮询,滚动列表,标记已读,查看全部链接)"
}
]
}
},
@@ -12517,6 +12607,8 @@
"lib/node-summary.ts",
"lib/rf-mappers.ts",
"config/block-registry.tsx",
"providers/lesson-plan-provider.tsx",
"services/default-data-service.ts",
"data-access.ts",
"data-access-versions.ts",
"data-access-templates.ts",
@@ -12549,7 +12641,29 @@
"components/blocks/text-study-block.tsx",
"components/blocks/exercise-block.tsx",
"components/blocks/reflection-block.tsx"
]
],
"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 默认实现"
}
}
},
"dbTables": {

View File

@@ -0,0 +1,121 @@
# 仪表盘模块审计报告 v2
> 审查日期2026-06-22第二轮
> 审查范围:基于 v1 重构后代码commit `868ac5f` + `21c1e7a`)的再次分析
> 前置报告:`docs/architecture/audit/dashboard-audit-report.md`v1
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.12、`docs/architecture/005_architecture_data.json`
---
## 一、v1 重构成果回顾
v1 报告识别的 P0/P1/P2 项目已完成的部分:
| # | 项目 | 状态 | 证据 |
|---|------|------|------|
| P0-1 | 权限校验 | ✅ 已完成 | [actions.ts](file:///e:/Desktop/CICD/src/modules/dashboard/actions.ts) 4 个 Server Action 均调用 `requirePermission()` |
| P0-2 | 根重定向角色硬编码 | ✅ 已完成 | [dashboard/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/dashboard/page.tsx) 改用 `resolvePermissions()` |
| P0-3 | i18n 零覆盖 | ⚠️ 部分完成 | 仅容器组件接入 i18n**10 个子组件仍英文硬编码** |
| P0-4 | 页面层越权编排 | ✅ 已完成 | teacher/student/parent 编排下沉至 actions.ts |
| P1-1 | 业务逻辑耦合 UI | ✅ 已完成 | [lib/dashboard-utils.ts](file:///e:/Desktop/CICD/src/modules/dashboard/lib/dashboard-utils.ts) 抽取 6 个纯函数 |
| P1-3 | 仅路由级错误边界 | ✅ 已完成 | [dashboard-section.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/dashboard-section.tsx) 分区 Error Boundary + Suspense |
| P2-2 | a11y 不足 | ❌ 未完成 | 仍缺语义化标签、表格 caption |
---
## 二、v2 新发现问题
### 2.1 i18n 覆盖严重不完整P0 — v1 遗漏)
v1 仅对容器组件(`admin-dashboard.tsx``teacher-dashboard-view.tsx``teacher-dashboard-header.tsx``teacher-stats.tsx``teacher-todo-card.tsx``student-stats-grid.tsx``student-dashboard-header.tsx``parent-dashboard.tsx``user-growth-chart.tsx`)接入 i18n**10 个子组件仍全英文硬编码**
| # | 文件 | 硬编码示例 | 违反规则 |
|---|------|-----------|----------|
| 1 | [teacher-quick-actions.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/teacher-dashboard/teacher-quick-actions.tsx) L12-24 | `"Create Assignment"` / `"Grade"` / `"My Classes"` | "所有用户可见文本必须适配 i18n" |
| 2 | [teacher-classes-card.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/teacher-dashboard/teacher-classes-card.tsx) L15-27 | `"My Classes"` / `"View all"` / `"No classes yet"` / `"Create a class to start managing students and schedules."` / `"Create class"` / `"Homeroom"` / `"Room"` | 同上 |
| 3 | [teacher-homework-card.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/teacher-dashboard/teacher-homework-card.tsx) L17-87 | `"Homework"` / `"Create new assignment"` / `"No assignments"` / `"Create an assignment to get started."` / `"Create"` / `"No due date"` / `"View all assignments"` | 同上 |
| 4 | [teacher-schedule.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/teacher-dashboard/teacher-schedule.tsx) L41-141 | `"Today's Schedule"` / `"No Classes Today"` / `"No timetable entries."` / `"View schedule"` / `"LIVE"` / `"Scroll for more"` / `"No more classes today"` | 同上 |
| 5 | [recent-submissions.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/teacher-dashboard/recent-submissions.tsx) L22-105 | `"Recent Submissions"` / `"No New Submissions"` / `"All caught up!..."` / `"View All"` / `"View submissions"` / `"Student"` / `"Assignment"` / `"Submitted"` / `"Action"` / `"Late"` / `"Grade"` | 同上 |
| 6 | [teacher-grade-trends.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/teacher-dashboard/teacher-grade-trends.tsx) L25-69 | `"Class Performance"` / `"Average scores for the last X assignments"` / `"No data available"` / `"Publish assignments to see class performance trends."` / `"Average Score (%)"` / `"X/Y submitted"` | 同上 |
| 7 | [student-grades-card.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/student-dashboard/student-grades-card.tsx) L30-101 | `"Recent Grades"` / `"No graded work yet"` / `"Finish and submit assignments to see your score trend."` / `"View all"` / `"Score (%)"` / `"Latest:"` / `"Points:"` / `"Assignment"` / `"Score"` / `"When"` | 同上 |
| 8 | [student-today-schedule-card.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/student-dashboard/student-today-schedule-card.tsx) L52-83 | `"Today's Schedule"` / `"View all"` / `"No classes today"` / `"Your timetable is clear for today."` / `"In Progress"` / `"Up Next"` | 同上 |
| 9 | [student-upcoming-assignments-card.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/student-dashboard/student-upcoming-assignments-card.tsx) L17-22,49-72 | `"Review"` / `"View"` / `"Continue"` / `"Start"` / `"Upcoming Assignments"` / `"View all"` / `"No assignments"` / `"You have no assigned homework right now."` / `"Title"` / `"Status"` / `"Due"` / `"Score"` / `"Action"` / `"Late"` | 同上 |
| 10 | [admin-dashboard.tsx](file:///e:/Desktop/CICD/src/modules/dashboard/components/admin-dashboard/admin-dashboard.tsx) L212 | `{u.role ?? "unknown"}` 硬编码 `"unknown"` | 同上 |
**后果**:中文用户看到大量英文,体验割裂;无法切换语言;维护时需逐文件改字符串。
### 2.2 四角色仍零共享抽象P1 — v1 未处理)
| 维度 | 现状 | 期望 |
|------|------|------|
| 问候语头部 | `TeacherDashboardHeader``StudentDashboardHeader` 代码 90% 重复(仅 props 名不同) | 抽象为 `DashboardGreetingHeader` |
| 快捷操作 | admin 的 `QuickActionCard`内联、parent 的 `QUICK_ENTRIES`内联、teacher 的 `TeacherQuickActions` — 三套独立实现 | 抽象为 `DashboardQuickActions` |
| 仪表盘布局容器 | admin/teacher/student 各写一套 `<div className="space-y-*">` | 抽象为 `DashboardLayout` |
**违反规则**"最大化复用:识别四个角色共用的 UI 块和业务逻辑块,抽象为泛型组件和 hooks"。
### 2.3 无单测P2 — v1 未处理)
`lib/dashboard-utils.ts` 抽取了 6 个纯函数但**无任何单测**
| 函数 | 测试覆盖 | 风险 |
|------|----------|------|
| `toWeekday` | ❌ 无 | 周日映射错误未被发现 |
| `countStudentAssignments` | ❌ 无 | 边界条件(无截止日期/已批改)未验证 |
| `sortUpcomingAssignments` | ❌ 无 | 排序稳定性未验证 |
| `filterTodaySchedule` | ❌ 无 | 空课表/排序未验证 |
| `computeTeacherMetrics` | ❌ 无 | 提交率分母为零等边界未验证 |
| `getGreetingKey` | ❌ 无 | 时段边界12:00/18:00未验证 |
**违反规则**"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock" + "可测试性"。
### 2.4 a11y 不足P2 — v1 未处理)
| 位置 | 问题 | 违反规则 |
|------|------|----------|
| `admin-dashboard.tsx` 表格 | 无 `<caption>` | "语义化标签、ARIA 属性、键盘导航" |
| `recent-submissions.tsx` 表格 | 无 `<caption>` | 同上 |
| `student-upcoming-assignments-card.tsx` 表格 | 无 `<caption>` | 同上 |
| `teacher-dashboard-view.tsx` 布局 | 无 `<section>` / `<aside>` 语义化标签 | 同上 |
| `student-dashboard-view.tsx` 布局 | 同上 | 同上 |
| `teacher-schedule.tsx` 时间线 | 无 `aria-label` 描述当前/过去/未来状态 | 同上 |
### 2.5 流式渲染未实现P1 — v1 未处理)
所有 `page.tsx``export const dynamic = "force-dynamic"` + `Promise.all` 等全部数据就绪后才渲染。虽然 `DashboardSection` 内部有 Suspense但 page 层已无 Suspense 边界,无法流式渲染首屏。
---
## 三、改进优先级v2
### P0紧急 — v1 遗漏的 i18n
| # | 问题 | 改进方向 |
|---|------|----------|
| v2-P0-1 | 10 个组件英文硬编码 | 全部接入 `useTranslations` / `getTranslations`;补充翻译键 |
### P1较严重 — 共享抽象 + 单测)
| # | 问题 | 改进方向 |
|---|------|----------|
| v2-P1-1 | 问候语头部重复 | 抽象 `DashboardGreetingHeader` 组件 |
| v2-P1-2 | 纯函数无单测 | 为 `lib/dashboard-utils.ts` 6 个函数添加单测 |
### P2优化 — a11y + 流式)
| # | 问题 | 改进方向 |
|---|------|----------|
| v2-P2-1 | 表格无 caption / 布局无语义化标签 | 补充 `<caption>` / `<section>` / `aria-label` |
---
## 四、架构图同步说明
v2 修改完成后需同步更新:
### 4.1 `004_architecture_impact_map.md`
- §2.12 dashboard 章节:补充新增共享组件(`DashboardGreetingHeader`)、单测文件(`lib/dashboard-utils.test.ts`
### 4.2 `005_architecture_data.json`
- `modules.dashboard.exports.components`:新增 `DashboardGreetingHeader`
- `modules.dashboard.exports.lib`:补充单测覆盖说明