资讯动态

领域驱动设计实战:返利系统聚合根与领域事件建模

发布时间:2026/10/14 22:36:45 来源:尧图企业网站定制
1. 项目缘起为什么返利系统需要领域驱动设计做电商返利系统很多人第一反应是“这不就是一套分润计算逻辑嘛”觉得几个表、几个接口就能搞定。但真正深入到业务里你会发现返利系统的复杂程度远超想象商品佣金比例随时调整、订单状态频繁流转、用户提现与风控规则交织、平台结算周期和商家账期错位……如果一开始没有把领域边界划清楚后续每一次规则迭代都可能牵一发动全身。我经历过一个真实的项目某电商导购平台的返利模块初期用传统三层架构快速上线半年后代码里到处都是if (orderStatus 3)这种硬编码判断佣金计算散落在Service层各个角落定时任务、消息消费者、接口调用各自维护一套状态流转逻辑。最痛的一次是结算规则调整改一个佣金比例参数结果报表数据对不上、用户提现被卡、商家结算重复打款三个线上事故连着来。复盘时大家达成一个共识不是开发能力不行是领域模型压根没建起来业务规则没有归属感自然就到处乱飞。那之后我们决定用领域驱动设计DDD对返利系统做一次重新建模。这篇文章不会讲DDD的理论教条而是聚焦整个系统中最关键的两个设计决策核心聚合根的划分和领域事件的定义与落地。如果你正在设计类似的返利、分销、分账系统或者正在为已有系统的腐化代码发愁这篇文章里的思路、代码示例和踩坑记录应该能帮你少走不少弯路。2. 领域拆解先用事件风暴把业务链路摊开2.1 返利系统的三条核心业务链路在划分聚合根之前必须先把返利系统的业务链路完全理清楚。我习惯用事件风暴的方式拉着运营、产品、财务、技术一起过流程。不要只画用例图而是把“发生了什么”逐个列出来。返利系统大致能梳理出三条主链路下单返利链路用户通过推广链接下单 → 电商平台回传订单待付款/已付款 → 订单状态变更确认收货 → 返利计算 → 返利入账 → 用户可提现。提现链路用户发起提现 → 校验可提现余额与风控规则 → 创建提现单 → 人工/自动审核 → 打款 → 结果回传 → 账户余额扣减。商品与佣金管理链路运营配置商品推广计划 → 商家调整佣金比例 → 商品类目变更 → 佣金策略版本更新 → 影响后续新订单的返利计算。三条链路不是孤立的它们通过一个核心概念交汇——返利订单。返利订单承载了用户、商品、电商订单号、佣金比例、状态流转、结算结果等几乎全部关键信息。所以接下来的聚合根设计一定是围绕返利订单展开的。2.2 聚合边界划分的核心原则DDD里聚合根的划分原则其实就一句话在一个事务边界内部保证业务一致性。但落地时很多人会犯错——把“感觉上相关”的东西全塞进一个聚合里结果聚合越来越大事务锁越来越多并发量一上来系统先垮。我个人的实操经验是三条优先从业务不变式出发找聚合边界而不是从数据关系的外键出发。比如“返利金额必须为正”“同一订单不能被重复计算返利”这些才是真正需要强一致性的规则。能容忍最终一致的关系不要放进同一个聚合。用户下单、订单回传、返利计算、提现这个链条天然是异步的不可能也不必要全部包裹在一个大事务里。聚合之间的交互优先用领域事件解耦不要直接调用对方的Repository或者Service。这也是这篇文章重点讲的部分。基于这三条原则我们把系统划分为四个主要聚合返利订单聚合核心、用户账户聚合、商品推广计划聚合、提现单聚合。下面逐一拆解每个聚合的职责与内部结构。3. 核心聚合根设计返利订单聚合3.1 为什么选择“返利订单”作为核心聚合根电商平台回传一个原始订单后系统要先判断这个订单是不是通过推广链接产生的再计算可返利润再确定佣金归属最后还要跟踪订单状态直到可结算。所以“返利订单”天然是所有返利业务的锚点。后续无论你加多少功能——团队管理、渠道分成、活动奖励——都会挂到这个聚合上。我把返利订单聚合定义为整个系统的核心聚合根而不是“用户”或者“商品”。原因很简单用户聚合承载的是账户资产与个人信息它的生命周期和返利订单并不一致商品推广计划聚合关注的是佣金配置的时效性它会频繁修改但每次修改不应该直接影响历史订单。返利订单这个聚合刚好是稳定且高并发的它记录的是每一次推广行为的快照天然适合作为一致性边界。3.2 聚合内部的实体与值对象聚合根内部结构如图表所示文字描述不用图RebateOrder聚合根 ├── orderId电商平台原始订单号全局唯一 ├── rebateOrderId内部订单号 ├── userInfo值对象userId, userLevel, inviteUserId等 ├── productSnapshot值对象productId, productName, categoryId, price等 ├── commissionPolicy值对象rate, floorAmount, validFrom, validTo等 ├── rebateAmount值对象money, currency ├── status枚举PENDING, VALIDATING, CALCULATED, SETTLED, CLOSED └── events领域事件列表记录已发布的事件这里重点解释一下**值对象Value Object**的使用。很多初学者会问为什么commissionPolicy不单独建表而要作为值对象塞到订单快照里因为返利计算有一个非常重要的特点佣金比例必须在订单发生时刻被固化。假设用户在周一通过推广链接下单当时的佣金比例是10%周三商家调整成了5%那么这个订单的返利应该按哪次的规则算答案是按周一的10%。如果commissionPolicy是单独的表关联查询一旦商家改了配置历史订单的返利计算依据就变了——这会导致报表对不上、用户投诉。所以正确的做法是把下单时刻的佣金政策完整复制进订单聚合形成不可变的快照。这个设计决策本质上是用空间换确定性也是这个聚合根存在的核心价值之一。3.3 关键业务规则与不变式聚合根内部维护的业务不变式我整理成了下表不变式说明违反后果同一电商订单号只能产生一条有效返利订单防止重复计算返利用户账户金额错误资损事故佣金比例必须是 [0,1] 范围内的正数防止配置脏数据进入计算异常金额入账审计难以追溯订单金额必须大于0才能进入返利计算防止无效订单干扰结算报表失真出现负数返利返利订单状态流转必须符合合法状态机防止跳状态导致结算错误提现提前放行或订单悬挂佣金快照时间戳必须早于订单创建时间确保规则时效性规则应用错误返利金额不准这些不变式在DDD落地时应该由聚合自身保证而不是靠Service层去检查。我在代码里是这样实现的public class RebateOrder { private RebateOrderId rebateOrderId; private String externalOrderId; private UserInfo userInfo; private ProductSnapshot productSnapshot; private CommissionPolicy commissionPolicy; private Amount rebateAmount; private RebateStatus status; private ListIDomainEvent domainEvents; private RebateOrder() {} // 工厂方法从电商回传订单创建聚合根 public static RebateOrder createFromExternalOrder( String externalOrderId, UserInfo userInfo, ProductSnapshot productSnapshot, CommissionPolicy policy) { // 参数校验就是业务规则的一部分 Preconditions.checkArgument(policy.getRate().compareTo(BigDecimal.ZERO) 0, 佣金比例必须大于0); Preconditions.checkArgument(productSnapshot.getPrice().compareTo(BigDecimal.ZERO) 0, 订单金额必须大于0); RebateOrder order new RebateOrder(); order.rebateOrderId RebateOrderId.generate(); order.externalOrderId externalOrderId; order.userInfo userInfo; order.productSnapshot productSnapshot; order.commissionPolicy policy; order.status RebateStatus.PENDING; // 领域事件在聚合内部记录事务提交后统一发布 order.addDomainEvent(new RebateOrderCreatedEvent( order.rebateOrderId, externalOrderId, userInfo.getUserId(), policy.getRate() )); return order; } // 状态流转方法确认收货 public void confirmReceived() { if (this.status ! RebateStatus.CALCULATED) { throw new IllegalStateException(只有已计算的订单才能确认收货当前状态 this.status); } this.status RebateStatus.SETTLED; // 状态变更也是一个领域事件 this.addDomainEvent(new RebateOrderSettledEvent(this.rebateOrderId, this.rebateAmount)); } // 聚合内部发布领域事件的方法统一出口 private void addDomainEvent(IDomainEvent event) { if (this.domainEvents null) { this.domainEvents new ArrayList(); } this.domainEvents.add(event); } public ListIDomainEvent getDomainEvents() { return domainEvents; } public void clearDomainEvents() { this.domainEvents.clear(); } }需要特别说明聚合根内部的事件列表是临时内存变量不是持久化的业务数据。它只在当前事务内使用事务提交后由Repository统一取出并发布到消息中间件然后清空。这是很多DDD落地失败的关键细节——把领域事件混到业务表里容易造成数据冗余和语义混乱。3.4 聚合根与其他聚合的协作聚合根之间不直接互相调用而是通过聚合ID引用实现关联。比如返利订单聚合中保存了userId和inviteUserId但这只是对用户聚合的引用不代表返利订单聚合内有用户信息表。这样设计带来的好处很直接用户账户聚合和返利订单聚合可以独立伸缩。假设大促期间下单量暴增我可以只扩展返利订单聚合对应的服务实例用户账户服务不必跟着扩容。修改用户聚合的内部结构不会影响返利订单聚合的代码。比如用户等级规则调整虽然可能影响后续订单的返利计算但已经生成的返利订单不需要做任何修改——它的快照数据已经固化了。跨聚合的事务一致性通过领域事件最终达成而不是靠分布式事务。这一点在下面详细展开。4. 领域事件建模让订单链路异步解耦4.1 领域事件在返利系统中的角色领域事件Domain Event用一句话解释就是聚合根对外告知“某件业务相关的事情已经发生了”。返利系统里订单从创建到结算的整个生命周期中每一步都会产生值得其他模块关注的事件RebateOrderCreatedEvent新返利订单产生用户账户模块可用它做后续分析。RebateCalculatedEvent返利金额计算完成通知用户侧展示“预计可赚xx元”。RebateOrderSettledEvent订单确认收货、返利终态确定通知账户模块入账。SettlementFailureEvent结算失败触发重试或人工介入。RebateOrderClosedEvent订单取消或关闭返利作废清理在途状态。设计领域事件时我遵循几个原则事件的语义必须是“已发生的事实”用过去时态命名。事件内容必须携带足够的上下文让消费方不需要回头查数据库就能处理。事件与命令要区分命令Command是我要求你做某件事事件Event是所有模块都必须接受的事实。两者混用是最常见的错误。4.2 领域事件的定义与代码示例返利订单创建事件的定义public class RebateOrderCreatedEvent implements IDomainEvent { private final String rebateOrderId; private final String externalOrderId; private final String userId; private final String inviteUserId; private final BigDecimal commissionRate; private final BigDecimal orderAmount; private final OffsetDateTime occurredAt; public RebateOrderCreatedEvent(String rebateOrderId, String externalOrderId, String userId, String inviteUserId, BigDecimal commissionRate, BigDecimal orderAmount) { this.rebateOrderId rebateOrderId; this.externalOrderId externalOrderId; this.userId userId; this.inviteUserId inviteUserId; this.commissionRate commissionRate; this.orderAmount orderAmount; this.occurredAt OffsetDateTime.now(); } Override public OffsetDateTime getOccurredAt() { return occurredAt; } // getters... }有人会问事件里带上inviteUserId和commissionRate不算冗余吗是不是破坏封装我的回答是领域事件就是为了跨聚合传递上下文才存在的。消费方比如用户账户模块拿到RebateOrderCreatedEvent可以直接更新用户的“推广收益概览”而不必反查订单服务。如果事件里只有rebateOrderId那消费方每次都要做一次RPC或查库异步化的收益就大打折扣了。当然这会带来事件与订单数据的重复存储但用空间换时间、换解耦在返利系统这种高读场景下非常划算。4.3 事件发布机制与事务边界的配合领域事件必须在聚合状态变更的同一个数据库事务内持久化才能保证“事件不会丢”。但直接发消息队列的话又会面临“数据库提交成功但消息发送失败”的双写问题。我用的方案是事务性发件箱Transactional Outbox模式配合消息表实现RebateOrder 聚合状态变更事务A ↓ 同时向 outbox_event 表写入一条事件记录同一个事务A ↓ 定时任务扫描 outbox_event 表中未发送的记录 ↓ 发送到 Kafka / RocketMQ ↓ 标记为已发送发送成功后再更新状态伪代码逻辑如下Transactional public void createRebateOrder(CreateRebateOrderCommand command) { RebateOrder order RebateOrder.createFromExternalOrder(...); rebateOrderRepository.save(order); // 关键和聚合保存放在同一个事务里 for (IDomainEvent event : order.getDomainEvents()) { outboxRepository.save(new OutboxEvent(event)); } order.clearDomainEvents(); }这里的OutboxEvent不是业务表而是发送箱表只做消息持久化中转。outboxRepository.save()和rebateOrderRepository.save()必须在一个事务里这是整个异步化不丢事件的核心保障。对应的定时发送任务Component public class OutboxPublisher { private static final int BATCH_SIZE 100; Scheduled(fixedDelay 1000) public void publishPendingEvents() { ListOutboxEvent pendingEvents outboxRepository.findTopNByStatus(BatchSize); for (OutboxEvent event : pendingEvents) { try { messageProducer.send(event.getTopic(), event.getPayload()); outboxRepository.markSent(event.getId()); } catch (Exception ex) { // 记录日志等待下轮重试 log.error(Outbox event publish failed, ex); } } } }定时轮询的间隔可以按实际延迟要求调我常用1秒到5秒。如果对延迟极其敏感也可以用监听数据库binlog的方式替代定时轮询但复杂度会高不少大多数返利场景下1秒的延迟完全够用。5. 其他核心聚合根账户聚合与提现单聚合5.1 用户账户聚合资产变更必须单点收敛返利订单聚合负责“产生返利”但“返利入账”“提现扣减”这一类资产操作必须收敛到用户账户聚合。理由很简单资产操作是高一致性要求不能分散到多个聚合里。用户账户聚合的构成userId账户归属。balance可提现余额。frozenBalance提现中冻结金额防止超提。totalEarned累计收益。version乐观锁版本号。这里有一个设计细节返利订单结算后账户聚合通过监听RebateOrderSettledEvent来增加余额而不是由订单聚合直接调用账户服务。这会让很多不熟悉DDD的同事困惑“这不多此一举吗直接加个余额不就行了”我的解释是资产变更必须有一条独立且唯一的入账通道。如果业务代码里到处都是“加余额”“减余额”的调用将来加一个风控逻辑比如限制单日提现额度、异常账户冻结的时候你根本不知道要去哪里加。通过统一监听事件入账账户聚合就成了所有资产操作的唯一漏斗规则只需要在入口处加一道全链路就都覆盖了。账户聚合核心代码public class UserAccount { private String userId; private Money balance; private Money frozenBalance; private Money totalEarned; private int version; public void settleRebate(Money amount) { Preconditions.checkArgument(amount.isPositive(), 入账金额必须为正); this.balance this.balance.add(amount); this.totalEarned this.totalEarned.add(amount); this.version; } public void freezeForWithdrawal(Money amount) { Preconditions.checkArgument(this.balance.compareTo(amount) 0, 余额不足); this.balance this.balance.subtract(amount); this.frozenBalance this.frozenBalance.add(amount); this.version; } public void confirmWithdrawal(Money amount) { Preconditions.checkArgument(this.frozenBalance.compareTo(amount) 0, 冻结金额不足); this.frozenBalance this.frozenBalance.subtract(amount); this.version; } }并发控制这里必须强调账户聚合的更新用乐观锁version字段配合数据库的UPDATE ... WHERE version ?语句。不要用分布式锁因为账户操作的并发量在返利场景下并不低大促时每秒几百上千笔入账分布式锁会成为性能瓶颈而且锁超时后容易出各种边界问题。5.2 提现单聚合长流程状态机提现单聚合和返利订单聚合类似也是一个长生命周期状态机但业务规则更偏重风控与资金安全。提现单的状态流转CREATED创建 → REVIEWING审批中 → APPROVED通过 → PAYING打款中 → PAID已打款 → REJECTED拒绝提现单聚合的核心不变式是冻结金额和提现单金额必须严格对应。用户发起提现后freezeForWithdrawal先把账户余额冻结审批通过后confirmWithdrawal减少冻结金额打款结果异步回传后提现单终态更新。如果打款失败需要走UNFREEZE流程把冻结金额解冻回余额。这个聚合和返利订单聚合不同的是提现单会频繁与外部渠道交互银行、支付平台所以需要多维护一个“渠道状态”字段用于和第三方对账。这里建议不要把渠道返回的原始报文解析后塞进领域模型应该新增一个WithdrawalChannelRecord值对象来承载渠道侧的数据快照。5.3 商品推广计划聚合规则变更的版本管理商品推广计划聚合相对独立。商家调整佣金比例、运营配置活动都发生在这一侧。它需要维护一个版本号每次变更生成新版本。返利订单在创建时读取的是当前生效的佣金版本并把该版本固化进订单聚合。需要注意佣金策略变更在返利系统里是一个高危操作。商家改低佣金后已产生的返利订单不能受影响改高佣金后也不应该追溯到历史订单否则报表和结算都会乱套。所以这个聚合内部要设计effectiveFrom和effectiveUntil两个时间字段新订单只取当前时间命中的佣金版本。实践中一个容易忽略的点运营可能在同一天内修改多次佣金比例如果只存一个最新版本历史就丢了。我建议设计成多版本共存每次变更新增一条记录查询时按时间区间过滤。你甚至可以加一个changeReason字段方便运营回溯“为什么今天佣金突然变高了”。6. 领域事件在返利系统中的应用场景清单为了更直观我把返利系统里常用到的领域事件及其消费方整理成一张表领域事件发布聚合消费方消费后的动作RebateOrderCreatedEvent返利订单用户通知服务推送“返利订单已生成”通知RebateCalculatedEvent返利订单用户查询服务展示“预计返利xx元”RebateOrderSettledEvent返利订单用户账户聚合增加可提现余额RebateOrderSettledEvent返利订单数据报表服务更新佣金收入统计RebateOrderClosedEvent返利订单用户账户聚合无入账操作清挂在途订单WithdrawalCreatedEvent提现单风控服务触发异步风控校验WithdrawalApprovedEvent提现单打款渠道适配器发起打款请求WithdrawalPaidEvent提现单通知服务通知用户提现成功CommissionPolicyChangedEvent商品推广计划返利订单工厂用于日志审计不参与订单创建这张表的要点是同一个事件可以有多个消费方一个消费方也可以监听多个事件。领域事件本质上是一种“发布-订阅”模型发布方不需要知道谁在听这正是它实现解耦的根本原因。7. 落地过程中的坑与排查技巧7.1 事件消费的幂等性 —— 99%的故障都出在这里领域事件发布后消费方必须保证幂等。原因很简单消息队列做不到恰好一次投递。我实际遇到的场景是Kafka 的 at-least-once 语义下同一个RebateOrderSettledEvent在消费端被处理了两次用户余额翻了倍。幂等处理的方案要放在消费方不能指望发布方做全局去重。实战中最稳妥的方式是消费方用一个processed_event_id表记录已经处理过的事件ID处理前先查重处理完成后插入记录这两步要在同一个数据库事务里完成。有些团队喜欢用Redis的SETNX做去重虽然性能好但要做持久化和过期时间设计不然Redis重启后又能重复消费了。我的建议是涉及资产类的消费用数据库去重别省这个成本。核心代码如下Transactional public void handleSettledEvent(RebateOrderSettledEvent event) { // 同一个事务里先查重再处理 if (processedEventRepository.exists(event.getRebateOrderId())) { log.info(重复事件直接跳过{}, event.getRebateOrderId()); return; } UserAccount account userAccountRepository.findByUserId(event.getUserId()); account.settleRebate(event.getRebateAmount()); userAccountRepository.save(account); processedEventRepository.save(new ProcessedEvent(event.getRebateOrderId())); }如果你用的是RocketMQ可以利用它的消息Key天然去重但底层原理是一样的都是“标记-校验-处理”。7.2 聚合根的膨胀 —— 别让“大订单聚合”拖垮性能实际建模中返利订单聚合会承载越来越多的“附属功能”售后单、用户评价、物流跟踪、发票……这些虽然和订单相关但不应该都塞进返利订单聚合根里。我见过一个反例某个团队把用户退款处理也映射到返利订单聚合里结果每次创建返利订单都要带上退款单据的关联判断导致订单创建接口的响应时间从150ms涨到了600ms。数据库锁竞争也变严重了——因为退款单的变更会和订单主表锁争抢。解决思路很简单把售后单单独建模。返利订单聚合只关注“是否需要收回已发返利”通过RebateOrderRevokedEvent和售后模块联动而不是让两个聚合合并成一个。聚合越小并发冲突越少性能越容易保证。7.3 事件命名混乱 —— 统一用“过去时业务含义”事件命名是设计评审中最容易起分歧的地方。有的团队用OrderPaid有的用OrderPaySuccess有的用PayCallback同一个业务含义三套名字消费方对接时痛苦不堪。我建议团队内部定一条铁律领域事件统一采用“业务对象过去式动作”的格式比如RebateOrderCreated、WithdrawalPaid。避免使用Callback、Notify这类偏向技术实现的词也避免用命令式的CreateRebateOrder。事件是已经发生的事实不是要求别人做什么。7.4 领域事件与消息队列的选型匹配不同的消息中间件接入成本和特性差别很大我简单对比一下消息中间件事务消息支持顺序消息适用场景Kafka支持配合Outbox实现单分区内有序海量事件、吞吐优先RocketMQ原生事务消息队列内有序金融级强可靠性RabbitMQ需配合Outbox不内置顺序中小规模、快速迭代返利系统里订单结算入账这类事件建议用RocketMQ或者Outbox配合Kafka不要用RabbitMQ直接裸奔事务。因为资产类操作对可靠性要求极高一旦事件丢失补数据的成本远大于用一点性能来换。但这里有一个折中Kafka的幂等性配置和事务性 API 在Java场景下会吃掉部分吞吐如果QPS只有几百根本感受不到性能差异。所以选型时先用可靠性换吞吐别为了“将来可能的高并发”牺牲当下的稳定性。7.5 单元测试的策略 —— 聚合根的测试要覆盖业务规则DDD强调聚合根是业务规则的家那单元测试就重点覆盖聚合的规则校验。我用JUnit写测试的示意class RebateOrderTest { Test void testConfirmReceivedWithWrongStatusThrowsException() { RebateOrder order RebateOrder.createFromExternalOrder(...); // 模拟状态还在 PENDING直接确认收货 assertThrows(IllegalStateException.class, order::confirmReceived); } Test void testSettleRebateFreezesWhenBalanceInsufficient() { UserAccount account UserAccount.create(user1); assertThrows(IllegalArgumentException.class, () - account.freezeForWithdrawal(Money.of(1000))); } }这类测试不用启动Spring容器、不用连数据库纯内存就能跑速度极快。它能保证你在重构代码时业务规则不会被改坏。这是DDD落地中最容易忽视也最回报最高的一部分投入。8. 工程落地要点总结一套可直接复用的骨架8.1 代码分层结构建议这里给出我常用的包结构方便直接抄作业com.example.rebate ├── application │ ├── service # 应用服务负责事务编排 │ ├── command # 命令对象 │ └── eventhandler # 领域事件监听器 ├── domain │ ├── model # 聚合根、实体、值对象 │ ├── repository # 仓储接口 │ ├── service # 领域服务只有聚合根装不下的逻辑才放这里 │ └── event # 领域事件定义 ├── infrastructure │ ├── persistence # Repository实现、JPA/MyBatis │ ├── mq # 消息队列适配器 │ └── outbox # 发件箱实现 └── interfaces ├── rest # 对外接口/Controller └── job # 定时任务注意应用服务只做事务编排和参数组装业务逻辑在聚合根内部。如果发现应用服务里开始出现if...else判断业务规则那就是领域逻辑漏出去了需要及时收回来。8.2 数据库表设计的对应关系聚合根和数据库表并不是一一对应的。我最初踩过一个坑以为每个值对象都要建一张表结果商品快照、佣金策略都拆了表查询时不得不做多次Join性能很差。实际的表设计原则是聚合本身一张主表值对象采用序列化或平铺字段的方式存储。rebate_order表的结构大概是字段说明类型rebate_order_id聚合根IDvarchar(64)external_order_id电商原始订单号唯一索引varchar(64)user_id用户IDvarchar(64)invite_user_id邀请人IDvarchar(64)product_snapshot商品快照JSONtextcommission_policy佣金快照JSONtextrebate_amount返利金额分bigintstatus状态varchar(32)version乐观锁版本intcreated_at / updated_at审计字段datetime把product_snapshot和commission_policy直接存JSON既保证了快照的完整性又避免了一堆冗余的关联表。如果你觉得JSON不直观也可以在ORM里映射为Embedded对象双管齐下。8.3 代码仓库的模块划分如果项目体量较大多人协作我建议按模块拆分成多仓库或多Modulerebate-order-service返利订单聚合的读写服务。user-account-service用户账户聚合的读写服务。withdrawal-service提现单聚合的读写服务。promotion-service商品推广计划聚合的读写服务。outbox-publisher统一的发件箱扫描与事件发布任务。每个Service内部都是完整的DDD四层分层Service之间通过领域事件异步通信通过接口进行必要的同步查询补偿。这样做之后团队可以并行开发订单模块的改动不会导致账户模块的代码冲突。9. 写给同行的话DDD不是银弹但返利系统真的很适合我在多个项目中实践过DDD有一个明确的感受DDD最怕“为了DDD而DDD”。如果系统只有简单的CRUD没有复杂业务规则没有多个状态机联动生搬硬套DDD反而增加开发和维护成本。但对返利系统这种业务规则密度高、状态流转复杂、多个子系统协作紧密的场景DDD的价值是实实在在的每个业务规则都找到了它的“家”跨模块通信都走领域事件新增需求时不再需要在一堆if里翻找改哪里。最后如果只能给一条建议我会说先从画事件风暴开始把业务流程中的每个“已发生事实”列出来再决定哪些属于一个聚合。聚合并不需要一开始就完美可以随着业务理解加深逐步调整。但领域语言的统一、聚合边界的划分、事件上下文的明确这三件事在项目第一天就要做对。踩过几次坑之后我的体会是返利系统的核心从来不是“怎么算钱”而是“如何保证算过的每一笔钱都对得上账”。DDD的聚合根和领域事件恰好是为这个核心服务的两把趁手工具。希望这篇文章能给你一些可落地的参考。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑