- 新增审计报告 docs/architecture/audit/announcements-messages-audit-report.md - 新增中英双语 i18n 字典 announcements.json / messages.json(11/13 个命名空间) - 重构所有 announcements 和 messaging 组件接入 next-intl(useTranslations) - 所有页面 page.tsx 使用 generateMetadata + getTranslations 替代硬编码 metadata - 新增 7 个 error.tsx 错误边界(4 公告 + 3 消息),统一 EmptyState + i18n + 重试 - a11y 改进:announcement-card / message-list / notification-dropdown 添加 aria-label - 同步架构图 004 和 005:i18n.messages 清单 + 已知问题修复记录
24 KiB
公告和消息模块审计报告
审查日期:2026-06-22 审查范围:
src/modules/announcements/**、src/modules/messaging/**、src/modules/notifications/**、src/app/(dashboard)/announcements/**、src/app/(dashboard)/admin/announcements/**、src/app/(dashboard)/messages/**架构图参考:docs/architecture/004_architecture_impact_map.md§2.13 / §2.14 / §2.16、docs/architecture/005_architecture_data.json
一、现有实现概要
1.1 文件分布
| 层 | 路径 | 文件数 | 说明 |
|---|---|---|---|
| 路由层 - 用户端公告 | src/app/(dashboard)/announcements/ |
2 个 page.tsx + 1 个 loading.tsx |
列表 + 详情,所有角色共用 |
| 路由层 - 管理端公告 | src/app/(dashboard)/admin/announcements/ |
2 个 page.tsx + 1 个 loading.tsx |
管理列表 + 编辑 |
| 路由层 - 消息 | src/app/(dashboard)/messages/ |
3 个 page.tsx + 3 个 loading.tsx + 1 个 error.tsx |
列表 + 详情 + 撰写 |
| 模块层 - announcements | src/modules/announcements/ |
4 个核心文件 + 5 个组件 | actions(296行) / data-access(197行) / types(61行) / schema(45行) |
| 模块层 - messaging | src/modules/messaging/ |
4 个核心文件 + 6 个组件 | actions(312行) / data-access(246行) / types(52行) / schema(44行) |
| 模块层 - notifications | src/modules/notifications/ |
6 个核心文件 + 5 个渠道文件 | actions(159行) / data-access(174行) / dispatcher(152行) / preferences(191行) / types(153行) |
1.2 数据流
[Route] /announcements/page.tsx
└─▶ announcements/data-access.getAnnouncements (status=published, audience={gradeId,classId})
└─▶ classes/data-access.getClassGradeId / getStudentActiveClassId / getStudentActiveGradeId
[Route] /admin/announcements/page.tsx
├─▶ announcements/data-access.getAnnouncements
├─▶ school/data-access.getGrades
└─▶ classes/data-access.getAdminClasses
(页面层直接编排 3 个模块的 data-access)
[Route] /messages/page.tsx
├─▶ messaging/data-access.getMessages
└─▶ notifications/data-access.getNotifications
(页面层直接编排 2 个模块的 data-access)
[Route] /messages/compose/page.tsx
└─▶ messaging/data-access.getRecipients
└─▶ classes/data-access.getStudentIdsByClassIds / getTeacherIdsByClassIds / getClassesByGradeId / getStudentActiveClassId
└─▶ users/data-access.getUserNamesByIds
[Action] announcements/actions.createAnnouncementAction
└─▶ notifications.sendBatchNotifications (发布公告时批量通知)
[Action] messaging/actions.sendMessageAction
└─▶ notifications.dispatcher.sendNotification (发消息时通知收件人)
1.3 架构图记录情况
004_architecture_impact_map.md 对三个模块的记录较为完整:
- §2.13 messaging:记录了 P0-4 / P1-5 已修复的双向依赖问题,文件清单准确
- §2.14 notifications:记录了渠道抽象和从 messaging 迁移的历史
- §2.16 announcements:记录了模块职责和依赖关系
但存在以下遗漏:
- 未记录 messaging 组件目录下
notification-dropdown.tsx和unread-message-badge.tsx两个组件 - 未记录 announcements 模块的
components/子目录(5 个组件文件未在文件清单中列出) - 未记录消息列表的客户端搜索行为(
getMessagesAction在客户端被调用) - 未记录通知下拉菜单的 30 秒轮询机制
二、现存问题与原因分析
2.1 国际化完全缺失(P0)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| announcements/components/announcement-list.tsx L24-29 | "All" / "Published" / "Draft" / "Archived" 硬编码 |
"所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键" |
| announcements/components/announcement-detail.tsx L29-38 | STATUS_LABEL / TYPE_LABEL 全英文硬编码 |
同上 |
| announcements/components/announcement-card.tsx L9-28 | STATUS_LABEL / TYPE_LABEL 重复定义且硬编码 |
同上 |
| announcements/components/announcement-form.tsx L86,92,98,108 | "New Announcement" / "Title" / "Content" 等硬编码 |
同上 |
| messaging/components/message-list.tsx L81-88 | "Inbox" / "Sent" / "Compose" 硬编码 |
同上 |
| messaging/components/message-detail.tsx L38,74,99-106 | "From" / "To" / "Message" / "New" / "Read" / "Sent" 硬编码 |
同上 |
| messaging/components/message-compose.tsx L78,84,102,113 | "Reply" / "New Message" / "To" / "Subject" 硬编码 |
同上 |
| messaging/components/notification-list.tsx L25-30,69-70 | TYPE_LABEL 硬编码,"Notifications" 标题硬编码 |
同上 |
| messaging/components/notification-dropdown.tsx L113 | "Notifications" / "Mark all read" 硬编码 |
同上 |
src/shared/i18n/messages/ |
无 announcements.json 或 messages.json |
翻译文件结构不完整 |
| i18n/request.ts L22-29 | 未加载 announcements/messages 翻译文件 | 翻译文件未注册 |
后果:所有用户可见文本无法切换语言,中文用户看到全英文界面,严重影响 K12 学校教师/家长/学生的使用体验。同一组件中 STATUS_LABEL 重复定义(card 和 detail 各一份),维护成本高。
2.2 角色硬编码与配置驱动缺失(P0)
| 位置 | 代码 | 违反规则 |
|---|---|---|
| layout/config/navigation.ts L39 | NAV_CONFIG: Partial<Record<Role, NavItem[]>> 按角色分组 |
"前端权限判断统一使用 usePermission().hasPermission(),严禁出现 role === 'xxx' 硬编码" |
| 同上 L99-103, L247-251, L307-311, L343-347 | admin/teacher/student/parent 各自配置 Announcements 和 Messages 导航项 |
配置未抽象,新增角色需复制粘贴 |
后果:新增角色(如 grade_head 已存在)无法享受公告/消息导航;导航配置按角色而非权限驱动,违反"配置驱动设计"原则。
2.3 架构分层:页面层越权编排(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| admin/announcements/page.tsx L33-37 | 页面层 Promise.all 调用 announcements/school/classes 三个模块的 data-access |
"app/ 只能调用 modules/ 的 Server Actions 和 data-access" — 虽语法允许,但编排逻辑应在模块 actions 层完成 |
| messages/page.tsx L17-20 | 页面层并行调用 messaging 和 notifications 两个模块的 data-access | 同上 |
| announcements/page.tsx L27-76 | resolveAudience 函数包含 50 行业务逻辑(根据 dataScope 解析受众) |
纯逻辑应抽为 hooks 或 data-access 层函数 |
announcements 模块无 getAdminAnnouncementsPageData 编排函数 |
缺失编排层 | "模块标准结构"要求 actions.ts 承担编排职责 |
后果:页面层臃肿、逻辑不可复用、不可测试;多个页面需要相同数据时需复制编排逻辑。
2.4 模块间组件耦合(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| messaging/components/notification-list.tsx L16 | 直接 import type { Notification, NotificationType } from "@/modules/notifications/types" |
"模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access(只能通过注入的接口调用)" |
| messaging/components/notification-dropdown.tsx L27 | 同上,直接 import notifications 模块类型 | 同上 |
| messaging/components/notification-list.tsx L15 | 直接 import ../actions 中的 markAllNotificationsAsReadAction / markNotificationAsReadAction |
messaging 模块的 actions re-export 了 notifications 的 actions,造成职责混乱 |
| messaging/actions.ts L196-248 | messaging 模块定义了 6 个通知相关 Action(getNotificationsAction / markNotificationAsReadAction 等) |
通知 Action 应由 notifications 模块提供,messaging 仅负责私信 |
后果:messaging 和 notifications 模块在 UI 层和 Action 层深度耦合,无法独立替换或测试;notifications 模块的 UI 组件无法复用到其他场景。
2.5 错误边界缺失(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
src/app/(dashboard)/announcements/error.tsx |
缺失 | "每个独立的数据区块必须用 React Error Boundary 包裹" |
src/app/(dashboard)/announcements/[id]/error.tsx |
缺失 | 同上 |
src/app/(dashboard)/admin/announcements/error.tsx |
缺失 | 同上 |
src/app/(dashboard)/admin/announcements/[id]/error.tsx |
缺失 | 同上 |
src/app/(dashboard)/messages/[id]/error.tsx |
缺失 | 同上 |
src/app/(dashboard)/messages/compose/error.tsx |
缺失 | 同上 |
src/app/(dashboard)/admin/announcements/loading.tsx |
缺失(仅有用户端 loading) | 加载骨架屏不完整 |
后果:数据加载失败时整页崩溃,用户体验差;无权限访问时显示原始错误而非友好提示。
2.6 通知轮询性能问题(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| notification-dropdown.tsx L65-68 | 每 30 秒轮询 getNotificationsAction + getUnreadNotificationCountAction |
"性能:优先使用 React Server Components 获取初始数据" |
| unread-message-badge.tsx L31-33 | 每 60 秒轮询 getUnreadMessageCountAction |
同上 |
| 两个组件未使用 RSC 初始数据 | 客户端首次渲染无数据,需等待轮询 | "客户端组件仅负责交互" |
后果:多用户同时在线时,每分钟产生大量无效请求;首屏渲染时无数据,显示空状态闪烁。
2.7 公告表单校验不足(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| schema.ts L9-10 | targetGradeId / targetClassId 为 optional,未根据 type 做条件必填校验 |
"输入使用 Zod 验证,验证失败返回结构化错误" |
| announcement-form.tsx L49-54 | type === "grade" 时不强制选择年级,type === "class" 时不强制选择班级 |
同上 |
| actions.ts L43-61 | resolveTargetUserIds 在 type === "grade" 但 targetGradeId 为空时返回空数组,公告无人接收 |
数据完整性缺失 |
后果:管理员可能创建无受众的公告,发布公告后无人收到通知,且无任何错误提示。
2.8 消息列表搜索逻辑复杂且无分页 UI(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| message-list.tsx L38-58 | 客户端 useEffect + setTimeout 防抖搜索,但未取消已发出的请求 |
"可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks" |
| 同上 L71-74 | filtered 在客户端再次过滤 displayMessages,与已搜索结果重复过滤 |
逻辑冗余 |
| 同上 L17 | 初始加载 pageSize: 50,但无分页 UI,超过 50 条无法查看 |
"明确处理空数据、无权限、网络异常等边界状态" |
| messages/page.tsx L18 | 一次性加载 50 条消息,无虚拟滚动 | 性能问题 |
后果:消息超过 50 条时用户无法查看历史;搜索逻辑与 UI 混合,无法单独测试。
2.9 无权限与空状态处理不友好(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| 所有页面 | requirePermission 抛出 PermissionDeniedError 后,由上层 error.tsx 处理,但无专门的无权限空状态 |
"明确处理空数据、无权限、网络异常等边界状态" |
| message-list.tsx L116-127 | 空状态文本硬编码且未区分"无权限"与"无数据" | 同上 |
| notification-list.tsx L80-86 | 通知空状态未提供"去设置通知偏好"等引导操作 | 用户体验不完整 |
后果:用户无法区分"无数据"和"无权限",无法找到下一步操作引导。
2.10 可访问性问题(P2)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| message-list.tsx L104-110 | 搜索框无 aria-label,仅靠 placeholder |
"可访问性(a11y):语义化标签、ARIA 属性、键盘导航" |
| notification-dropdown.tsx L139-144 | DropdownMenuItem 的 onSelect 阻止默认行为后手动调用 handleMarkRead,键盘导航时焦点处理不明确 |
同上 |
| announcement-card.tsx L66-72 | 整个 Card 作为链接,但无 aria-label 描述跳转目标 |
同上 |
| notification-list.tsx L118-124 | "Mark as read" 按钮无 aria-label,屏幕阅读器无法识别 |
同上 |
后果:视障用户无法有效使用公告和消息功能,不符合 WCAG 2.1 AA 标准。
2.11 监控埋点缺失(P2)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| announcements/actions.ts | 发布/归档/删除公告无埋点 | "监控:方案中预留关键操作埋点接口" |
| messaging/actions.ts | 发送/删除消息无埋点 | 同上 |
| notifications/data-access.ts L167-173 | 仅 console.info 输出发送日志,无结构化埋点 |
同上 |
后果:无法追踪公告阅读率、消息回复率等关键指标;通知发送失败无法告警。
2.12 消息软删除无事务(P2)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| messaging/data-access.ts L180-191 | deleteMessage 执行两个独立的 UPDATE(senderDeletedAt + receiverDeletedAt),无事务 |
"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤" |
| 同上 | 两个 UPDATE 之间可能部分失败,导致数据不一致 | 数据完整性问题 |
后果:发送方删除后接收方可能仍可见,或反之,造成数据不一致。
2.13 测试覆盖不足(P2)
| 位置 | 问题 | 违反规则 |
|---|---|---|
tests/e2e/announcements.spec.ts |
仅 2 个测试(未登录重定向 + 登录后可见),无管理端测试 | "可测试性" |
tests/e2e/ |
无 messaging 模块 E2E 测试 | 同上 |
src/modules/announcements/ |
无单元测试 | 同上 |
src/modules/messaging/ |
无单元测试 | 同上 |
src/modules/notifications/ |
无单元测试 | 同上 |
后果:重构时无回归保障,关键业务逻辑(权限过滤、受众解析、通知分发)错误无法及时发现。
三、行业差距对比
3.1 公告模块差距
| 功能 | 行业优秀实践 | 当前状态 | 影响 |
|---|---|---|---|
| 公告分类标签 | 支持自定义标签(紧急、活动、政策),可按标签筛选 | 仅 type(school/grade/class)和 status,无标签 | 教师无法快速筛选紧急公告 |
| 已读回执 | 显示已读/未读用户列表,支持提醒未读 | 无已读回执,仅通知发送 | 管理员无法知道公告是否被阅读 |
| 富文本编辑 | 支持富文本、图片、附件 | 仅纯文本 Textarea | 公告内容单调,无法插入图片 |
| 定时发布 | 支持指定时间自动发布 | publishedAt 字段存在但表单未暴露 |
管理员无法提前安排公告 |
| 公告置顶 | 支持置顶重要公告 | 无置顶功能 | 重要公告可能被新公告淹没 |
| 多渠道推送 | 站内 + 短信 + 邮件 + 微信 | 已实现多渠道(notifications 模块) | ✅ 已达标 |
| 评论互动 | 支持公告下评论或确认收到 | 无互动功能 | 无法收集公告反馈 |
3.2 消息模块差距
| 功能 | 行业优秀实践 | 当前状态 | 影响 |
|---|---|---|---|
| 消息分组 | 按联系人分组显示对话 | 仅按时间列表,无对话分组 | 教师与同一家长的来回消息散落各处 |
| 实时推送 | WebSocket / SSE 实时推送 | 30/60 秒轮询 | 消息延迟最高 30 秒,服务器压力大 |
| 消息草稿 | 支持草稿自动保存 | 无草稿功能 | 用户意外离开页面内容丢失 |
| 附件支持 | 支持发送文件附件 | 仅纯文本 | 无法发送作业截图等 |
| 消息星标 | 支持标记重要消息 | 无星标功能 | 重要消息无法快速找回 |
| 消息模板 | 支持常用消息模板 | 无模板 | 教师重复输入相同内容 |
| 群发消息 | 支持按班级/年级群发 | 仅支持单发 | 教师需逐个发送通知 |
| 消息搜索 | 全文搜索 + 按联系人/时间筛选 | 仅关键词搜索 subject + content | 无法按联系人筛选历史消息 |
| 已读回执 | 实时显示对方已读状态 | 仅 readAt 字段,无实时更新 |
发送方不知道消息是否被看到 |
3.3 通知模块差距
| 功能 | 行业优秀实践 | 当前状态 | 影响 |
|---|---|---|---|
| 通知分类管理 | 支持按类型分组(作业/成绩/公告/消息) | 仅按时间列表,类型仅作为 Badge | 用户无法快速找到特定类型通知 |
| 通知静音 | 支持单类通知静音 | 有 quietHours 但仅全局免打扰 |
用户想静音作业通知但保留成绩通知无法实现 |
| 通知归档 | 支持归档已处理通知 | 仅标记已读,无归档 | 通知列表越来越长 |
| 通知优先级 | 支持高/中/低优先级 | 无优先级 | 紧急通知被普通通知淹没 |
| 桌面推送 | 支持浏览器桌面通知 | 仅站内下拉 | 用户不打开页面就收不到通知 |
3.4 多角色体验差距
| 角色 | 痛点 | 当前状态 | 影响 |
|---|---|---|---|
| admin | 公告管理需切换到独立页面 | /admin/announcements 与 /announcements 分离 |
管理员查看用户视角需切换路由 |
| teacher | 消息收件人列表无法搜索 | MessageCompose 仅 Select 下拉 |
班级多时难以找到目标家长 |
| parent | 无法主动给教师发消息 | 依赖 getRecipients 返回的列表 |
家长需等待教师先发消息才能回复 |
| student | 公告无"确认收到"按钮 | 仅被动查看 | 学校无法确认学生是否看到公告 |
四、改进优先级建议
P0(紧急,影响核心功能与安全)
- i18n 全覆盖:创建
announcements.json和messages.json翻译文件,重构所有组件使用useTranslations替换硬编码文本,更新i18n/request.ts加载新文件。 - 消除角色硬编码:将
NAV_CONFIG改为权限驱动配置,公告和消息导航项仅声明permission,不按角色分组。 - 补充错误边界:为所有缺失的页面添加
error.tsx,区分"无权限"、"未找到"、"网络错误"三种状态。
P1(重要,影响架构与体验)
- 解耦 messaging 与 notifications:将通知相关组件(
notification-list.tsx、notification-dropdown.tsx)迁移至 notifications 模块;messaging 模块仅保留私信组件;通过 Context 注入数据服务接口。 - 页面编排下沉:在 announcements 和 messaging 模块新增
getAdminAnnouncementsPageData/getMessagesPageData编排函数,页面层仅调用单一函数。 - 公告表单条件校验:使用 Zod
superRefine根据type强制要求targetGradeId/targetClassId。 - 消息列表分页与虚拟滚动:添加分页 UI,超过 50 条时支持加载更多;搜索逻辑抽离为
useMessageSearchhook。 - 通知实时推送:将 30 秒轮询替换为 SSE 或 WebSocket,减少无效请求;首屏使用 RSC 获取初始数据。
- 消息软删除事务化:使用数据库事务包裹
senderDeletedAt和receiverDeletedAt更新。
P2(优化,提升完整性与可维护性)
- a11y 改进:为搜索框、按钮、链接添加
aria-label;确保键盘导航完整。 - 监控埋点:在关键 Action 中预留
trackEvent接口,记录发布公告、发送消息、标记已读等操作。 - 测试覆盖:补充 messaging 模块 E2E 测试;为
resolveTargetUserIds、getRecipients、selectChannels等纯函数添加单元测试。 - 行业功能补齐:公告已读回执、消息分组对话、消息草稿、通知优先级(按业务优先级逐步实施)。
- 架构图同步:补充 announcements 组件目录、messaging 的 notification-dropdown/unread-message-badge 组件、客户端搜索行为、轮询机制。
五、架构图同步说明
本次审计发现架构图存在以下遗漏,需补充:
5.1 004_architecture_impact_map.md 需补充
§2.13 messaging 模块文件清单:
- 当前记录:
actions.ts276 行 /data-access.ts/schema.ts41 行 - 实际状态:
actions.ts312 行 /data-access.ts246 行 /schema.ts44 行 /types.ts52 行 - 遗漏组件:
components/notification-dropdown.tsx、components/unread-message-badge.tsx未在文件清单中列出 - 遗漏行为:
notification-dropdown.tsx每 30 秒轮询、unread-message-badge.tsx每 60 秒轮询
§2.16 announcements 模块文件清单:
- 当前记录:仅列出 actions/data-access/schema/types
- 遗漏组件目录:
components/下 5 个组件(admin-announcements-view.tsx、announcement-card.tsx、announcement-detail.tsx、announcement-form.tsx、announcement-list.tsx)未列出
§2.13 messaging 依赖关系:
- 遗漏:
messaging/components/notification-list.tsx和notification-dropdown.tsx直接 import@/modules/notifications/types,存在跨模块 UI 类型依赖
5.2 005_architecture_data.json 需补充
modules.messaging.components数组缺少notification-dropdown.tsx和unread-message-badge.tsx两个节点modules.announcements.components数组完全缺失(5 个组件节点未记录)modules.messaging.exports缺少UnreadMessageBadge组件导出routes节点中/messages路由的dataAccess字段未记录客户端搜索行为(getMessagesAction在客户端被调用)
5.3 无需修改的部分
- §2.14 notifications 模块记录完整准确
- P0-4 / P1-5 修复历史记录准确
- 依赖矩阵(§3)中 messaging → notifications 的单向依赖记录正确