feat(announcements,messaging,notifications): 实现所有长期问题 — SSE 实时推送 + 通知日志持久化 + 优先级/归档 + 消息星标/草稿 + 公告已读回执/置顶 + 分类筛选/桌面推送 + 测试覆盖

P1-8 通知实时推送(SSE):
- 新增 /api/notifications/stream SSE 端点(15 秒推送,5 分钟超时)
- 新增 useNotificationStream Hook(SSE + 轮询降级)
- NotificationDropdown 改用 SSE 实时推送

P2-12 测试覆盖:
- notifications/dispatcher.test.ts(6 个测试,渠道选择逻辑)
- notifications/channels/in-app-channel.test.ts(9 个测试,类型映射)
- messaging/schema.test.ts(34 个测试,Zod 校验)
- tests/e2e/messages.spec.ts(消息模块 E2E 测试)
- vitest.unit.config.ts 添加 server-only stub

P2-13a 通知发送日志持久化:
- 新增 notification_logs 表(userId/title/channel/status/messageId/error/sentAt)
- logNotificationSend 改为 async 写入 DB(失败降级 console)
- dispatcher 传递 payload 用于持久化

P2-13b 通知优先级和归档:
- messageNotifications 表新增 priority(low/normal/high/urgent)和 isArchived 字段
- getNotifications 支持归档和优先级筛选
- 新增 archiveNotificationAction
- NotificationList 显示优先级 Badge 和归档按钮

P2-13c 消息星标和草稿:
- messages 表新增 isStarred 字段
- 新增 message_drafts 表
- 新增 toggleMessageStar + 草稿 CRUD Server Actions
- 新增 5 个草稿 data-access 函数

P2-13d 公告已读回执和置顶:
- announcements 表新增 isPinned 字段
- 新增 announcement_reads 表(唯一索引保证幂等)
- 新增 toggleAnnouncementPinAction + markAnnouncementAsReadAction
- getAnnouncements 排序置顶优先

P2-13e 通知分类筛选和桌面推送:
- NotificationList 添加按类型筛选按钮组
- 新增 useDesktopNotifications Hook(浏览器 Notification API)
- NotificationDropdown 集成桌面推送(新通知触发)

架构图同步:
- 004 和 005 均已更新(新增表、Action、Hook、组件描述)
This commit is contained in:
SpecialX
2026-06-23 10:13:57 +08:00
parent 696346dc08
commit f75602d14e
39 changed files with 2557 additions and 110 deletions

View File

@@ -1096,11 +1096,11 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
## 2.13 messaging私信模块
**职责**站内私信messages 表 CRUD
**职责**站内私信messages 表 CRUD+ 消息草稿message_drafts 表 CRUD
**导出函数**
- 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 新增:消息首页编排函数,一次性获取消息列表和通知列表)/ `getMessageDetailPageData`(✅ V2-P1-3 新增:消息详情页编排函数,获取详情并自动标记已读)
- Actions`sendMessageAction` / `markMessageAsReadAction` / `deleteMessageAction` / `getMessagesAction` / `getMessageDetailAction` / `getRecipientsAction` / `getUnreadMessageCountAction` / `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`(✅ P1-4 已修复:通知 CRUD Action 已迁移至 notifications 模块messaging 仅保留私信和通知偏好 Action;✅ V2-P2-13c 新增:`toggleMessageStarAction` 星标切换 / `getMessageDraftsAction` 草稿列表 / `saveMessageDraftAction` 草稿保存(创建或更新)/ `deleteMessageDraftAction` 草稿删除
- Data-access`getMessages`(✅ V2-P2-13c 新增 starredOnly 星标筛选)/ `getMessageById` / `getMessageThread` / `createMessage` / `markMessageAsRead` / `deleteMessage` / `toggleMessageStar`(✅ V2-P2-13c 新增:接收方星标切换)/ `getUnreadMessageCount` / `getRecipients`(按 DataScope 过滤可发送对象class_taught 教师→学生、grade_managed 年级管理员→教师/学生、all 管理员、class_members 学生→自己班级的任课教师/班主任、children 家长→孩子的班主任/任课教师;通过 classes data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID/ `getMessagesPageData`(✅ P1-5 新增:消息首页编排函数,一次性获取消息列表和通知列表)/ `getMessageDetailPageData`(✅ V2-P1-3 新增:消息详情页编排函数,获取详情并自动标记已读)/ `getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById`(✅ V2-P2-13c 新增:消息草稿 CRUD操作 message_drafts 表)
- Hooks`useMessageSearch`(✅ P1-7 新增:消息搜索 hook含防抖和请求竞态取消
- Notification-preferences~~re-export shim实际逻辑在 `notifications/preferences.ts`~~ ✅ P0-b 已修复:`notification-preferences.ts` 文件已删除(通知模块去重),消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`
@@ -1130,6 +1130,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ V2-P1-3 已修复:~~消息详情页分散编排~~ 新增 `getMessageDetailPageData` 编排函数,替代 page.tsx 中 `after()` + `getMessageById` + `markMessageAsRead` 的分散编排
- ✅ V2-P1-4 已修复:~~表单无服务端校验错误展示~~ `message-compose.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示receiverId/subject/content
- ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `unread-message-badge.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量(~~60_000ms~~ → 30_000ms✅ V2-P3 与通知组件保持一致)
- ✅ V2-P2-13c 已实现:消息星标和草稿功能。`messages` 表新增 `isStarred` 字段(接收方可标记重要消息)+ `messages_receiver_starred_idx` 复合索引;新增 `message_drafts`userId/receiverId/subject/content/parentMessageId + updatedAt/createdAt`message_drafts_user_idx``message_drafts_user_updated_idx` 索引data-access 新增 `toggleMessageStar` / `getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById`actions 新增 `toggleMessageStarAction` / `getMessageDraftsAction` / `saveMessageDraftAction` / `deleteMessageDraftAction``getMessages` 支持 `starredOnly` 筛选i18n 新增 actions.star/unstar/saveDraft/deleteDraft + messages.draftSaved/draftDeleted/starToggled + empty.noDrafts/noStarred 翻译键;迁移文件 `drizzle/0008_message_star_draft.sql`
**文件清单**
| 文件 | 行数 | 职责 |
@@ -1164,8 +1165,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- Data-access`createNotification` / `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount` / `archiveNotification` / `unarchiveNotification` / `getUserContactInfo` / `logNotificationSend` / `logNotificationSendBatch`(✅ P0-4 / P1-5 修复后从 messaging 迁移;✅ V2-P2-13b 新增archiveNotification / unarchiveNotification 归档函数)
- Preferences`getNotificationPreferences` / `upsertNotificationPreferences`(✅ P0-4 / P1-5 修复后从 messaging 迁移)
- Channels`InAppChannelSender` / `SmsChannelSender` / `EmailChannelSender` / `WeChatChannelSender`
- Components`NotificationList` / `NotificationDropdown`(✅ P1-4 新增:从 messaging/components 迁移;✅ V2-P2-13bNotificationList 支持优先级 Badge 显示和归档操作)
- Hooks`useNotificationStream`(✅ V2-P3 新增SSE 实时推送 + 轮询降级 Hook
- Components`NotificationList` / `NotificationDropdown`(✅ P1-4 新增:从 messaging/components 迁移;✅ V2-P2-13bNotificationList 支持优先级 Badge 显示和归档操作;✅ V2-P2-13cNotificationList 支持按类型筛选
- Hooks`useNotificationStream`(✅ V2-P3 新增SSE 实时推送 + 轮询降级 Hook`useDesktopNotifications`(✅ V2-P2-13c 新增:浏览器桌面推送 Hook支持权限管理和新通知桌面提醒
- Types`NotificationPriority`(✅ V2-P2-13b 新增:通知优先级类型 low/normal/high/urgent
**依赖关系**
@@ -1182,6 +1183,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `notification-dropdown.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量30_000ms
- ✅ V2-P3 已优化:~~30 秒轮询~~ `notification-dropdown.tsx` 改为 SSE 实时推送(`/api/notifications/stream`SSE 不可用时自动降级为轮询(调用 Server Actions间隔 30 秒)
- ✅ V2-P2-13b 新增通知优先级和归档功能。schema.ts `messageNotifications` 表新增 `priority`low/normal/high/urgent默认 normal`isArchived`(默认 false字段 + 2 个索引types.ts 新增 `NotificationPriority` 类型data-access.ts 新增 `archiveNotification` / `unarchiveNotification` 函数,`getNotifications` 支持归档和优先级筛选(默认仅返回未归档),`createNotification` 支持 priorityactions.ts 新增 `archiveNotificationAction`(含 trackEvent 埋点 notification.archivedNotificationList 组件支持优先级 Badge 显示和归档按钮i18n 新增 priority/actions.archive/messages.archiveFailed 翻译键
- ✅ V2-P2-13c 新增:通知分类筛选 + 浏览器桌面推送。NotificationList 组件新增按类型筛选按钮组all/message/announcement/homework/grade空状态区分"无通知"和"无筛选结果";新增 `useDesktopNotifications` Hook浏览器 Notification API支持权限管理、自动请求权限、新通知桌面推送、点击跳转NotificationDropdown 集成桌面推送监听新通知并触发桌面提醒首次加载不批量推送i18n 新增 filter.all / empty.noFilterResults / empty.noFilterResultsDesc 翻译键
- ⚠️ P1发送日志仅 console`notification_logs`
- ✅ 渠道抽象优秀(接口 + 工厂 + Mock 实现)
@@ -1193,21 +1195,23 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `preferences.ts` | 166 | 通知偏好 CRUDP0-4 / P1-5 修复后从 messaging 迁移) |
| `actions.ts` | ~300 | 7 个 Server Action✅ P1-4新增 4 个通知 CRUD Action✅ V2-P2-13b新增 archiveNotificationAction |
| `types.ts` | ~130 | 通知负载 + 渠道配置 + 通知记录 + 偏好类型P0-4 / P1-5 修复后扩充;✅ V2-P2-13b新增 NotificationPriority 类型 + priority/isArchived 字段) |
| `index.ts` | ~80 | 对外导出入口(✅ P1-4新增组件和 CRUD Action 导出;✅ V2-P2-13b新增归档函数/Action/类型导出) |
| `index.ts` | ~80 | 对外导出入口(✅ P1-4新增组件和 CRUD Action 导出;✅ V2-P2-13b新增归档函数/Action/类型导出;✅ V2-P2-13c新增 useNotificationStream / useDesktopNotifications Hook 导出 |
| `channels/*` | 5 文件 | 4 个渠道实现 |
| `components/notification-list.tsx` | ~170 | ✅ P1-4 新增(从 messaging 迁移):通知列表组件;✅ V2-P2-13b支持优先级 Badge 显示和归档操作 |
| `components/notification-dropdown.tsx` | ~150 | ✅ P1-4 新增(从 messaging 迁移):通知下拉菜单组件;✅ V2-P3改用 SSE 实时推送 + 轮询降级 |
| `components/notification-list.tsx` | ~190 | ✅ P1-4 新增(从 messaging 迁移):通知列表组件;✅ V2-P2-13b支持优先级 Badge 显示和归档操作;✅ V2-P2-13c支持按类型筛选 + 空状态区分 |
| `components/notification-dropdown.tsx` | ~180 | ✅ P1-4 新增(从 messaging 迁移):通知下拉菜单组件;✅ V2-P3改用 SSE 实时推送 + 轮询降级;✅ V2-P2-13c集成 useDesktopNotifications 桌面推送 |
| `hooks/use-notification-stream.ts` | ~195 | ✅ V2-P3 新增SSE 实时推送 HookEventSource + 轮询降级) |
| `hooks/use-desktop-notifications.ts` | ~100 | ✅ V2-P2-13c 新增:浏览器桌面推送 HookNotification API + 权限管理 + 点击跳转) |
**组件清单**
| 组件 | 职责 |
|------|------|
| `components/notification-list.tsx` | 通知列表(消息页底部,展示所有通知,支持标记已读;✅ V2-P0-1useTranslations 命名空间从 "messages" 切换到 "notifications";✅ V2-P2-13b支持优先级 Badge 显示和归档操作) |
| `components/notification-dropdown.tsx` | 通知下拉菜单(站点头部,✅ V2-P3改用 SSE 实时推送 `/api/notifications/stream` + 轮询降级;✅ V2-P0-1useTranslations 命名空间切换;✅ V2-P2-1POLL_INTERVAL_MS 常量) |
| `components/notification-list.tsx` | 通知列表(消息页底部,展示所有通知,支持标记已读;✅ V2-P0-1useTranslations 命名空间从 "messages" 切换到 "notifications";✅ V2-P2-13b支持优先级 Badge 显示和归档操作;✅ V2-P2-13c支持按类型筛选 + 空状态区分"无通知"/"无筛选结果" |
| `components/notification-dropdown.tsx` | 通知下拉菜单(站点头部,✅ V2-P3改用 SSE 实时推送 `/api/notifications/stream` + 轮询降级;✅ V2-P0-1useTranslations 命名空间切换;✅ V2-P2-1POLL_INTERVAL_MS 常量;✅ V2-P2-13c集成 useDesktopNotifications 桌面推送,监听新通知并触发桌面提醒 |
**客户端行为**
- `notification-dropdown.tsx`:✅ V2-P3 改用 `useNotificationStream` Hook 消费 SSE 实时推送(`/api/notifications/stream`SSE 不可用时自动降级为轮询(调用 `getNotificationsAction` + `getUnreadNotificationCountAction`,间隔 30 秒)
- `notification-dropdown.tsx`:✅ V2-P3 改用 `useNotificationStream` Hook 消费 SSE 实时推送(`/api/notifications/stream`SSE 不可用时自动降级为轮询(调用 `getNotificationsAction` + `getUnreadNotificationCountAction`,间隔 30 秒);✅ V2-P2-13c 集成 `useDesktopNotifications` Hook通过 `prevNotificationIds` 对比检测新通知并触发桌面推送(首次加载不批量推送)
- `hooks/use-notification-stream.ts`:✅ V2-P3 新增,管理 SSE 连接生命周期EventSource onopen/onmessage/onerror降级时通过 `pollFnRef` 调用 Server Actions 轮询
- `hooks/use-desktop-notifications.ts`:✅ V2-P2-13c 新增,封装浏览器 Notification API权限管理、自动请求权限、桌面推送、点击跳转回调SSR 安全(`typeof window !== "undefined"` 检查)
---
@@ -1243,8 +1247,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**职责**:公告 CRUD + 发布/归档 + 发布通知。
**导出函数**
- 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 新增)/ `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData`(✅ P1-5 新增:管理端列表页和编辑页编排函数,页面层仅调用单一函数)
- Actions`getAnnouncementsAction` / `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction`(✅ P1-2 已修复actions 层不再直接访问 DB全部下沉到 data-access✅ 发布公告时触发通知模块 `sendBatchNotifications`/ `toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`(✅ V2-P2-13d 新增:置顶切换、已读标记、批量已读状态查询)
- Data-access`getAnnouncements`(支持 `audience` 受众过滤)/ `getAnnouncementById` / `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`(后 5 个为 P1-2 新增)/ `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData`(✅ P1-5 新增:管理端列表页和编辑页编排函数,页面层仅调用单一函数)/ `toggleAnnouncementPin` / `markAnnouncementAsRead` / `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser`(✅ V2-P2-13d 新增:置顶切换 + 已读回执 CRUD操作 `announcement_reads` 表)
**依赖关系**
- 依赖:`shared/*``@/auth``school`(获取年级列表)、`classes`(获取班级列表 + 解析受众)、`users`(获取目标用户 ID 列表)、`notifications`(发布公告时发送通知)
@@ -1267,14 +1271,15 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ V2-P0-2 已修复:~~通知标题硬编码~~ `createAnnouncementAction` / `updateAnnouncementAction` / `publishAnnouncementAction` 通过 `getTranslations('announcements')` 生成 i18n 通知标题(`notification.publishedTitle` / `publishedContent`
- ✅ V2-P1-1 已修复:~~AnnouncementList 客户端 useState/useMemo 过滤~~ 改为纯服务端过滤模式Select 切换仅更新 URL `?status=` 触发 RSC 重新渲染
- ✅ V2-P1-4 已修复:~~表单无服务端校验错误展示~~ `announcement-form.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示title/content/targetGradeId/targetClassId
- ✅ V2-P2-13d 已实现:公告置顶 + 已读回执。`announcements` 表新增 `isPinned` 字段(置顶优先排序,`getAnnouncements` orderBy 改为 `desc(isPinned), desc(createdAt)`);新增 `announcement_reads` 表(唯一索引 announcementId+userId 保证幂等);新增 3 个 Server Action`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`+ 5 个 data-access 函数(`toggleAnnouncementPin` / `markAnnouncementAsRead` / `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser``Announcement` 类型新增 `isPinned` / `isReadByCurrentUser?` / `readCount?` 字段;新增 `AnnouncementRead` 类型;埋点新增 `announcement.pin_toggled` / `announcement.marked_read`
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~330 | 6 个 Server Action + 通知触发逻辑 + trackEvent 埋点P1-2 已修复,无直接 DB 操作) |
| `data-access.ts` | ~230 | 公告 CRUD + 发布/归档 + 受众过滤 + `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData` 编排函数(✅ P1-5 新增) |
| `actions.ts` | ~400 | 9 个 Server Action + 通知触发逻辑 + trackEvent 埋点P1-2 已修复,无直接 DB 操作V2-P2-13d 新增置顶/已读/批量已读状态 3 个 Action |
| `data-access.ts` | ~300 | 公告 CRUD + 发布/归档 + 受众过滤 + `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData` 编排函数(✅ P1-5 新增V2-P2-13d 新增置顶切换 + 已读回执 5 个函数,操作 `announcement_reads` |
| `schema.ts` | ~70 | Zod 校验 + `refineAudience` 条件校验(✅ P1-6 新增 superRefine |
| `types.ts` | ~65 | 类型定义(`GetAnnouncementsParams` 新增 `audience` 字段) |
| `types.ts` | ~75 | 类型定义(`GetAnnouncementsParams` 新增 `audience` 字段V2-P2-13d `Announcement` 新增 `isPinned` / `isReadByCurrentUser?` / `readCount?`,新增 `AnnouncementRead` 接口 |
**组件清单**
| 组件 | 职责 |

View File

@@ -8408,6 +8408,45 @@
"usedBy": [
"待扩展"
]
},
{
"name": "toggleAnnouncementPinAction",
"permission": "ANNOUNCEMENT_MANAGE",
"signature": "(id: string) => Promise<ActionState<string>>",
"purpose": "V2-P2-13d 新增:切换公告置顶状态(置顶公告在列表中优先显示)",
"deps": [
"requirePermission",
"data-access.toggleAnnouncementPin"
],
"usedBy": [
"待扩展"
]
},
{
"name": "markAnnouncementAsReadAction",
"permission": "ANNOUNCEMENT_READ",
"signature": "(announcementId: string) => Promise<ActionState<string>>",
"purpose": "V2-P2-13d 新增:标记公告为已读(当前用户维度,幂等)",
"deps": [
"requirePermission",
"data-access.markAnnouncementAsRead"
],
"usedBy": [
"待扩展"
]
},
{
"name": "getAnnouncementReadStatusAction",
"permission": "ANNOUNCEMENT_READ",
"signature": "(announcementIds: string[]) => Promise<ActionState<Record<string, boolean>>>",
"purpose": "V2-P2-13d 新增:批量获取当前用户对多个公告的已读状态(用于列表页标记已读/未读)",
"deps": [
"requirePermission",
"data-access.getAnnouncementReadStatusForUser"
],
"usedBy": [
"待扩展"
]
}
],
"dataAccess": [
@@ -8505,6 +8544,71 @@
"usedBy": [
"archiveAnnouncementAction"
]
},
{
"name": "toggleAnnouncementPin",
"signature": "(id: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "V2-P2-13d 新增:切换公告置顶状态(查询当前 isPinned 后取反更新)",
"deps": [
"shared.db",
"shared.db.schema.announcements"
],
"usedBy": [
"toggleAnnouncementPinAction"
]
},
{
"name": "markAnnouncementAsRead",
"signature": "(announcementId: string, userId: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "V2-P2-13d 新增:标记公告为已读(先查后插,依赖唯一索引保证幂等)",
"deps": [
"shared.db",
"shared.db.schema.announcementReads"
],
"usedBy": [
"markAnnouncementAsReadAction"
]
},
{
"name": "isAnnouncementReadByUser",
"signature": "(announcementId: string, userId: string) => Promise<boolean>",
"file": "data-access.ts",
"purpose": "V2-P2-13d 新增:检查用户是否已读指定公告",
"deps": [
"shared.db",
"shared.db.schema.announcementReads"
],
"usedBy": [
"待扩展"
]
},
{
"name": "getAnnouncementReadCount",
"signature": "(announcementId: string) => Promise<number>",
"file": "data-access.ts",
"purpose": "V2-P2-13d 新增:获取公告的已读用户数(管理端统计)",
"deps": [
"shared.db",
"shared.db.schema.announcementReads"
],
"usedBy": [
"待扩展"
]
},
{
"name": "getAnnouncementReadStatusForUser",
"signature": "(announcementIds: string[], userId: string) => Promise<Map<string, boolean>>",
"file": "data-access.ts",
"purpose": "V2-P2-13d 新增:批量获取用户对多个公告的已读状态(列表页标记已读/未读)",
"deps": [
"shared.db",
"shared.db.schema.announcementReads"
],
"usedBy": [
"getAnnouncementReadStatusAction"
]
}
],
"schemas": [
@@ -8532,7 +8636,7 @@
"name": "Announcement",
"type": "interface",
"file": "types.ts",
"definition": "{ id, title, content, type, status, targetGradeId, targetClassId, authorId, authorName, publishedAt, createdAt, updatedAt }",
"definition": "{ id, title, content, type, status, targetGradeId, targetClassId, authorId, authorName, publishedAt, isPinned, isReadByCurrentUser?, readCount?, createdAt, updatedAt }",
"usedBy": [
"announcements/components",
"页面"
@@ -8576,6 +8680,15 @@
"getAnnouncements",
"getAnnouncementsAction"
]
},
{
"name": "AnnouncementRead",
"type": "interface",
"file": "types.ts",
"definition": "{ id, announcementId, userId, readAt }",
"usedBy": [
"data-access.announcement_reads 相关函数"
]
}
],
"components": [
@@ -10820,6 +10933,64 @@
"unread-message-badge.tsx"
]
},
{
"name": "toggleMessageStarAction",
"permission": "MESSAGE_READ",
"signature": "(messageId: string) => Promise<ActionState<string>>",
"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<ActionState<MessageDraft[]>>",
"purpose": "V2-P2-13c 新增:获取当前用户的消息草稿列表(按 updatedAt 倒序)",
"deps": [
"requirePermission",
"data-access.getMessageDrafts"
],
"usedBy": [
"messages/compose/page.tsx"
]
},
{
"name": "saveMessageDraftAction",
"permission": "MESSAGE_SEND",
"signature": "(prevState: ActionState<string> | null, formData: FormData) => Promise<ActionState<string>>",
"purpose": "V2-P2-13c 新增保存消息草稿FormData 含 draftId 时更新,否则创建新草稿;返回草稿 ID",
"deps": [
"requirePermission",
"data-access.createMessageDraft",
"data-access.updateMessageDraft"
],
"usedBy": [
"messages/compose/page.tsx"
]
},
{
"name": "deleteMessageDraftAction",
"permission": "MESSAGE_SEND",
"signature": "(draftId: string) => Promise<ActionState<string>>",
"purpose": "V2-P2-13c 新增删除消息草稿仅草稿所有者可删revalidatePath /messages/compose",
"deps": [
"requirePermission",
"data-access.deleteMessageDraft",
"revalidatePath"
],
"usedBy": [
"messages/compose/page.tsx"
]
},
{
"name": "getNotificationPreferencesAction",
"permission": "MESSAGE_READ",
@@ -10929,6 +11100,19 @@
"deleteMessageAction"
]
},
{
"name": "toggleMessageStar",
"signature": "(id: string, userId: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "V2-P2-13c 新增:切换消息星标状态(仅接收方可标记;查询当前 isStarred 后取反更新)",
"deps": [
"shared.db",
"shared.db.schema.messages"
],
"usedBy": [
"toggleMessageStarAction"
]
},
{
"name": "getUnreadMessageCount",
"signature": "(userId: string) => Promise<number>",
@@ -10987,6 +11171,74 @@
"getRecipientsAction",
"messages/compose/page.tsx"
]
},
{
"name": "getMessageDrafts",
"signature": "(userId: string) => Promise<MessageDraft[]>",
"file": "data-access.ts",
"purpose": "V2-P2-13c 新增:获取用户消息草稿列表(按 updatedAt 倒序React cache 包装;批量解析 receiverName",
"deps": [
"shared.db",
"shared.db.schema.messageDrafts",
"shared.db.schema.users"
],
"usedBy": [
"getMessageDraftsAction"
]
},
{
"name": "createMessageDraft",
"signature": "(data: CreateMessageDraftInput) => Promise<string>",
"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<void>",
"file": "data-access.ts",
"purpose": "V2-P2-13c 新增:更新消息草稿(仅 owner 可改未提供字段保留原值updatedAt 由 onUpdateNow 自动更新)",
"deps": [
"shared.db",
"shared.db.schema.messageDrafts"
],
"usedBy": [
"saveMessageDraftAction"
]
},
{
"name": "deleteMessageDraft",
"signature": "(id: string, userId: string) => Promise<void>",
"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<MessageDraft | null>",
"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"
]
}
],
"notificationPreferences": [
@@ -11032,7 +11284,7 @@
"name": "Message",
"type": "type",
"file": "types.ts",
"definition": "{ id, senderId, receiverId, subject: string | null, content, isRead, readAt: string | null, parentMessageId: string | null, createdAt, senderName, receiverName }",
"definition": "{ id, senderId, receiverId, subject: string | null, content, isRead, isStarred, readAt: string | null, parentMessageId: string | null, createdAt, senderName, receiverName }",
"usedBy": [
"messaging/components",
"页面"
@@ -11121,6 +11373,36 @@
"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",
@@ -11705,12 +11987,30 @@
{
"name": "NotificationList",
"file": "components/notification-list.tsx",
"purpose": "P1-4 新增(从 messaging 迁移通知完整列表全部标记已读、单条标记已读、查看链接V2-P0-1 优化useTranslations 命名空间从 'messages' 切换到独立的 'notifications'"
"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 常量"
"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 实时推送 HookEventSource + 轮询降级),管理 SSE 连接生命周期,降级时通过 pollFnRef 调用 Server Actions 轮询",
"usedBy": [
"components/notification-dropdown.tsx"
]
},
{
"name": "useDesktopNotifications",
"file": "hooks/use-desktop-notifications.ts",
"purpose": "V2-P2-13c 新增:浏览器桌面推送 HookNotification API支持权限管理、自动请求权限、新通知桌面推送、点击跳转回调SSR 安全typeof window !== 'undefined' 检查)",
"usedBy": [
"components/notification-dropdown.tsx"
]
}
]
}
@@ -14956,7 +15256,7 @@
},
"dbTables": {
"_meta": {
"total": 59,
"total": 60,
"orm": "Drizzle ORM 0.45",
"database": "MySQL",
"idStrategy": "CUID2 (varchar length 128)",
@@ -15152,7 +15452,11 @@
"tables": {
"announcements": {
"owner": "announcements",
"description": "公告(school/grade/class,draft/published/archived)"
"description": "公告(school/grade/class,draft/published/archived;V2-P2-13d 新增 isPinned 置顶字段)"
},
"announcementReads": {
"owner": "announcements",
"description": "公告已读回执(V2-P2-13d 新增,唯一索引 announcementId+userId 保证幂等,cascade 删除)"
}
}
},
@@ -15209,7 +15513,11 @@
"tables": {
"messages": {
"owner": "messaging",
"description": "站内消息(含回复链 parentMessageId软删除 senderDeletedAt/receiverDeletedAt)"
"description": "站内消息(含回复链 parentMessageId软删除 senderDeletedAt/receiverDeletedAtV2-P2-13c 新增 isStarred 星标字段 + messages_receiver_starred_idx 复合索引)"
},
"messageDrafts": {
"owner": "messaging",
"description": "V2-P2-13c 新增:消息草稿(userId/receiverId/subject/content/parentMessageId + updatedAt/createdAt含 message_drafts_user_idx 和 message_drafts_user_updated_idx 索引)"
},
"messageNotifications": {
"owner": "notifications",
@@ -15841,6 +16149,7 @@
"auth-guard.requirePermission",
"auth-guard.requireAuth",
"db.schema.announcements",
"db.schema.announcementReads",
"types.permissions"
],
"auth": [