Files
NextEdu/docs/architecture/audit/archive/announcements-audit-report.md

31 KiB
Raw Blame History

公告announcements模块审计报告

审查日期2026-06-25 审查范围:src/modules/announcements/**src/app/(dashboard)/announcements/**src/app/(dashboard)/admin/announcements/**src/shared/i18n/messages/{zh-CN,en}/announcements.json 架构图参考:docs/architecture/004_architecture_impact_map.md §2.16、docs/architecture/005_architecture_data.json#announcements 关联报告:announcements-messages-audit-report.md合并版2026-06-22已不再维护本文为公告模块的独立深度审计


一、现有实现概要

1.1 文件分布

路径 文件 行数 说明
路由 · 用户端 src/app/(dashboard)/announcements/page.tsx 1 36 列表页(所有非管理角色共用)
路由 · 用户端 src/app/(dashboard)/announcements/[id]/page.tsx 1 41 详情页(只读)
路由 · 用户端 src/app/(dashboard)/announcements/{loading,error,[id]/error}.tsx 3 60 骨架屏 + 错误边界
路由 · 管理端 src/app/(dashboard)/admin/announcements/page.tsx 1 45 管理列表页
路由 · 管理端 src/app/(dashboard)/admin/announcements/[id]/page.tsx 1 47 编辑页(直接渲染表单,无详情视图)
路由 · 管理端 src/app/(dashboard)/admin/announcements/{loading,error,[id]/error}.tsx 3 64 骨架屏 + 错误边界
模块 src/modules/announcements/actions.ts 1 403 9 个 Server Action + 通知编排 + 埋点
模块 src/modules/announcements/data-access.ts 1 413 CRUD + 发布/归档 + 置顶/已读 + 3 个页面编排函数
模块 src/modules/announcements/types.ts 1 75 类型定义
模块 src/modules/announcements/schema.ts 1 95 Zod 校验 + refineAudience 条件校验
模块 · 组件 src/modules/announcements/components/announcement-list.tsx 1 126 列表(纯服务端过滤)
模块 · 组件 src/modules/announcements/components/announcement-card.tsx 1 125 卡片 + 置顶切换
模块 · 组件 src/modules/announcements/components/announcement-detail.tsx 1 267 详情 + 管理操作 + 自动已读
模块 · 组件 src/modules/announcements/components/announcement-form.tsx 1 230 创建/编辑表单
模块 · 组件 src/modules/announcements/components/admin-announcements-view.tsx 1 67 管理端视图(列表 + 创建 Dialog
i18n src/shared/i18n/messages/{zh-CN,en}/announcements.json 2 103/103 11 命名空间翻译字典
测试 0 0 零测试文件

文件大小均在规范内(组件 ≤500 行actions/data-access ≤800 行)。

1.2 数据流

[Route] /announcements/page.tsx
   └─▶ announcements/data-access.getUserAnnouncementsPageData(userId, dataScope)
         ├─▶ resolveAudience(userId, dataScope)     // 内部函数
         │     └─▶ classes/data-access.{getClassGradeId | getStudentActiveClassId | getStudentActiveGradeId}
         └─▶ getAnnouncements({ status: "published", audience })

[Route] /announcements/[id]/page.tsx            ⚠️ 未做受众/状态过滤
   ├─▶ announcements/data-access.getAnnouncementById(id)
   └─▶ announcements/data-access.isAnnouncementReadByUser(id, userId)

[Route] /admin/announcements/page.tsx
   └─▶ announcements/data-access.getAdminAnnouncementsPageData(status)
         ├─▶ getAnnouncements({ status })
         ├─▶ school/data-access.getGrades()
         └─▶ classes/data-access.getAdminClasses()

[Route] /admin/announcements/[id]/page.tsx
   └─▶ announcements/data-access.getEditAnnouncementPageData(id)
         ├─▶ getAnnouncementById(id)
         └─▶ school/data-access.getGrades()

[Action] createAnnouncementAction / updateAnnouncementAction / publishAnnouncementAction
   └─▶ notifyAnnouncementPublished(announcement)
         ├─▶ resolveTargetUserIds(announcement)     // ⚠️ 纯业务逻辑在 actions.ts
         │     ├─▶ users/data-access.{getAllUserIds | getUserIdsByGradeId}
         │     └─▶ classes/data-access.{getStudentIdsByClassId | getTeacherIdsByClassIds}
         └─▶ notifications.sendBatchNotifications(payloads)

1.3 架构图记录情况

004_architecture_impact_map.md §2.16 对 announcements 模块的记录较为完整:

  • 导出函数9 个 Action + 14 个 data-access 函数)记录准确
  • 依赖关系(shared/*@/authschoolclassesusersnotifications)记录准确
  • 已修复问题清单P1-2/P1-5/P1-6/V2-P0-2/V2-P1-1/V2-P1-4/V2-P2-13d/V3-P0-2记录详实
  • 文件清单与组件清单行数准确

但架构图存在以下遗漏/不一致(详见第五章):

  1. 未记录 AnnouncementDetail 组件中 canManage=true 分支为死代码(无任何页面使用)
  2. 未记录 getAnnouncementReadStatusAction死代码(无任何调用方)
  3. 未记录 /announcements/[id] 路由层存在的安全越权风险(无受众过滤)
  4. 未记录 resolveAudience 内部函数的多孩子/多年级数据截断 Bug
  5. 未记录 actions 返回的英文字符串未走 i18n 的问题

二、现存问题与原因分析

2.1 【P0 · 安全越权】用户端详情页无受众/状态过滤

  • 位置src/app/(dashboard)/announcements/[id]/page.tsx:26-27
  • 问题:详情页直接调用 getAnnouncementById(id),未传入 audience 也未校验 status === "published"
  • 后果:任意持有 ANNOUNCEMENT_READ 权限的登录用户,只要知道/猜到公告 IDcuid2即可读取
    • 草稿(status="draft")公告——提前泄露未发布内容
    • 已归档(status="archived")公告——绕过归档语义
    • 其他年级/班级的定向公告——跨班级信息泄露(如某班处分通知被外班学生读到)
  • 违反规则:项目规则"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤" + "Parent routes must include permission checks with both parentId and studentId to prevent information leakage"。
  • 根因getAnnouncementById 设计为通用读取函数,未提供"按受众过滤"重载;路由层也未在读取后做二次校验。

2.2 【P0 · 数据截断】resolveAudience 仅取首个 gradeId / classId / childId

  • 位置src/modules/announcements/data-access.ts:351-395
  • 问题
    if (dataScope.type === "grade_managed") {
      const gradeId = dataScope.gradeIds[0]   // ⚠️ 仅取第一个
    }
    if (dataScope.type === "class_members" || dataScope.type === "class_taught") {
      const classId = dataScope.classIds[0]   // ⚠️ 仅取第一个
    }
    if (dataScope.type === "children") {
      const childId = dataScope.childrenIds[0] // ⚠️ 仅取第一个孩子
    }
    
  • 后果
    • 家长有多个孩子在不同班级/年级时,只能看到第一个孩子的定向公告,第二个孩子的班主任通知完全不可见——直接违反 K12 家长端核心诉求。
    • 年级主任管理多个年级时,只能看到第一个年级的公告。
    • 教师任课多个班级时,只能看到第一个班级的公告。
  • 违反规则:项目规则"Parent routes must include permission checks with both parentId and studentId" 与"data-access 层结合当前用户权限过滤"。
  • 根因getAnnouncementsaudience 参数设计为单值 { gradeId?, classId? },不支持多值;resolveAudience 为迁就该签名做了截断。

2.3 【P0 · 越权写】置顶/已读 Action 缺少资源所有权二次校验

  • 位置src/modules/announcements/actions.ts:335-376
  • 问题
    • toggleAnnouncementPinAction 仅校验 ANNOUNCEMENT_MANAGE,未校验公告是否存在、未校验调用者是否为该公告作者或管理员范围。
    • markAnnouncementAsReadAction 仅校验 ANNOUNCEMENT_READ,未校验该公告是否对当前用户可见(即未结合 2.1 的受众过滤)。任意用户可对任意公告 ID包括草稿、他人班级公告写入已读记录污染 announcement_reads 表。
  • 后果:数据库完整性被破坏;统计 readCount 失真;为后续基于已读率的分析埋下错误数据。
  • 违反规则:项目规则"Server Action 二次校验"。
  • 根因Action 层信任了 requirePermission 的角色校验未做资源级resource-level授权。

2.4 【P1 · i18n 违规】Actions 返回英文硬编码消息

  • 位置src/modules/announcements/actions.ts 全文
  • 问题:所有 Action 返回的 ActionState.message 均为英文字符串:
    • "Announcement created" / "Announcement updated" / "Announcement deleted"
    • "Announcement published" / "Announcement archived"
    • "Announcement not found" / "Invalid form data" / "Unexpected error"
    • "Pin status toggled" / "Announcement marked as read"
  • 这些 message 通过 toast.success(res.message) / toast.error(res.message) 直接展示给用户(见 announcement-detail.tsx:80,100,115announcement-form.tsx:80,88)。
  • 后果:中文用户在创建/发布/删除公告后看到英文 Toasti18n 字典中已定义的 messages.created / messages.updated 等翻译键完全未使用。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n" + "Server Action 返回值统一采用 ActionState<T> 类型"(隐含 message 应可本地化)。
  • 根因Actions 在 try 块内同步返回字符串,未通过 getTranslations("announcements") 获取本地化文案i18n 字典定义了键但 Action 未消费。

2.5 【P1 · 死代码】AnnouncementDetail 管理分支与 getAnnouncementReadStatusAction 无调用方

  • 位置
  • 问题
    • 全仓搜索 AnnouncementDetail 的使用方,仅 src/app/(dashboard)/announcements/[id]/page.tsx:34-38 一处,且 canManage={false}。管理端 /admin/announcements/[id] 直接渲染 AnnouncementForm(编辑模式),没有管理端详情页
    • 全仓搜索 getAnnouncementReadStatusAction零调用方。该 Action 返回 Record<string,boolean>,本应用于列表页批量标记已读/未读,但列表页从未调用。
  • 后果
    • 管理员无法在 UI 中执行发布/归档/删除/置顶操作(除非进入编辑表单),严重限制了管理端可用性。
    • announcement.readCount 字段在 AnnouncementDetail 中展示,但因 canManage 永远为 false用户永远看不到已读人数——已读统计功能在 UI 层完全不可见。
    • 列表页公告卡片没有"已读/未读"视觉区分,已读回执的数据无法驱动 UI。
  • 违反规则:项目规则"避免 backwards-compatibility hacks ... 如果确定未使用,应完全删除" + "识别四个角色共用的 UI 块"。
  • 根因:管理端路由设计遗漏了详情视图;已读状态查询 Action 未被列表组件消费。

2.6 【P1 · 耦合】组件直接 import actions未通过 Context/Provider 注入

  • 位置
    • announcement-card.tsx:13import { toggleAnnouncementPinAction } from "../actions"
    • announcement-detail.tsx:25-31import { archiveAnnouncementAction, deleteAnnouncementAction, markAnnouncementAsReadAction, publishAnnouncementAction, toggleAnnouncementPinAction } from "../actions"
    • announcement-form.tsx:21import { createAnnouncementAction, updateAnnouncementAction } from "../actions"
  • 问题:组件硬编码依赖具体 Server Action无法在不修改组件代码的前提下替换为 mock 实现。
  • 后果
    • 组件不可单元测试(必须 mock 整个 ../actions 模块)。
    • 无法为不同角色注入不同实现(如家长端只读、教师端可编辑班级公告)。
    • 未来若要将公告组件复用于"班级空间"或"家长端聚合页",必须重写组件。
  • 违反规则:用户要求"完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。
  • 根因:组件设计未遵循依赖注入原则。

2.7 【P1 · 耦合】AnnouncementForm 硬编码路由跳转

  • 位置announcement-form.tsx:82,217
  • 问题:表单提交成功后 router.push("/admin/announcements"),取消按钮也跳转到 /admin/announcements
  • 后果:表单无法在管理端以外的场景复用(如教师端发布班级公告、嵌入到班级详情页的快速发布公告入口)。
  • 违反规则:用户要求"组合优先 ... 逻辑复用一律抽取为自定义 hooks" + "最大化复用"。
  • 根因:表单未通过 onSuccess / onCancel 回调或 successHref prop 解耦导航。

2.8 【P1 · 业务逻辑位置】resolveTargetUserIds 放在 actions.ts

  • 位置src/modules/announcements/actions.ts:48-66
  • 问题:受众解析 + 用户 ID 聚合是纯业务逻辑(无 I/O 副作用之外的逻辑),却放在 Server Action 文件中,与 Action 编排逻辑混杂。
  • 后果
    • 无法独立单元测试(必须 mock getAllUserIds / getStudentIdsByClassId 等跨模块 data-access
    • data-access.ts 中的 resolveAudience 形成两套受众解析逻辑,职责重叠。
  • 违反规则:项目规则"可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离" + "Server Actions / Data Access 模块:建议 ≤ 800 行 ... 超过应考虑拆分"。
  • 根因actions.ts 既承担 HTTP 编排又承担业务规则,未分离 service 层。

2.9 【P1 · 性能】toggleAnnouncementPinmarkAnnouncementAsRead 非原子操作

  • 位置src/modules/announcements/data-access.ts:210-224,234-250
  • 问题
    • toggleAnnouncementPin:先 SELECT isPinned,再 UPDATE。两次 DB 往返,且在并发场景下存在 lost update两个管理员同时切换会得到错误结果
    • markAnnouncementAsRead:先 SELECT id,再 INSERT。已有唯一索引保证幂等,但多一次 SELECT 浪费往返。
  • 后果高并发时数据不一致DB 负载翻倍。
  • 违反规则:项目规则"性能:优先使用 React Server Components"(隐含高效数据访问)。
  • 根因:未使用 Drizzle 的 sql 表达式或 onDuplicateKeyUpdate/INSERT IGNORE 语义。

2.10 【P1 · 错误处理】handleActionError 吞错误上下文

  • 位置src/modules/announcements/actions.ts:34-40
  • 问题
    function handleActionError(e: unknown): ActionState<never> {
      if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
      if (e instanceof Error) return { success: false, message: e.message }
      return { success: false, message: "Unexpected error" }
    }
    
    • console.error 记录错误堆栈,生产环境无法定位故障。
    • 直接把 e.message 返回给前端,可能泄露内部错误信息(如 SQL 错误)。
    • "Unexpected error" 为英文硬编码。
  • 后果:可观测性差;安全信息泄露风险。
  • 违反规则:项目规则"错误与边界处理" + "i18n 就绪"。
  • 根因:错误处理未与日志/埋点/i18n 集成。
  • 位置announcement-card.tsx:80-92,116-121
  • 问题AnnouncementCard 在有 href 时用 <Link> 包裹整个卡片,同时卡片内的"置顶"按钮是一个 <button>handleTogglePin 调用 e.preventDefault() + e.stopPropagation() 处理鼠标点击,但:
    • 键盘聚焦到置顶按钮后按 Enter,部分浏览器会同时触发外层 <a> 的导航。
    • 屏幕阅读器会朗读"链接 标题",但置顶按钮的 aria-label 在链接上下文中语义模糊。
  • 后果键盘用户可能误跳转a11y 不达标。
  • 违反规则:项目规则"可访问性a11y语义化标签、ARIA 属性、键盘导航"。
  • 根因:交互按钮不应嵌套在导航链接内;应使用"卡片头部可点击 + 操作按钮独立"的布局。

2.12 【P2 · 类型不安全】mapRow 内联对象类型与 schema 脱钩

  • 位置src/modules/announcements/data-access.ts:23-53
  • 问题mapRow 的参数类型是手写的内联对象,未使用 Drizzle 推导类型 typeof announcements.$inferSelect
  • 后果schema 变更(如新增字段)时,mapRow 不会在编译期报错,导致类型漂移。
  • 违反规则:项目规则"TypeScript 严格模式 ... 函数返回值必须显式标注"。
  • 根因:未利用 Drizzle 的类型推导能力。

2.13 【P2 · i18n 字典冗余/缺失并存】

  • 位置src/shared/i18n/messages/zh-CN/announcements.json
  • 问题
    • 已定义但未使用的键:messages.created / messages.updated / messages.deleted / messages.published / messages.archived / messages.notFound / messages.createFailed / messages.invalidForm / messages.markedRead(共 9 个死键,因 actions 未消费)。
    • 缺失的键:description.detail(详情页描述)、description.create(创建 Dialog 描述)。
  • 后果i18n 字典维护成本上升;新增页面时找不到对应键。
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n"。
  • 根因i18n 键与代码未做同步校验。

2.14 【P2 · 死分支】detailHrefBuilder prop 未被使用

  • 位置announcement-list.tsx:39,70-74
  • 问题AnnouncementList 同时支持 detailHrefPrefix(字符串前缀)和 detailHrefBuilder(函数)两种 prop但全仓搜索 detailHrefBuilder 的传入方为零(所有调用方都使用 detailHrefPrefix)。
  • 后果:死代码增加维护负担。
  • 违反规则:项目规则"避免 backwards-compatibility hacks"。
  • 根因V3 重构引入 detailHrefPrefix 后未清理旧 prop。

2.15 【P2 · 重复骨架屏】用户端与管理端 loading.tsx 完全重复

2.16 【P2 · 无分页 UI】getAnnouncements 支持分页但 UI 未消费

  • 位置data-access.ts:55-112 支持 page / pageSizeannouncement-list.tsx 无分页控件。
  • 问题:列表页默认 pageSize=20,超过 20 条公告时静默截断,用户无法翻页。
  • 后果历史公告不可访问K12 学校一学期公告数通常 > 20。
  • 违反规则:用户要求"可扩展性:配置驱动设计"。
  • 根因:分页参数未贯穿到 UI。

2.17 【P2 · 表单与 Dialog 行为冲突】

  • 位置admin-announcements-view.tsx:57-64
  • 问题AdminAnnouncementsView 在 Dialog 中渲染 AnnouncementForm(创建模式)。但 AnnouncementForm 的 Cancel 按钮 router.push("/admin/announcements") 会触发整页跳转,而不是关闭 Dialog。提交成功后也是 router.push 而非 onSuccess 回调。
  • 后果用户体验割裂Dialog 内按钮触发路由跳转);handleOpenChange 中的 router.refresh() 与表单跳转重复。
  • 违反规则:项目规则"组合优先 ... 严禁使用继承或深层嵌套 HOC"。
  • 根因:表单未与容器解耦。

三、行业差距对比

参考 Google Classroom、钉钉教育、企业微信家校通、飞书校园版、PowerSchool 等主流 K12 产品的公告/通知模块,对比差距如下:

维度 行业主流实践 当前实现 差距影响
多受众定向 支持多班级/多年级/多角色组合发布(如"高三1班+2班家长" 仅支持单年级或单班级 年级组长需重复发布 N 次
富文本/附件 富文本编辑器 + 附件PDF 通知、图片) 纯文本 whitespace-pre-wrap 学校正式通知无法排版、无法附带 PDF
分类/标签 学科、活动、安全、家长信等分类筛选 仅按 status 筛选 家长在海量公告中找不到关注项
定时发布 选择未来时间自动发布 schema 有 publishedAt 但 UI 未消费 管理员需手动踩点发布
到期/置顶 自动到期 + 多级优先级(紧急/普通) 仅 pinned 布尔 紧急通知与普通通知无差异
已读统计仪表盘 管理端列表展示每条公告已读率、未读名单、可一键催读 readCount 字段存在但 UI 未展示 管理员无法评估公告触达效果
草稿预览 编辑时预览发布后效果 无预览 发布前无法验证排版
批量操作 列表多选 + 批量归档/删除 逐条操作 学期末清理 50 条公告需 50 次点击
搜索 标题/正文全文搜索 无搜索 历史公告无法检索
Dashboard 集成 首页"最新公告"Widget + 未读红点 仅家长端有快速入口链接 用户必须主动进入公告页
通知点击回跳 点击通知直达公告详情并自动已读 通知 actionUrl 指向详情页,但详情页无受众校验 通知点击可能触发越权
多语言/多角色文案 同一公告对家长/学生/教师展示不同侧重点 同一文案对所有角色 家长看到教师内部用语
无障碍 列表语义化 <ul>/<li>、键盘可达 <div> + <Link> 包裹按钮 屏幕阅读器用户导航困难
错误恢复 失败自动重试 + 离线草稿 失败仅 Toast 提示 网络波动时内容丢失

核心差距:当前实现停留在"CRUD + 状态机"的最小可用形态,缺少 K12 公告模块的"触达-反馈-统计"闭环。其中"已读统计不可见"和"无富文本/附件"是 K12 学校最痛的两个缺口。


四、改进优先级建议

P0必须立即修复 · 安全与数据正确性)

# 问题 改进方向
P0-1 详情页越权读取 新增 getAnnouncementByIdForUser(id, userId, dataScope) data-access 函数,结合 status="published" 与受众过滤;路由层调用此函数,未命中返回 notFound()
P0-2 resolveAudience 多孩子/多年级截断 audience 参数升级为 { gradeIds: string[]; classIds: string[] }getAnnouncementsinArray 查询;resolveAudience 返回完整数组而非首个
P0-3 置顶/已读 Action 缺资源级校验 toggleAnnouncementPinAction 校验公告存在;markAnnouncementAsReadAction 调用新增的 getAnnouncementByIdForUser 校验可见性后再写入

P1高优先级 · 架构与可维护性)

# 问题 改进方向
P1-1 Actions 返回英文硬编码 引入 getTranslations("announcements"),所有 ActionState.message 改用 i18n 键;新增 messageKey 字段或直接返回本地化字符串
P1-2 死代码:管理端详情分支 / getAnnouncementReadStatusAction 新增 /admin/announcements/[id]/view 详情页消费 AnnouncementDetail canManage=true;列表组件调用 getAnnouncementReadStatusAction 展示已读/未读角标;或删除死分支
P1-3 组件直接 import actions 新建 announcements-service-context.tsx,定义 AnnouncementsService 接口(含 togglePin / publish / archive / delete / markRead / create / update 方法签名),用 Provider 注入默认实现;组件 useContext 消费
P1-4 AnnouncementForm 硬编码路由 新增 onSuccess? / onCancel? 回调 prop回调优先于 router.push;默认 successHref prop 兜底
P1-5 resolveTargetUserIds 放 actions.ts 下沉到 data-access.tsresolveAnnouncementTargetUserIds(announcement) 纯函数actions.ts 仅做编排
P1-6 非原子 toggle / markRead toggleAnnouncementPin 改为 UPDATE ... SET is_pinned = NOT is_pinnedmarkAnnouncementAsRead 改为 INSERT ... ON DUPLICATE KEY UPDATE id=idDrizzle 的 onDuplicateKeyUpdate
P1-7 handleActionError 吞错误 新增 console.error + trackEvent("announcement.action_error")message 走 i18n不向客户端返回原始 e.message
P1-8 表单与 Dialog 行为冲突 表单通过 onSuccess 回调关闭 Dialog移除表单内的 router.push

P2中优先级 · 体验与工程化)

# 问题 改进方向
P2-1 a11y按钮嵌套在 Link 内 重构 AnnouncementCard:卡片本身为 <Link>,置顶按钮用绝对定位 + z-index 独立于链接,或改用 <article> + 独立链接 + 独立按钮的语义结构
P2-2 mapRow 类型脱钩 改用 typeof announcements.$inferSelect 推导;移除手写内联类型
P2-3 i18n 死键 / 缺键 删除未使用的 9 个 messages.* 死键(或随 P1-1 启用);补 description.detail / description.create
P2-4 detailHrefBuilder 死 prop 删除该 prop仅保留 detailHrefPrefix
P2-5 重复骨架屏 抽取 AnnouncementListSkeleton 共享组件到 components/
P2-6 已实施 无分页 UI → 新增 AnnouncementPagination 组件;getUserAnnouncementsPageData 返回 { items, total, page, pageSize }
P2-7 已实施 无测试 → 新增 schema.test.ts18 测试,refineAudience 矩阵)、is-announcement-visible.test.ts15 测试,纯函数含多孩子场景)、announcement-card.test.tsx16 测试,交互 + a11y

中长期P3 · 功能演进,对应行业差距)

# 方向 说明
P3-1 富文本 + 附件 接入 files 模块(已支持 targetType="announcement");引入轻量富文本编辑器(如 Tiptap
P3-2 分类/标签 新增 announcement_tags 表 + 列表筛选;预置 K12 分类(学科/活动/安全/家长信)
P3-3 定时发布 表单增加 publishedAt 日期选择器;新增 cron 校验到点自动 status="published"
P3-4 已读统计仪表盘 管理端列表展示已读率柱状图;详情页展示未读名单 + 一键催读(触发 sendBatchNotifications
P3-5 批量操作 列表多选 + 批量归档/删除 Action
P3-6 全文搜索 接入 app/api/search 已有的全局搜索(当前已支持 announcement 类型)
P3-7 Dashboard 集成 新增 AnnouncementsWidget(最新 3 条 + 未读红点),挂载到各角色 Dashboard
P3-8 草稿预览 表单"预览"按钮展开只读视图

五、架构图同步说明

本次审计发现 004_architecture_impact_map.md §2.16 与 005_architecture_data.json#announcements 存在以下遗漏,需在实施后同步更新:

  1. 新增节点

    • data-access.resolveAnnouncementTargetUserIdsP1-5 下沉的纯函数)
    • data-access.getAnnouncementByIdForUserP0-1 新增的受众过滤读取)
    • AnnouncementsServiceContextP1-3 新增的依赖注入 Provider
    • AnnouncementListSkeletonP2-5 抽取的共享骨架屏)
    • AnnouncementPaginationP2-6 新增的分页组件)
  2. 删除节点

    • actions.getAnnouncementReadStatusAction(若 P1-2 选择删除而非启用)
    • AnnouncementList.detailHrefBuilder propP2-4 删除)
  3. 修改节点

    • GetAnnouncementsParams.audience 类型从 { gradeId?; classId? } 改为 { gradeIds: string[]; classIds: string[] }
    • AnnouncementDetailcanManage 分支启用记录(新增管理端详情页后)
    • actions.ts 行数变化(下沉 resolveTargetUserIds 后减少)
    • data-access.ts 行数变化(新增函数后增加)
  4. 新增依赖关系

    • announcements → filesP3-1 附件集成后)
    • announcements → dashboardP3-7 Widget 集成后)
  5. 已知问题清单更新

    • 新增"P0-1 详情页越权"(修复后标记
    • 新增"P0-2 多孩子截断"(修复后标记
    • 新增"P0-3 资源级校验缺失"(修复后标记
    • 新增"P1-1 Actions i18n"(修复后标记

附:实施清单(与上述优先级一一对应)

实施将按 P0 → P1 → P2 → P3 顺序推进P3 为中长期演进,本次实施聚焦 P0/P1/P2P3 中富文本/附件/Dashboard 集成将择期推进。每完成一项同步更新架构图与运行 npm run lint + npx tsc --noEmit 验证。