[{"data":1,"prerenderedAt":665},["ShallowReactive",2],{"blog-ddd-im-message-notification-search-open-platform":3},{"id":4,"title":5,"body":6,"category":649,"date":650,"description":205,"extension":651,"meta":652,"navigation":653,"path":654,"seo":655,"series":656,"seriesOrder":657,"stem":658,"tags":659,"__hash__":664},"blog\u002Fblog\u002Fddd-im-message-notification-search-open-platform.md","DDD 实战（六）：消息、通知、搜索、开放平台的跨域协作",{"type":7,"value":8,"toc":636},"minimark",[9,13,28,31,34,37,74,77,80,85,93,107,110,113,116,119,123,126,155,158,161,183,186,190,195,198,208,211,214,217,221,224,250,253,273,276,279,282,293,296,300,303,306,329,332,335,352,359,362,366,369,372,403,406,412,418,421,444,447,451,454,457,460,474,477,480,484,487,490,507,510,524,527,530,534,537,540,557,560,579,582,585,589,592,598,601,621,624,627,630],[10,11,5],"h1",{"id":12},"ddd-实战六消息通知搜索开放平台的跨域协作",[14,15,16],"blockquote",{},[17,18,19,20,27],"p",{},"推荐 ",[21,22,26],"a",{"href":23,"rel":24},"https:\u002F\u002Fbewild.ai?code=BYZDOTME",[25],"nofollow","BeWild"," 代充 Codex ，工作猛猛提效。",[17,29,30],{},"如果说账号、组织、关系、群组的难点是“边界怎么切”，那么消息、通知、搜索、开放平台的难点就是“边界切开以后怎么协作”。",[17,32,33],{},"协同办公 IM 的消息发送看似只有一个动作：用户发出一条消息。",[17,35,36],{},"但在真实系统里，一条消息可能触发很多后续动作：",[38,39,40,44,47,50,53,56,59,62,65,68,71],"ul",{},[41,42,43],"li",{},"写入消息事实。",[41,45,46],{},"更新会话列表。",[41,48,49],{},"计算未读数。",[41,51,52],{},"多端同步。",[41,54,55],{},"离线推送。",[41,57,58],{},"更新搜索索引。",[41,60,61],{},"触发机器人。",[41,63,64],{},"推送开放平台事件。",[41,66,67],{},"记录审计日志。",[41,69,70],{},"触发风控策略。",[41,72,73],{},"生成 AI 摘要。",[17,75,76],{},"如果把这些动作都放进一个同步事务，消息链路会越来越慢，也越来越脆。",[17,78,79],{},"DDD 的做法不是把所有东西拆开以后互相 RPC，而是识别核心事实和派生动作，用领域事件组织跨上下文协作。",[81,82,84],"h2",{"id":83},"_1-公开能力能观察到什么","1. 公开能力能观察到什么",[17,86,87,88],{},"飞书开放平台提供 IM 消息相关 API，应用可以向用户、群组等目标发送消息。",[21,89,92],{"href":90,"rel":91},"https:\u002F\u002Fopen.feishu.cn\u002Fdocument\u002Fserver-docs\u002Fim-v1\u002Fmessage\u002Fcreate",[25],"飞书发送消息 API",[17,94,95,96,101,102],{},"飞书和钉钉开放平台也都提供事件订阅、机器人、通讯录、审批等开放能力入口。",[21,97,100],{"href":98,"rel":99},"https:\u002F\u002Fopen.feishu.cn\u002Fdocument\u002Fhome\u002Findex",[25],"飞书开放平台文档","、",[21,103,106],{"href":104,"rel":105},"https:\u002F\u002Fopen.dingtalk.com\u002Fdocument\u002F",[25],"钉钉开放平台文档",[17,108,109],{},"这些公开资料能说明两件事：",[17,111,112],{},"第一，成熟协同办公 IM 会把消息能力开放给内部应用和外部应用。消息不只是用户手动发送，也可能来自机器人、审批、系统通知、第三方应用。",[17,114,115],{},"第二，消息会和很多平台能力协作。开放平台需要事件，机器人需要回调，通知需要投递，搜索需要索引，审计需要记录。",[17,117,118],{},"但公开资料不能说明厂商内部如何拆服务、如何设计表、如何投递事件。因此本文只基于公开能力推导一种合理架构。",[81,120,122],{"id":121},"_2-消息发送主链路应该尽量短","2. 消息发送主链路应该尽量短",[17,124,125],{},"一个稳健的消息发送主链路，可以控制在这些步骤内：",[127,128,129,132,135,138,141,144,152],"ol",{},[41,130,131],{},"接口层接收请求，解析发送者、租户、设备和客户端消息 ID。",[41,133,134],{},"Identity & Access 校验登录态和设备状态。",[41,136,137],{},"Group & Conversation 校验会话是否存在、成员是否可发送、群是否禁言。",[41,139,140],{},"Governance & Security 做必要的前置策略校验，比如敏感词、频率限制、内容大小。",[41,142,143],{},"Messaging 使用客户端消息 ID 做幂等，写入消息事实。",[41,145,146,147,151],{},"Messaging 记录 ",[148,149,150],"code",{},"MessageSent"," 领域事件或写入 outbox。",[41,153,154],{},"返回发送成功或明确失败。",[17,156,157],{},"这个链路的目标，是让“消息成为事实”这件事尽快完成。",[17,159,160],{},"下面这些动作，不应该阻塞主链路：",[38,162,163,165,168,171,174,177,180],{},[41,164,55],{},[41,166,167],{},"搜索索引。",[41,169,170],{},"机器人回调。",[41,172,173],{},"开放平台事件投递。",[41,175,176],{},"AI 摘要。",[41,178,179],{},"消息审计报表。",[41,181,182],{},"未读数的复杂聚合。",[17,184,185],{},"它们重要，但不是消息写入事实的前置条件。",[81,187,189],{"id":188},"_3-messagesent-是业务事实不是技术通知","3. MessageSent 是业务事实，不是技术通知",[17,191,192,194],{},[148,193,150],{}," 应该表达一个已经发生的业务事实：某个发送者在某个会话里成功发送了一条消息。",[17,196,197],{},"事件内容可以包含：",[199,200,206],"pre",{"className":201,"code":203,"language":204,"meta":205},[202],"language-text","MessageSent\n├── event_id\n├── occurred_at\n├── tenant_id\n├── conversation_id\n├── message_id\n├── sender_id\n├── message_type\n├── content_digest\n└── visibility_scope\n","text","",[148,207,203],{"__ignoreMap":205},[17,209,210],{},"注意，它不应该包含所有消息正文和所有下游所需字段。",[17,212,213],{},"领域事件要在“足够表达事实”和“不泄漏领域内部模型”之间取平衡。搜索需要正文时，可以根据 message ID 回查消息上下文，或者由消息上下文发布面向索引的受控事件。开放平台只需要部分字段时，就不应该把内部敏感字段暴露出去。",[17,215,216],{},"事件设计得越像数据库行复制，边界越容易被破坏。",[81,218,220],{"id":219},"_4-通知上下文投递是派生能力","4. 通知上下文：投递是派生能力",[17,222,223],{},"通知上下文消费各种领域事件：",[38,225,226,230,235,240,245],{},[41,227,228],{},[148,229,150],{},[41,231,232],{},[148,233,234],{},"GroupMemberJoined",[41,236,237],{},[148,238,239],{},"ApprovalCompleted",[41,241,242],{},[148,243,244],{},"DocumentShared",[41,246,247],{},[148,248,249],{},"OrganizationMemberRemoved",[17,251,252],{},"它负责把业务事实转成用户可感知的提醒：",[38,254,255,258,261,264,267,270],{},[41,256,257],{},"站内通知。",[41,259,260],{},"移动端 Push。",[41,262,263],{},"桌面端提醒。",[41,265,266],{},"邮件提醒。",[41,268,269],{},"短信提醒。",[41,271,272],{},"应用内红点。",[17,274,275],{},"通知上下文拥有的是通知偏好、投递任务、投递状态、频控规则。它不拥有消息事实，也不应该决定消息是否发送成功。",[17,277,278],{},"这里有一个重要 trade off：未读数应该归消息域还是通知域？",[17,280,281],{},"我的建议是分层看：",[38,283,284,287,290],{},[41,285,286],{},"会话内的读状态和消息游标，属于 Messaging 或 Conversation。",[41,288,289],{},"跨应用红点、桌面提醒、Push 状态，属于 Notification。",[41,291,292],{},"页面上展示的总未读数，可以是查询侧组合视图。",[17,294,295],{},"不要为了一个 UI 数字，把消息和通知强行揉成一个模型。",[81,297,299],{"id":298},"_5-搜索上下文索引不是事实源","5. 搜索上下文：索引不是事实源",[17,301,302],{},"搜索上下文最容易被误用。",[17,304,305],{},"用户希望全局搜索：",[38,307,308,311,314,317,320,323,326],{},[41,309,310],{},"联系人。",[41,312,313],{},"群。",[41,315,316],{},"消息。",[41,318,319],{},"文档。",[41,321,322],{},"应用。",[41,324,325],{},"审批。",[41,327,328],{},"机器人。",[17,330,331],{},"于是很多系统会让搜索服务直接读各业务库，甚至反向修改业务状态。这会让搜索服务变成隐形中心。",[17,333,334],{},"更合理的方式是：",[127,336,337,340,343,346,349],{},[41,338,339],{},"各业务上下文发布领域事件。",[41,341,342],{},"搜索上下文消费事件，构建自己的索引文档。",[41,344,345],{},"索引文档只作为查询视图。",[41,347,348],{},"权限过滤必须继承来源上下文的权限规则。",[41,350,351],{},"索引可删除、可重建、可延迟，但不能成为事实源。",[17,353,354,355,358],{},"例如消息被撤回后，Messaging 发布 ",[148,356,357],{},"MessageRecalled","。Search 消费事件后更新或删除索引。即使搜索索引延迟，消息事实仍然以 Messaging 为准。",[17,360,361],{},"这也是为什么搜索适合最终一致。要求每条消息写入后立刻被全局搜索命中，通常会显著拖慢主链路。",[81,363,365],{"id":364},"_6-开放平台上下文隔离外部复杂度","6. 开放平台上下文：隔离外部复杂度",[17,367,368],{},"开放平台是协同办公 IM 的生态入口，但它不应该侵入内部领域模型。",[17,370,371],{},"开放平台上下文通常负责：",[38,373,374,377,380,383,386,388,391,394,397,400],{},[41,375,376],{},"应用注册。",[41,378,379],{},"API 凭证。",[41,381,382],{},"OAuth 授权。",[41,384,385],{},"权限范围。",[41,387,328],{},[41,389,390],{},"Webhook。",[41,392,393],{},"事件订阅。",[41,395,396],{},"API 配额。",[41,398,399],{},"开发者审计。",[41,401,402],{},"回调重试。",[17,404,405],{},"它和内部领域的关系，应该是“授权访问 + 事件订阅 + 防腐层”。",[17,407,408,409,411],{},"例如内部 Messaging 发布 ",[148,410,150],{},"，Open Platform 不应该直接把这个内部事件原样推给第三方应用。它应该转换成开放平台的发布语言：",[199,413,416],{"className":414,"code":415,"language":204,"meta":205},[202],"Internal MessageSent\n        ↓\nOpen Platform Event Translator\n        ↓\nExternal im.message.receive_v1\n",[148,417,415],{"__ignoreMap":205},[17,419,420],{},"转换过程中要处理：",[38,422,423,426,429,432,435,438,441],{},[41,424,425],{},"租户隔离。",[41,427,428],{},"应用权限。",[41,430,431],{},"字段脱敏。",[41,433,434],{},"事件版本。",[41,436,437],{},"回调签名。",[41,439,440],{},"失败重试。",[41,442,443],{},"频率限制。",[17,445,446],{},"这就是防腐层的价值。它防止外部 API 形态反向污染内部领域模型，也防止内部模型变化直接破坏开发者生态。",[81,448,450],{"id":449},"_7-机器人看似消息实则跨域","7. 机器人：看似消息，实则跨域",[17,452,453],{},"机器人是一个很好的边界案例。",[17,455,456],{},"机器人可以发送消息，所以它看起来属于 Messaging。机器人有凭证、回调和权限，所以它又像 Open Platform。机器人可能绑定群，所以它也和 Group 有关系。机器人还可能响应审批、文档、告警事件。",[17,458,459],{},"更合理的拆法是：",[38,461,462,465,468,471],{},[41,463,464],{},"Open Platform 拥有 Bot、AppCredential、WebhookSubscription。",[41,466,467],{},"Group 拥有“某机器人是否被添加到群”的群内关系。",[41,469,470],{},"Messaging 只接受一个经过授权的发送主体，写入消息事实。",[41,472,473],{},"Governance 负责风控、频控和审计策略。",[17,475,476],{},"这样设计后，“机器人发消息”不是一个特殊消息链路，而是一个经过开放平台授权的发送用例。",[17,478,479],{},"这能避免 Messaging 被开放平台概念污染，也能避免 Open Platform 直接改消息表。",[81,481,483],{"id":482},"_8-事件驱动不是到处发-mq","8. 事件驱动不是到处发 MQ",[17,485,486],{},"领域事件很有用，但它不是万能药。",[17,488,489],{},"适合事件驱动的场景通常具备这些特征：",[38,491,492,495,498,501,504],{},[41,493,494],{},"事件表示已经发生的业务事实。",[41,496,497],{},"订阅方可以异步处理。",[41,499,500],{},"短暂延迟业务可接受。",[41,502,503],{},"消费失败可以重试或补偿。",[41,505,506],{},"发布方不需要知道所有订阅方。",[17,508,509],{},"不适合事件驱动的场景包括：",[38,511,512,515,518,521],{},[41,513,514],{},"必须在当前请求内给出确定结果。",[41,516,517],{},"必须强一致校验的业务不变量。",[41,519,520],{},"失败后不能重试也不能补偿。",[41,522,523],{},"事件只是为了绕开清晰接口。",[17,525,526],{},"例如“成员是否有权限发送群消息”通常不能异步事件化。发送前就要校验。否则用户会看到发送成功，几秒后又被撤回，体验和一致性都很差。",[17,528,529],{},"但“消息发送后更新搜索索引”适合事件化。搜索延迟几秒通常可以接受，失败也可以重试。",[81,531,533],{"id":532},"_9-一致性策略先分清强一致和最终一致","9. 一致性策略：先分清强一致和最终一致",[17,535,536],{},"跨域协作最大的难点，是一致性。",[17,538,539],{},"协同办公 IM 中，通常需要强一致的有：",[38,541,542,545,548,551,554],{},[41,543,544],{},"同一消息 ID 的幂等写入。",[41,546,547],{},"群成员权限和发送资格的关键校验。",[41,549,550],{},"群主唯一性。",[41,552,553],{},"撤回权限校验。",[41,555,556],{},"账号禁用后的关键操作拦截。",[17,558,559],{},"可以最终一致的有：",[38,561,562,565,567,569,571,574,576],{},[41,563,564],{},"搜索索引更新。",[41,566,55],{},[41,568,170],{},[41,570,173],{},[41,572,573],{},"审计报表。",[41,575,176],{},[41,577,578],{},"全局未读聚合视图。",[17,580,581],{},"DDD 不要求所有东西都最终一致。它要求你把一致性边界说清楚。",[17,583,584],{},"强一致的规则应该留在聚合和必要的同步校验里。最终一致的派生能力，应该通过领域事件、outbox、幂等消费和补偿任务完成。",[81,586,588],{"id":587},"_10-推荐的跨域协作链路","10. 推荐的跨域协作链路",[17,590,591],{},"一条消息发送后的推荐协作链路可以这样设计：",[199,593,596],{"className":594,"code":595,"language":204,"meta":205},[202],"Client\n  ↓\nMessaging Application Service\n  ↓ 校验\nIdentity \u002F Group \u002F Governance\n  ↓ 写入\nMessage Aggregate + Outbox\n  ↓ 发布\nMessageSent\n  ↓\nNotification \u002F Search \u002F Open Platform \u002F Audit \u002F AI\n",[148,597,595],{"__ignoreMap":205},[17,599,600],{},"其中关键规则是：",[38,602,603,606,609,612,615,618],{},[41,604,605],{},"Messaging 拥有消息事实。",[41,607,608],{},"Notification 拥有通知任务。",[41,610,611],{},"Search 拥有索引视图。",[41,613,614],{},"Open Platform 拥有外部应用交互。",[41,616,617],{},"Audit 拥有审计追加记录。",[41,619,620],{},"AI 拥有异步分析任务。",[17,622,623],{},"每个上下文都可以失败、重试和降级，但不能反向篡改消息事实。",[17,625,626],{},"这就是 DDD 在复杂 IM 平台中的实际价值：它不是把系统拆散，而是让每个模块知道自己拥有的事实、消费的事实和不该越界的事实。",[17,628,629],{},"下一篇我们把领域模型继续往下推，看它如何影响部署模块：单体、模块化单体、微服务和 monorepo 到底怎么选。",[17,631,632],{},[21,633,635],{"href":634},"\u002Fblog\u002F","返回博客列表",{"title":205,"searchDepth":637,"depth":637,"links":638},2,[639,640,641,642,643,644,645,646,647,648],{"id":83,"depth":637,"text":84},{"id":121,"depth":637,"text":122},{"id":188,"depth":637,"text":189},{"id":219,"depth":637,"text":220},{"id":298,"depth":637,"text":299},{"id":364,"depth":637,"text":365},{"id":449,"depth":637,"text":450},{"id":482,"depth":637,"text":483},{"id":532,"depth":637,"text":533},{"id":587,"depth":637,"text":588},"架构设计","2026-06-26","md",{},true,"\u002Fblog\u002Fddd-im-message-notification-search-open-platform",{"title":5,"description":205},"DDD 实战",6,"blog\u002Fddd-im-message-notification-search-open-platform",[660,661,662,663],"DDD","领域事件","事件驱动","IM","5vn88qWWWilDQPqlUM1Ioy5R-6rN8O93YeHq6coPfoA",1783309725604]