[{"data":1,"prerenderedAt":642},["ShallowReactive",2],{"blog-ddd-im-account-organization-relationship-group":3},{"id":4,"title":5,"body":6,"category":626,"date":627,"description":114,"extension":628,"meta":629,"navigation":630,"path":631,"seo":632,"series":633,"seriesOrder":634,"stem":635,"tags":636,"__hash__":641},"blog\u002Fblog\u002Fddd-im-account-organization-relationship-group.md","DDD 实战（五）：账号、组织、关系、群组的边界取舍",{"type":7,"value":8,"toc":614},"minimark",[9,13,28,31,34,37,40,43,46,51,54,61,67,73,79,85,91,94,97,101,104,107,118,121,140,143,146,163,166,170,173,179,182,249,252,266,269,283,286,290,293,296,302,305,328,331,345,348,365,368,372,375,378,381,398,401,418,421,425,428,431,434,437,440,443,457,460,464,467,470,478,481,498,501,518,521,525,528,531,554,557,560,564,567,573,576,593,596,599,605,608],[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],{},"DDD 讨论中最有价值的部分，往往不是画出一张漂亮的领域图，而是在争议点上说清楚取舍。",[17,32,33],{},"账号、组织、好友关系、群聊关系，就是协同办公 IM 里最容易产生争议的一组边界。",[17,35,36],{},"有人会说：账号、好友、群成员都是“人和人之间的关系”，应该放在一个用户中心里。",[17,38,39],{},"也有人会说：组织成员和好友关系是两种完全不同的关系，必须拆开。",[17,41,42],{},"还有人会说：群聊是消息的一部分，群成员关系应该归消息域。",[17,44,45],{},"这些说法都不是完全错误。DDD 没有一个脱离业务阶段和组织约束的标准答案。真正重要的是：每种划分方案解决了什么问题，又制造了什么成本。",[47,48,50],"h2",{"id":49},"_1-先把几个词拆开","1. 先把几个词拆开",[17,52,53],{},"开始讨论边界之前，先把常见词拆开。",[17,55,56,60],{},[57,58,59],"strong",{},"Account","：可登录系统的账号。它关注登录、凭证、设备、安全策略。",[17,62,63,66],{},[57,64,65],{},"Member","：某个账号在某个企业或租户下的组织身份。它关注部门、岗位、角色、在职状态、通讯录可见性。",[17,68,69,72],{},[57,70,71],{},"Contact","：某个主体可联系的对象。它关注联系人列表、备注、关系来源、可见范围。",[17,74,75,78],{},[57,76,77],{},"Friend","：一种双向或经过确认的关系。它关注好友申请、通过、拒绝、删除、拉黑。",[17,80,81,84],{},[57,82,83],{},"ExternalContact","：企业外部的联系人，可能是客户、供应商、合作伙伴。",[17,86,87,90],{},[57,88,89],{},"GroupMember","：某个成员在某个群里的身份。它关注群昵称、群角色、禁言状态、入群方式。",[17,92,93],{},"这些词背后都可能指向同一个自然人，但它们属于不同业务语境。",[17,95,96],{},"如果团队不能先在语言层面拆开，代码层面的拆分一定会吵不清楚。",[47,98,100],{"id":99},"_2-方案一全部放进用户中心","2. 方案一：全部放进用户中心",[17,102,103],{},"最简单的方案，是把账号、组织成员、好友关系、群成员都放进一个用户中心。",[17,105,106],{},"这种方案通常长这样：",[108,109,115],"pre",{"className":110,"code":112,"language":113,"meta":114},[111],"language-text","user-center\n├── account\n├── member\n├── friend\n├── contact\n└── group-member\n","text","",[116,117,112],"code",{"__ignoreMap":114},[17,119,120],{},"它的优点很明显：",[122,123,124,128,131,134,137],"ul",{},[125,126,127],"li",{},"早期实现快。",[125,129,130],{},"查询链路短。",[125,132,133],{},"事务简单。",[125,135,136],{},"团队理解成本低。",[125,138,139],{},"一个接口就能拿到“用户全部信息”。",[17,141,142],{},"对于早期产品，或者团队规模很小、业务规则很简单的系统，这个方案并不荒唐。",[17,144,145],{},"问题在于，它的长期代价也很明显：",[122,147,148,151,154,157,160],{},[125,149,150],{},"用户中心会持续膨胀。",[125,152,153],{},"任何关系规则变化都会修改用户中心。",[125,155,156],{},"组织、好友、群成员的生命周期纠缠在一起。",[125,158,159],{},"下游所有模块都依赖用户中心。",[125,161,162],{},"用户中心故障会影响登录、组织、消息、群聊和联系人。",[17,164,165],{},"这类模块最后很容易变成“上帝模块”。它不是因为设计者水平差，而是因为早期为了简单把太多语义塞进了一个边界。",[47,167,169],{"id":168},"_3-方案二账号组织关系群组分开","3. 方案二：账号、组织、关系、群组分开",[17,171,172],{},"更符合中大型协同办公 IM 长期演进的方案，是拆成四个上下文：",[108,174,177],{"className":175,"code":176,"language":113,"meta":114},[111],"Identity & Access\nTenant & Organization\nContact & Relationship\nGroup & Conversation\n",[116,178,176],{"__ignoreMap":114},[17,180,181],{},"各自职责如下：",[183,184,185,201],"table",{},[186,187,188],"thead",{},[189,190,191,195,198],"tr",{},[192,193,194],"th",{},"上下文",[192,196,197],{},"拥有的事实",[192,199,200],{},"不拥有的事实",[202,203,204,216,227,238],"tbody",{},[189,205,206,210,213],{},[207,208,209],"td",{},"Identity & Access",[207,211,212],{},"账号、凭证、设备、登录会话",[207,214,215],{},"部门、好友、群成员",[189,217,218,221,224],{},[207,219,220],{},"Tenant & Organization",[207,222,223],{},"企业、部门、成员、组织角色",[207,225,226],{},"登录凭证、好友申请、群治理",[189,228,229,232,235],{},[207,230,231],{},"Contact & Relationship",[207,233,234],{},"好友、联系人、外部联系人、黑名单",[207,236,237],{},"账号安全、部门树、群公告",[189,239,240,243,246],{},[207,241,242],{},"Group & Conversation",[207,244,245],{},"群、群成员、群角色、会话设置",[207,247,248],{},"消息正文、组织主数据、好友主关系",[17,250,251],{},"这个方案的好处是边界更清晰：",[122,253,254,257,260,263],{},[125,255,256],{},"账号安全策略变化，不影响好友模型。",[125,258,259],{},"组织架构调整，不直接修改群成员模型。",[125,261,262],{},"群治理增强，不把消息域拖复杂。",[125,264,265],{},"关系模型可以独立支持好友、客户、外部联系人。",[17,267,268],{},"代价也很明确：",[122,270,271,274,277,280],{},[125,272,273],{},"查询需要跨上下文组合。",[125,275,276],{},"需要事件同步或缓存快照。",[125,278,279],{},"一些流程从单事务变成最终一致。",[125,281,282],{},"开发团队必须理解上下文之间的边界。",[17,284,285],{},"这个方案适合中长期产品，因为它把变化点拆开了。但如果团队没有工程纪律，只是把表拆到不同服务里，调用又互相穿透，那它只会增加复杂度。",[47,287,289],{"id":288},"_4-方案三抽象成统一关系图谱","4. 方案三：抽象成统一关系图谱",[17,291,292],{},"还有一种方案，是建立一个统一的 Relationship Graph，把组织关系、好友关系、群成员关系都抽象成“主体之间的边”。",[17,294,295],{},"例如：",[108,297,300],{"className":298,"code":299,"language":113,"meta":114},[111],"Subject --[relation_type]--> Subject\n",[116,301,299],{"__ignoreMap":114},[17,303,304],{},"关系类型可以是：",[122,306,307,310,313,316,319,322,325],{},[125,308,309],{},"belongs_to_department",[125,311,312],{},"reports_to",[125,314,315],{},"friend_with",[125,317,318],{},"blocks",[125,320,321],{},"member_of_group",[125,323,324],{},"admin_of_group",[125,326,327],{},"external_contact_of",[17,329,330],{},"这个方案在模型上很诱人。它适合这些场景：",[122,332,333,336,339,342],{},[125,334,335],{},"产品高度依赖关系查询。",[125,337,338],{},"权限判断可以抽象成关系图遍历。",[125,340,341],{},"需要统一表达组织、社交、客户、群组等关系。",[125,343,344],{},"团队有能力维护图模型和查询引擎。",[17,346,347],{},"但它的风险也很大：",[122,349,350,353,356,359,362],{},[125,351,352],{},"抽象过早。",[125,354,355],{},"业务语言被技术图模型吞掉。",[125,357,358],{},"不同关系的生命周期差异被掩盖。",[125,360,361],{},"群治理、组织管理、好友申请的规则很难统一表达。",[125,363,364],{},"普通业务开发理解成本高。",[17,366,367],{},"所以我不建议把统一关系图谱作为第一阶段主模型。它可以作为搜索、推荐、权限分析、组织洞察等派生能力存在，但不应该替代各个业务上下文的事实模型。",[47,369,371],{"id":370},"_5-账号和组织的关键取舍","5. 账号和组织的关键取舍",[17,373,374],{},"账号和组织最容易被合并，因为它们都围绕“人”。",[17,376,377],{},"但在企业协作产品里，账号和成员的生命周期不一致。",[17,379,380],{},"账号可能长期存在，成员身份可能随企业变化：",[122,382,383,386,389,392,395],{},[125,384,385],{},"一个账号加入多个企业。",[125,387,388],{},"一个账号从企业 A 离职，但仍在企业 B 有成员身份。",[125,390,391],{},"一个成员被禁用，但账号仍可登录个人空间。",[125,393,394],{},"一个成员转岗，只影响组织上下文。",[125,396,397],{},"一个账号设备异常，只影响身份安全上下文。",[17,399,400],{},"因此，我更倾向于：",[122,402,403,406,409,412,415],{},[125,404,405],{},"Account 属于 Identity & Access。",[125,407,408],{},"Member 属于 Tenant & Organization。",[125,410,411],{},"Account 和 Member 用稳定 ID 关联。",[125,413,414],{},"组织上下文不直接修改登录凭证。",[125,416,417],{},"身份上下文不直接维护部门树。",[17,419,420],{},"这样切的代价是多了一层关联和同步，但收益是长期模型更稳定。",[47,422,424],{"id":423},"_6-组织和关系的关键取舍","6. 组织和关系的关键取舍",[17,426,427],{},"组织关系和好友关系也容易混在一起，因为它们都可以出现在通讯录里。",[17,429,430],{},"但组织通讯录和好友关系的业务含义不同。",[17,432,433],{},"组织通讯录来自企业管理。它通常由管理员、HR 系统或组织同步流程维护。成员之间是否可见，取决于组织策略、部门权限、字段脱敏规则。",[17,435,436],{},"好友关系来自个人或业务互动。它可能需要申请、同意、拒绝、删除、拉黑、备注、分组。",[17,438,439],{},"如果把组织成员和好友关系都放在通讯录模块里，通讯录会变得很难定义。它到底是组织事实源，还是个人关系事实源，还是搜索视图？",[17,441,442],{},"更好的方式是：",[122,444,445,448,451,454],{},[125,446,447],{},"Tenant & Organization 拥有组织成员事实。",[125,449,450],{},"Contact & Relationship 拥有好友、联系人、外部联系人事实。",[125,452,453],{},"通讯录页面只是一个组合视图。",[125,455,456],{},"搜索上下文可以建立联系人索引，但不拥有关系事实。",[17,458,459],{},"这样一来，“通讯录”不再是一个大领域，而是多个领域在用户界面上的聚合入口。",[47,461,463],{"id":462},"_7-群组和消息的关键取舍","7. 群组和消息的关键取舍",[17,465,466],{},"群组和消息也容易混在一起。",[17,468,469],{},"如果从用户体验看，群就是一个聊天窗口。但从领域模型看，群至少有两部分：",[122,471,472,475],{},[125,473,474],{},"群组事实：群资料、群成员、群角色、入群规则、群公告、群治理。",[125,476,477],{},"消息事实：消息内容、发送状态、撤回、已读、转发、引用。",[17,479,480],{},"群组上下文应该回答：",[122,482,483,486,489,492,495],{},[125,484,485],{},"这个群是否存在？",[125,487,488],{},"这个成员是否在群里？",[125,490,491],{},"这个成员是否能发言？",[125,493,494],{},"这个成员是否能邀请别人？",[125,496,497],{},"谁可以修改群公告？",[17,499,500],{},"消息上下文应该回答：",[122,502,503,506,509,512,515],{},[125,504,505],{},"这条消息是否已写入？",[125,507,508],{},"这条消息是否已撤回？",[125,510,511],{},"哪些成员已读？",[125,513,514],{},"客户端如何补拉？",[125,516,517],{},"消息如何投递到多端？",[17,519,520],{},"两者协作时，消息发送可以调用群组上下文校验发送资格，也可以消费群组上下文的成员快照。但消息不应该直接维护群成员主数据。",[47,522,524],{"id":523},"_8-边界会随着业务变化而变化","8. 边界会随着业务变化而变化",[17,526,527],{},"今天合理的领域划分，几年后可能变得不合理。这不是 DDD 失败，而是业务演进的结果。",[17,529,530],{},"出现这些信号时，就应该重新审视边界：",[122,532,533,536,539,542,545,548,551],{},[125,534,535],{},"某个上下文经常因为另一个上下文的需求而修改。",[125,537,538],{},"一个模型里的字段大多只服务少数场景。",[125,540,541],{},"事务边界越来越难维持。",[125,543,544],{},"模块之间出现大量互相读写数据库。",[125,546,547],{},"团队职责和代码边界不一致。",[125,549,550],{},"某个模块的发布频率明显高于其他模块。",[125,552,553],{},"某个模块的故障影响范围远超它的业务职责。",[17,555,556],{},"比如早期好友关系放在账号模块里没有问题。但当好友申请、外部联系人、客户关系、黑名单、跨企业协作都出现后，就应该把关系上下文独立出来。",[17,558,559],{},"再比如早期群组和消息在一个模块里没有问题。但当群治理、群机器人、超大群、群权限、群文件、群公告都变复杂后，就应该重新拆分群组上下文。",[47,561,563],{"id":562},"_9-我的推荐基线","9. 我的推荐基线",[17,565,566],{},"如果目标是中大型协同办公 IM，我会选择一个相对稳健的基线：",[108,568,571],{"className":569,"code":570,"language":113,"meta":114},[111],"Identity & Access\nTenant & Organization\nContact & Relationship\nGroup & Conversation\nMessaging\n",[116,572,570],{"__ignoreMap":114},[17,574,575],{},"其中：",[122,577,578,581,584,587,590],{},[125,579,580],{},"Account 不等于 Member。",[125,582,583],{},"Member 不等于 Contact。",[125,585,586],{},"Contact 不等于 GroupMember。",[125,588,589],{},"Group 不等于 Conversation。",[125,591,592],{},"Conversation 不等于 Message。",[17,594,595],{},"这不是唯一答案，但它能给长期演进留出空间。",[17,597,598],{},"对于工程落地，可以先不拆成五个服务。可以先在一个代码仓库里拆成五个模块，建立清晰的包边界、接口边界和数据所有权。等吞吐、团队、发布频率和故障隔离真的需要，再拆部署。",[17,600,601,602],{},"DDD 的现实主义在这里：",[57,603,604],{},"模型边界要比部署边界先出现。",[17,606,607],{},"下一篇我们继续看更复杂的跨域协作：消息、通知、搜索和开放平台如何通过领域事件协同。",[17,609,610],{},[21,611,613],{"href":612},"\u002Fblog\u002F","返回博客列表",{"title":114,"searchDepth":615,"depth":615,"links":616},2,[617,618,619,620,621,622,623,624,625],{"id":49,"depth":615,"text":50},{"id":99,"depth":615,"text":100},{"id":168,"depth":615,"text":169},{"id":288,"depth":615,"text":289},{"id":370,"depth":615,"text":371},{"id":423,"depth":615,"text":424},{"id":462,"depth":615,"text":463},{"id":523,"depth":615,"text":524},{"id":562,"depth":615,"text":563},"架构设计","2026-06-26","md",{},true,"\u002Fblog\u002Fddd-im-account-organization-relationship-group",{"title":5,"description":114},"DDD 实战",5,"blog\u002Fddd-im-account-organization-relationship-group",[637,638,639,640],"DDD","限界上下文","IM","架构取舍","T4gwIzNOfHtPjfTdfxzrhQYRurY8pqquGyCTrYiZUiQ",1784193099114]