DDD 实战(六):消息、通知、搜索、开放平台的跨域协作
DDD 实战(六):消息、通知、搜索、开放平台的跨域协作
推荐 BeWild 代充 Codex ,工作猛猛提效。
如果说账号、组织、关系、群组的难点是“边界怎么切”,那么消息、通知、搜索、开放平台的难点就是“边界切开以后怎么协作”。
协同办公 IM 的消息发送看似只有一个动作:用户发出一条消息。
但在真实系统里,一条消息可能触发很多后续动作:
- 写入消息事实。
- 更新会话列表。
- 计算未读数。
- 多端同步。
- 离线推送。
- 更新搜索索引。
- 触发机器人。
- 推送开放平台事件。
- 记录审计日志。
- 触发风控策略。
- 生成 AI 摘要。
如果把这些动作都放进一个同步事务,消息链路会越来越慢,也越来越脆。
DDD 的做法不是把所有东西拆开以后互相 RPC,而是识别核心事实和派生动作,用领域事件组织跨上下文协作。
1. 公开能力能观察到什么
飞书开放平台提供 IM 消息相关 API,应用可以向用户、群组等目标发送消息。飞书发送消息 API
飞书和钉钉开放平台也都提供事件订阅、机器人、通讯录、审批等开放能力入口。飞书开放平台文档、钉钉开放平台文档
这些公开资料能说明两件事:
第一,成熟协同办公 IM 会把消息能力开放给内部应用和外部应用。消息不只是用户手动发送,也可能来自机器人、审批、系统通知、第三方应用。
第二,消息会和很多平台能力协作。开放平台需要事件,机器人需要回调,通知需要投递,搜索需要索引,审计需要记录。
但公开资料不能说明厂商内部如何拆服务、如何设计表、如何投递事件。因此本文只基于公开能力推导一种合理架构。
2. 消息发送主链路应该尽量短
一个稳健的消息发送主链路,可以控制在这些步骤内:
- 接口层接收请求,解析发送者、租户、设备和客户端消息 ID。
- Identity & Access 校验登录态和设备状态。
- Group & Conversation 校验会话是否存在、成员是否可发送、群是否禁言。
- Governance & Security 做必要的前置策略校验,比如敏感词、频率限制、内容大小。
- Messaging 使用客户端消息 ID 做幂等,写入消息事实。
- Messaging 记录
MessageSent领域事件或写入 outbox。 - 返回发送成功或明确失败。
这个链路的目标,是让“消息成为事实”这件事尽快完成。
下面这些动作,不应该阻塞主链路:
- 离线推送。
- 搜索索引。
- 机器人回调。
- 开放平台事件投递。
- AI 摘要。
- 消息审计报表。
- 未读数的复杂聚合。
它们重要,但不是消息写入事实的前置条件。
3. MessageSent 是业务事实,不是技术通知
MessageSent 应该表达一个已经发生的业务事实:某个发送者在某个会话里成功发送了一条消息。
事件内容可以包含:
MessageSent
├── event_id
├── occurred_at
├── tenant_id
├── conversation_id
├── message_id
├── sender_id
├── message_type
├── content_digest
└── visibility_scope
注意,它不应该包含所有消息正文和所有下游所需字段。
领域事件要在“足够表达事实”和“不泄漏领域内部模型”之间取平衡。搜索需要正文时,可以根据 message ID 回查消息上下文,或者由消息上下文发布面向索引的受控事件。开放平台只需要部分字段时,就不应该把内部敏感字段暴露出去。
事件设计得越像数据库行复制,边界越容易被破坏。
4. 通知上下文:投递是派生能力
通知上下文消费各种领域事件:
MessageSentGroupMemberJoinedApprovalCompletedDocumentSharedOrganizationMemberRemoved
它负责把业务事实转成用户可感知的提醒:
- 站内通知。
- 移动端 Push。
- 桌面端提醒。
- 邮件提醒。
- 短信提醒。
- 应用内红点。
通知上下文拥有的是通知偏好、投递任务、投递状态、频控规则。它不拥有消息事实,也不应该决定消息是否发送成功。
这里有一个重要 trade off:未读数应该归消息域还是通知域?
我的建议是分层看:
- 会话内的读状态和消息游标,属于 Messaging 或 Conversation。
- 跨应用红点、桌面提醒、Push 状态,属于 Notification。
- 页面上展示的总未读数,可以是查询侧组合视图。
不要为了一个 UI 数字,把消息和通知强行揉成一个模型。
5. 搜索上下文:索引不是事实源
搜索上下文最容易被误用。
用户希望全局搜索:
- 联系人。
- 群。
- 消息。
- 文档。
- 应用。
- 审批。
- 机器人。
于是很多系统会让搜索服务直接读各业务库,甚至反向修改业务状态。这会让搜索服务变成隐形中心。
更合理的方式是:
- 各业务上下文发布领域事件。
- 搜索上下文消费事件,构建自己的索引文档。
- 索引文档只作为查询视图。
- 权限过滤必须继承来源上下文的权限规则。
- 索引可删除、可重建、可延迟,但不能成为事实源。
例如消息被撤回后,Messaging 发布 MessageRecalled。Search 消费事件后更新或删除索引。即使搜索索引延迟,消息事实仍然以 Messaging 为准。
这也是为什么搜索适合最终一致。要求每条消息写入后立刻被全局搜索命中,通常会显著拖慢主链路。
6. 开放平台上下文:隔离外部复杂度
开放平台是协同办公 IM 的生态入口,但它不应该侵入内部领域模型。
开放平台上下文通常负责:
- 应用注册。
- API 凭证。
- OAuth 授权。
- 权限范围。
- 机器人。
- Webhook。
- 事件订阅。
- API 配额。
- 开发者审计。
- 回调重试。
它和内部领域的关系,应该是“授权访问 + 事件订阅 + 防腐层”。
例如内部 Messaging 发布 MessageSent,Open Platform 不应该直接把这个内部事件原样推给第三方应用。它应该转换成开放平台的发布语言:
Internal MessageSent
↓
Open Platform Event Translator
↓
External im.message.receive_v1
转换过程中要处理:
- 租户隔离。
- 应用权限。
- 字段脱敏。
- 事件版本。
- 回调签名。
- 失败重试。
- 频率限制。
这就是防腐层的价值。它防止外部 API 形态反向污染内部领域模型,也防止内部模型变化直接破坏开发者生态。
7. 机器人:看似消息,实则跨域
机器人是一个很好的边界案例。
机器人可以发送消息,所以它看起来属于 Messaging。机器人有凭证、回调和权限,所以它又像 Open Platform。机器人可能绑定群,所以它也和 Group 有关系。机器人还可能响应审批、文档、告警事件。
更合理的拆法是:
- Open Platform 拥有 Bot、AppCredential、WebhookSubscription。
- Group 拥有“某机器人是否被添加到群”的群内关系。
- Messaging 只接受一个经过授权的发送主体,写入消息事实。
- Governance 负责风控、频控和审计策略。
这样设计后,“机器人发消息”不是一个特殊消息链路,而是一个经过开放平台授权的发送用例。
这能避免 Messaging 被开放平台概念污染,也能避免 Open Platform 直接改消息表。
8. 事件驱动不是到处发 MQ
领域事件很有用,但它不是万能药。
适合事件驱动的场景通常具备这些特征:
- 事件表示已经发生的业务事实。
- 订阅方可以异步处理。
- 短暂延迟业务可接受。
- 消费失败可以重试或补偿。
- 发布方不需要知道所有订阅方。
不适合事件驱动的场景包括:
- 必须在当前请求内给出确定结果。
- 必须强一致校验的业务不变量。
- 失败后不能重试也不能补偿。
- 事件只是为了绕开清晰接口。
例如“成员是否有权限发送群消息”通常不能异步事件化。发送前就要校验。否则用户会看到发送成功,几秒后又被撤回,体验和一致性都很差。
但“消息发送后更新搜索索引”适合事件化。搜索延迟几秒通常可以接受,失败也可以重试。
9. 一致性策略:先分清强一致和最终一致
跨域协作最大的难点,是一致性。
协同办公 IM 中,通常需要强一致的有:
- 同一消息 ID 的幂等写入。
- 群成员权限和发送资格的关键校验。
- 群主唯一性。
- 撤回权限校验。
- 账号禁用后的关键操作拦截。
可以最终一致的有:
- 搜索索引更新。
- 离线推送。
- 机器人回调。
- 开放平台事件投递。
- 审计报表。
- AI 摘要。
- 全局未读聚合视图。
DDD 不要求所有东西都最终一致。它要求你把一致性边界说清楚。
强一致的规则应该留在聚合和必要的同步校验里。最终一致的派生能力,应该通过领域事件、outbox、幂等消费和补偿任务完成。
10. 推荐的跨域协作链路
一条消息发送后的推荐协作链路可以这样设计:
Client
↓
Messaging Application Service
↓ 校验
Identity / Group / Governance
↓ 写入
Message Aggregate + Outbox
↓ 发布
MessageSent
↓
Notification / Search / Open Platform / Audit / AI
其中关键规则是:
- Messaging 拥有消息事实。
- Notification 拥有通知任务。
- Search 拥有索引视图。
- Open Platform 拥有外部应用交互。
- Audit 拥有审计追加记录。
- AI 拥有异步分析任务。
每个上下文都可以失败、重试和降级,但不能反向篡改消息事实。
这就是 DDD 在复杂 IM 平台中的实际价值:它不是把系统拆散,而是让每个模块知道自己拥有的事实、消费的事实和不该越界的事实。
下一篇我们把领域模型继续往下推,看它如何影响部署模块:单体、模块化单体、微服务和 monorepo 到底怎么选。