DDD 实战(四):协同办公 IM 的领域全景
海外 AI 服务代充推荐:BeWild(推广链接)。
前三篇——规则该归谁管、战略设计、战术设计——谈的是方法,从这一篇开始把它用在一个具体系统上。
笔者第一次给这个系统画领域地图时,犯的正是把产品菜单抄成服务清单的错:通讯录一个服务,聊天一个服务,工作台一个服务。图画得挺整齐,可一被追问“搜索结果里的成员姓名谁说了算”,就答不上来了——菜单描述的是用户从哪里进入,说明不了数据归谁、规则由谁决定。
协同办公 IM 还有一层麻烦:它表面上是聊天工具,实际承载的东西远不止聊天。钉钉、飞书这类产品的开放平台把通讯录、消息、审批、机器人、事件订阅都开放出来,等于把内部能力边界摆在了明面上。它更像一个企业协作操作系统,IM 只是最高频的那个入口。
前面三篇建立了 DDD 的基本概念,这篇将它们用于企业协作 IM。钉钉、飞书/Lark 的官网和开放平台文档只作为能力观察入口;下文的上下文、模型和拆分建议都是设计推导,不代表任何厂商的内部架构。
从公开能力整理业务范围
协同办公 IM 看起来是聊天工具,但成熟产品早已不是单纯发消息。
从钉钉官网可以看到,它面向企业沟通、组织协作、业务流程、AI 办公和开放生态等场景。钉钉官网
飞书/Lark 也把即时沟通、日历、文档、审批、开放平台和智能协作组合在一起,强调一站式企业协作。飞书官网、Lark 官网
开放平台文档进一步暴露了很多业务能力边界。比如飞书开放平台文档中可以看到通讯录、IM 消息、群组、事件订阅、机器人等能力入口。飞书开放平台文档
钉钉开放平台也提供通讯录、身份、消息、机器人、事件订阅、审批等开放能力入口。钉钉开放平台文档
这些入口可以帮助整理账号、通讯录、消息、群组和应用集成等能力。至于它们是否共用数据库、是否独立部署,公开 API 无法回答,领域地图也不需要先假定答案。
第一版地图以数据所有权为线索
下面列出一组候选边界。需要先说清楚:这张地图不是服务拆分清单,它的用途是讨论业务边界、数据所有权和演进方向。工作台与流程、安全与治理等暂时并列在一行,便于检查能力覆盖;它们未必适合共享一个领域模型,后续要按规则继续细分。
先用一张图看清事实的流向:
实线是事实与查询,虚线是可以延迟的业务事件。图里没有画工作台与开放平台——它们和关系、群组、消息处在同一层,只是不在这条事实主干上。这张图先回答“谁的事实从哪来、往哪去”,下面再把每个位置的职责摊开成表。
| 限界上下文 | 主要职责 | 核心模型 | 不应该承担 |
|---|---|---|---|
| Identity & Access | 账号、登录、认证、设备、登录会话、安全策略 | Account、Credential、Device、LoginSession、AuthPolicy | 不管理企业部门和好友关系 |
| Tenant & Organization | 企业、部门、成员、组织角色、通讯录可见性 | Tenant、Department、Member、OrgRole、VisibilityRule | 不处理登录凭证和群成员治理 |
| Contact & Relationship | 好友、联系人、外部联系人、黑名单、关系权限 | ContactRelation、FriendRequest、ExternalContact、BlockRelation | 不保存组织主数据 |
| Group & Conversation | 群、群成员、会话、群治理、会话设置 | Group、GroupMember、Conversation、ConversationSetting | 不保存消息正文和搜索索引 |
| Messaging | 消息发送、消息状态、撤回、引用、转发、已读未读 | Message、MessageReceipt、RecallRecord、MessageCursor | 不管理组织架构和群治理规则 |
| Notification | 站内通知、离线推送、提醒、通知偏好 | Notification、PushSubscription、Reminder、NotifyPreference | 不作为业务事实源 |
| Workbench & Workflow | 工作台、应用入口、审批、表单、任务、公告 | WorkbenchLayout、AppShortcut、ApprovalInstance、Form、Task | 不承担消息投递主链路 |
| Open Platform | 应用、机器人、Webhook、事件订阅、API 凭证、配额 | App、Bot、WebhookSubscription、ApiCredential、ApiQuota | 不拥有内部业务数据 |
| Search | 联系人搜索、消息搜索、文档搜索、应用搜索 | SearchDocument、IndexTask、SearchPermission | 不反写业务事实 |
| Governance & Security | 审计、风控、水印、敏感词、合规、数据隔离 | AuditEvent、RiskRule、DlpPolicy、ComplianceReport | 不绕过业务域直接修改聚合 |
这张表首先用于追问所有权。例如搜索结果里的成员姓名来自 Organization,但索引任务的重试状态属于 Search。两者都持有数据,只有前者有权决定姓名的业务事实。
“不应该承担”一列也需要结合用例验证。Open Platform 不拥有内部消息事实,却拥有应用、授权和订阅自己的事实,不能把它理解为无状态转发层。
账号能登录,不代表能访问某家企业
账号上下文处理的是“谁可以登录系统,以及以什么方式登录”。
它关心的是:
- 账号 ID。
- 手机号、邮箱、第三方身份。
- 密码、验证码和单点登录;涉及第三方身份时区分身份认证与 OAuth 授权。
- 设备绑定。
- 登录会话。
- 多端登录策略。
- 风险登录和安全策略。
组织成员上下文处理的是“某个账号在某个企业里是什么身份”。一个账号可以加入多个企业,可以在不同企业有不同部门、岗位、角色和可见性。
如果把账号和成员合并,麻烦不止一处:移除企业 A 的成员时很难表达“仍可登录,也仍可访问企业 B”;组织字段的调整会顺带污染登录模型;登录安全策略和组织权限策略被绑在一起,改哪一边都要顾及另一边;没有账号的外部联系人、客户和临时协作者,更是找不到地方安放。
因此本案例让账号上下文拥有 Account,组织上下文拥有 Member,两者通过 account ID 或主体 ID 关联。
分开以后还要补足授权判断:有效登录态只证明主体身份,不能代替目标企业内的成员有效性检查。这个关联是模型拆分之后必须承担的成本。
组织维护成员事实,也要控制事实的传播
协同办公 IM 和普通社交 IM 的最大差异之一,是它以组织为中心。
组织上下文通常要管理:
- 租户和企业。
- 部门树。
- 成员。
- 岗位、角色和职级。
- 通讯录可见性。
- 成员生命周期:邀请、入职、转岗、离职、禁用。
- 组织字段脱敏和权限。
开放平台中的通讯录能力也能说明组织数据的重要性。飞书开放平台和钉钉开放平台都把用户、部门、通讯录作为开放能力的重要部分。飞书开放平台文档、钉钉查询用户详情
消息发送要知道成员是否有效,文档授权可能需要部门身份,开放平台还要通知成员变更。让这些模块都读写组织表,会使组织结构调整变成跨系统改动。
笔者倾向于让 Organization 提供查询接口和变更事件,由下游保存所需的最少快照。同时约定哪些字段可以延迟,哪些撤权必须及时生效。通讯录查询和消息发送可以消费同一成员事实,却未必采用相同的缓存策略。撤权类变更走实时校验还是允许缓存过期,是这两条路径上代价差别最大的一个决定。
联系关系有自己的建立与终止过程
很多早期 IM 系统会把好友关系直接放进用户模块。简单产品可以这么做,但协同办公 IM 往往更复杂。
关系上下文可能需要表达:
- 好友申请。
- 好友关系。
- 外部联系人。
- 客户联系人。
- 拉黑关系。
- 联系人备注。
- 关系可见性。
- 组织内外的联系策略。
当申请、确认、拉黑、备注等规则开始独立变化,就有理由为关系建立单独模型。触发这件事的往往是新场景:客户联系人、跨企业好友、临时会话、合作伙伴联系人。每多一种关系形态,塞在账号上的那几个字段就更难解释一层。它可以先留在同一个部署单元,并不要求同时拆服务。
关系上下文不拥有账号,也不拥有组织成员。它只拥有“关系事实”。它可以引用账号 ID、成员 ID、外部联系人 ID,但不能修改账号状态或组织身份。
这里还要决定关系两端引用什么身份。跨企业好友可能关联账号,企业客户关系可能关联成员或外部联系人。直接统一成两个 user_id 会把这种差异藏起来,迁移时需要重新解释已有数据。
群治理、会话入口和消息分别怎样变化
协同办公 IM 中,“群聊”是最容易混淆的模型。
至少要区分三个概念:
- Group:群组本身,关注群资料、群成员、群主、管理员、入群规则、群公告、群治理。
- Conversation:参与通信的会话及其标识;用户侧的置顶、免打扰、草稿等通常是各自的 ConversationSetting,不能当作所有参与者共享的状态。
- Message:消息事实,关注发送、投递、撤回、已读、转发、引用、内容类型。
三者各自回答一个不同的问题:群组回答“这个群是什么、谁在里面、规则是什么”,会话回答“用户从哪个入口进入这段通信”,消息回答“这条消息如何成为事实并被投递”。把这三个问题混在一个模型里,任何一处调整都会牵动另外两处。
一个群可以对应一个群会话,单聊也可以有会话。这里把群治理和会话管理暂时放在同一候选边界,便于早期实现;如果会话列表、多端设置和同步规则独立变复杂,再评估拆出 Conversation。
如果把群、会话、消息都塞进一个 Messaging 模块,早期很快,但后期容易出现问题:
- 群治理规则和消息投递规则混在一起。
- 会话列表设置和群成员状态互相污染。
- 群成员变更频繁影响消息主链路。
- 大群消息扩散时,群聚合可能被错误放大。
例如修改群公告会影响共享群资料,置顶只影响某个用户的入口,撤回则改变消息可见状态。把这三个用例分别追踪一遍,比仅凭“都在聊天页面”决定模型归属更可靠。
消息先定义发送成功的含义
消息上下文通常是协同办公 IM 的核心域之一。
它关注的是:
- 消息 ID 和幂等。
- 消息内容类型。
- 消息发送状态。
- 撤回、编辑、引用、回复、转发。
- 已读未读。
- 多端同步游标。
- 离线补拉。
- 大群扇出。
- 加密消息和普通消息的差异。
飞书开放平台的 IM 消息 API 暴露了应用发送消息、消息内容、接收方类型等能力,可以作为公开能力观察入口。飞书发送消息 API
公开 API 并不揭示内部模型。本案例选择把消息持久化和派生动作分开:发送成功表示服务端接受并可靠保存消息,不表示所有终端已经收到,也不表示已读。
消息发送主链路应该尽量短:
- 鉴权和基础校验。
- 会话和群组规则校验。
- 消息幂等写入。
- 在同一事务中记录消息及待发布的
MessageSent事件。 - 返回发送结果。
通知、搜索、机器人和 AI 摘要可以随后异步处理。若审计要求业务成立时必须留下记录,应把必要记录纳入提交边界,再异步生成审计报表。
工作台是入口,流程和开放平台各有规则
协同办公产品的复杂度,往往从“聊天”扩展到“工作发生在哪里”。
工作台和流程上下文可能包含:
- 应用入口。
- 审批。
- 表单。
- 公告。
- 任务。
- 日程。
- 低代码应用。
开放平台上下文可能包含:
- 企业自建应用。
- 第三方应用。
- 机器人。
- Webhook。
- 事件订阅。
- API 凭证。
- 权限范围。
- 配额和审计。
飞书开放平台和钉钉开放平台都把开放能力作为重要入口,这说明协同办公 IM 不只是用户之间的通信系统,也是企业应用生态的承载层。飞书开放平台文档、钉钉开放平台文档
工作台负责展示和配置入口,不一定拥有审批、任务、日程的执行状态。如果审批有独立的节点、流转和撤销规则,就应有自己的模型。开放平台则负责应用授权、凭证、订阅和对外协议,通过接口或事件访问内部业务事实。这层关系适合用防腐层隔开:应用、授权范围和事件版本属于开放平台自己的模型,内部上下文不该把领域对象直接交给它对外发布;反过来,外部 API 的形态也不该渗进内部模型。
搜索、通知和治理不能按同一种方式接入
搜索、通知、安全治理这些能力经常被误放。
搜索不是业务事实源。消息、联系人、文档、应用才是事实源,搜索只是索引和查询视图。搜索索引可以按约定延迟、从事实源重建;索引中的过期权限却不能直接作为授权依据。撤权或消息撤回时,可能还需要查询时复核或过滤,避免索引滞后暴露内容。
通知也不是业务事实源。通知由业务事件触发,比如成员加入、审批通过、消息发送、文档分享。通知上下文负责偏好、渠道、频控、推送状态,不应该决定业务动作是否成立。
治理和安全更特殊。审计、风控、敏感词、水印、合规策略会影响多个域,但它们也不应该绕过业务模型直接改数据。更好的做法是:
- 风控前置参与决策。
- 审计通过事件记录事实。
- DLP 和水印通过策略影响文档、消息和文件上下文。
- 合规报表消费审计和业务事件。
安全治理这一行尤其需要继续拆解。发送前的风控判断和事后的审计报表,在延迟要求、失败处理和数据访问上都不同;把它们写进同一个候选域,不代表应该共用一条同步调用链。举个具体的:敏感词命中要在消息受理时拦住,这是同步判断,失败就得拒绝发送;审计报表可以等消息发出去几小时之后再生成,失败重试就好。硬把这两件事串成一条链,等于让报表的积压拖住发送。
用现有用例检验这张地图
如果从零设计一个协同办公 IM,不建议一开始就拆十几个微服务。更务实的做法是:
第一阶段,先在代码里建立模块边界:
- identity
- organization
- relationship
- group
- messaging
- notification
- workflow
- openplatform
- search
- governance
随后从实际负载、发布频率和隔离要求中找拆分点,下面这些模块值得评估,但没有固定顺序:
- messaging
- group
- openplatform
- search
- notification
例如搜索构建已影响消息进程内存,就有拆 worker 的具体理由;仅仅有一个 Search 方框还不够。边界评审可以先走“成员离职”“群主转让”“撤回消息”三条流程,检查写入口、权限与失败补偿是否都有归属,再确定哪些部署变化值得做。
这三条流程各自照出一类问题。“成员离职”照的是传播延迟:组织发出事件之后,群组、消息、搜索分别在什么时间点失效?如果索引允许延迟五分钟,这五分钟里离职者还能不能搜到原企业的消息?“群主转让”照的是不变量归属:旧群主、新群主和群资料上的 ownerID 必须在一次事务里改完,这件事到底写在哪个上下文里?“撤回消息”照的是事件顺序:撤回事件先于发送事件到达索引端时,索引得能识别出来并拒绝用旧事件把内容补回去。
三条走完,通常会发现几处责任悬空——没人说得出该由谁保证。悬空的那些位置,就是这张地图还要修的地方。
接下来打算专门讨论最容易争议的边界:账号、组织、关系、群组和会话。