负责租户系统公告从草稿、发布、撤回到归档的完整生命周期,并管理受众筛选与用户已读事实。
一张图读懂主路径
珊瑚色节点表示本模块的核心决策或状态边界。
能力边界
- 草稿、发布、撤回与归档状态迁移
- 全员、角色与部门受众筛选
- 用户唯一已读事实与管理端已读计数
模块明确不包含:Announcements 只决定公告生命周期与可见性。当前用户、角色和部门仍以 Identity 与组织成员关系为准;站外消息投递仍归 Notifications。
代码地图
| 项目 | 主要职责 |
|---|---|
BitzOrcas.Platform.Announcements.Contracts | 公开契约、端口、DTO 与事件 |
BitzOrcas.Platform.Announcements.Application | 命令、查询、处理器与应用策略 |
BitzOrcas.Platform.Announcements.Infrastructure | 持久化、连接器与框架适配器 |
源码位置: src/Platform/Announcements
核心用例
CreateAnnouncementCommandUpdateAnnouncementCommandPublishAnnouncementCommandWithdrawAnnouncementCommandArchiveAnnouncementCommandListMyAnnouncementsQueryMarkAnnouncementReadCommand
阅读用例时,先看请求契约和权限声明,再跟进处理器调用的聚合或端口,最后检查提交后的事件、缓存和审计。
关键模型与契约
| 关键类型 | 阅读重点 |
|---|---|
AnnouncementDto | 管理端生命周期投影 |
AnnouncementView | 经过受众筛选的用户投影 |
IAnnouncementStore | 生命周期与可见性 Store 端口 |
IAnnouncementReadStore | 幂等已读状态端口 |
用例边界示例
| 字段 | 说明 |
|---|---|
| 代表性契约 | PublishAnnouncementCommand |
| 返回结果 | Result<AnnouncementDto> |
| 审查重点 | 只允许发布草稿,按服务端时钟校验生效窗口,并在公告可见前持久化新的并发版本。 |
示例伪代码
var existing = await store.FindByIdAsync(command.AnnouncementId, cancellationToken);// ① 只有 Draft 可以发布,所有时间判断都使用服务端时钟。if (existing is null) return AnnouncementErrors.NotFound;if (existing.Status != AnnouncementStatus.Draft.Name) return AnnouncementErrors.InvalidTransition;var now = clock.UtcNow;var effectiveAt = existing.EffectiveAt ?? now;// ② 已经过期或晚于生效时间的失效窗口都必须拒绝。if (existing.ExpiresAt is not null && (existing.ExpiresAt <= effectiveAt || existing.ExpiresAt <= now)) return AnnouncementErrors.InvalidEffectiveWindow;// ③ 迁移用 record with 表达式生成新快照;SaveAsync 成功后才对用户侧查询可见。var published = existing with{ Status = AnnouncementStatus.Published.Name, PublishedAt = now, EffectiveAt = effectiveAt, WithdrawnAt = null,};return await store.SaveAsync(published, cancellationToken);集成关系
用户侧查询通过组织成员仓储解析可信部门标识,再与当前用户角色共同筛选;模块不接受请求自行声明部门归属。
跨模块协作遵循以下方向:
- 调用方依赖本模块公开 Contracts 或窄端口。
- 状态变化通过版本化集成事件传播。
- 具体数据库、消息、缓存、文件或厂商 SDK 留在 Infrastructure。
- 组合根先注册失败关闭默认实现,再接入生产适配器。
安全、租户与隐私
管理端路由受公告权限与默认关闭的 announcements.manage Feature 约束。用户端从可信上下文取得租户、用户、角色和部门;公告不在受众或有效期内时按不可见处理。
模块保存租户数据时必须写入 TenantId,列表和详情使用同一授权规则。日志、事件和审计只保留诊断所需字段。
失败语义、幂等与可观测性
草稿更新携带显式并发版本;已发布正文不可修改,撤回立即停止展示,归档为终态。已读 Store 必须保证每个租户、用户与公告只有一条已读事实。
| 情况 | 处理原则 |
|---|---|
| 校验、未找到、冲突或禁止 | 返回类型化 Result/Error,并由统一映射生成 Problem Details |
| 适配器或关键状态不可用 | 安全或高价值操作失败关闭;只读降级必须显式标记 |
| 重复请求或重复事件 | 使用稳定幂等键,数据库唯一约束作为最后防线 |
| 外部超时 | 传播取消,记录脱敏诊断,仅对安全操作执行有界重试 |
测试矩阵
| # | 必须覆盖 | 建议层级 |
|---|---|---|
| 1 | 合法生命周期与过期并发版本 | 单元 + 集成 |
| 2 | 角色与部门受众筛选失败关闭 | 集成/契约 |
| 3 | 生效、过期与服务端时钟边界 | 集成/契约 |
| 4 | 幂等已读与管理端已读计数 | 集成/契约 |
测试还要固定一条通用红线:租户 A 的标识、缓存键、事件或查询条件不能让租户 B 读取或修改数据。
源码导航与变更检查
在 BitzOrcasVNext 仓库根目录运行:
# 列出公开类型,确认新增契约是否真的属于模块边界。rg -n "^public (sealed |abstract |static |partial )*(record|class|interface|enum)" src/Platform/Announcements -g '*.cs'
# 检查跨层引用;调用方不应依赖另一个模块的 Infrastructure。rg -n "ProjectReference|PackageReference" src/Platform/Announcements -g '*.csproj'
# 查找待补实现和省略式代码;预期没有命中。rg -n "TODO|FIXME|// \.\.\.|省略" src/Platform/Announcements -g '*.cs'扩展与发布检查
- 新能力是否由明确的 Contracts、权限和 Feature Key 表达?
- 持久化、连接器和 Provider 是否可以通过窄适配器替换?
- 生产组合是否仍命中 Null/Unavailable 默认实现?健康检查会不会误报绿色?
- 事务、事件、缓存失效、审计和后台任务是否有失败与重试证据?
- 中英文文档、图表、契约测试和运维手册是否随代码一起更新?