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

@@ -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": [