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:
@@ -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 实时推送 Hook(EventSource + 轮询降级),管理 SSE 连接生命周期,降级时通过 pollFnRef 调用 Server Actions 轮询",
|
||||
"usedBy": [
|
||||
"components/notification-dropdown.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "useDesktopNotifications",
|
||||
"file": "hooks/use-desktop-notifications.ts",
|
||||
"purpose": "V2-P2-13c 新增:浏览器桌面推送 Hook(Notification API),支持权限管理、自动请求权限、新通知桌面推送、点击跳转回调;SSR 安全(typeof window !== 'undefined' 检查)",
|
||||
"usedBy": [
|
||||
"components/notification-dropdown.tsx"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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/receiverDeletedAt;V2-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": [
|
||||
|
||||
Reference in New Issue
Block a user