欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!
在我后台回复 「资料」 可领取
编程高频电子书!
在我后台回复「面试」可领取硬核面试笔记!文章导读地址:点击查看文章导读!
感谢你的关注!
基于电商履约场景的 DDD 实战

基于 Cola 实现电商履约架构设计
我们在实际进行架构设计的时候,不用每一个方法都严格按照 Cola 或者 DDD 去设计,可以根据自己的需求以及习惯,做出一些合理的变动
这里将履约这个模块分为 6 个部分(后两个模块不重要,主要是前 4 个模块要清楚每一个模块的作用):

- api:与外界负责交互的模块
 - application:原来的 app 层,负责全局服务的处理
 - domain:领域层
 - infrastructure:基础设施层
 - rpc:与其他模块进行 rpc 通信的接口
 - start:启动 SpringBoot 项目的模块
 
订单履约流程
订单履约的流程,当订单支付成功之后,订单上下文会发送一个【订单支付成功】的事件,在履约上下文中,就可以监听【订单支付成功】事件,之后开始履约的流程
- 首先会进入到 api 层
 
监听器放在 api 层,与外界进行交互,监听器收到事件之后,将外部的事件转为 Command,再调用 application 层提供的【应用服务】来进行处理
api -- 
@Component
@Slf4j
public class OrderPayedEventListener implements MessageListenerConcurrently {
    @Autowired
    private FulfillApplicationService fulfillApplicationService;
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt messageExt : list) {
                String message = new String(messageExt.getBody());
                log.info("OrderPayedEventListener message:{}", message);
                // 1、将message转化为OrderPayedEvent领域事件
                OrderPayedEvent orderPayedEvent = JSONObject.parseObject(message, OrderPayedEvent.class);
                // 2、把领域事件,转换为一个command
                OrderFulfillCommand orderFulfillCommand = buildCommand(orderPayedEvent);
                // 3、交给app层的应用服务逻辑,去推动命令的流程编排和执行
                fulfillApplicationService.executeOrderFulfill(orderFulfillCommand);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
}
 
- application 层
 
在 application 层,就通过 Executor 执行履约的操作
@Component
public class FulfillApplicationService {
    @Autowired
    private OrderFulfillCommandExecutor orderFulfillCommandExecutor;
   
    public void executeOrderFulfill(OrderFulfillCommand orderFulfillCommand) {
        // 通过 Executor 来进行履约的处理
        orderFulfillCommandExecutor.execute(orderFulfillCommand);
    }
}
 
这里我们将 Executor 的具体实现也放在 application 层中去,在 Executor 中实现具体的履约流程:
-  
首先是
保存订单,这里先将 Command 给转成履约单 FulfillOrder,再将履约单进行保存操作,并且插入日志这里保存履约单的操作写到了 Gateway 中去,因为 Gateway 就是与下层 Infrastructure 层交互的
保存订单之后通过 【fulfillOrderLogDomainService】 来插入操作日志,这里将日志的插入操作写入到了 【fulfillOrderLogDomainService】 (即履约订单日志领域服务)中去,在这个 【DomainService】 中再调用 Gateway 去和 Infrastructure 进行交互,因为这里在插入日志信息的时候,还有一些额外的信息需要设置一下,所以在 DomainService 层进行了设置,之后再通过 Gateway 进行保存,可以保证 Gateway 层的职责比较单一(仅仅和 Infrastructure 交互或者其他模块交互)
 -  
其次是
风控拦截,风控拦截已经是其他模块的功能了,毫无疑问使用 Gateway 来和其他上下文进行通信,这里在 Gateway 中就可以直接和风控模块暴露的 rpc 接口进行通信了(使用 Gateway 层解耦)如果该订单被风控拦截了,使用【domainEventGateway】来发布被拦截的领域事件
 -  
接下来是
预分仓,由于预分仓这里,我们需要一些距离计算的方法来选择合适的仓库,而 Gateway 的目的是和其他模块进行交互,功能比较单一,因此预分仓这里我们再去创建一个【warehouseDomainService】即仓库的领域服务,在仓库的领域服务中再通过 Gateway 和仓库交互来获取所有仓库并且进行仓库的选择预分仓之后通过 【fulfillOrderLogDomainService】 来插入操作日志
 -  
下来是
分物流,这里和预分仓是类似的,分物流中的操作也比较复杂,所以不能直接放到 Gateway 中去,要先创建一个【logisticsDomainService】物流的领域服务,在物流的领域服务中去执行分物流的一系列操作分物流之后通过 【fulfillOrderLogDomainService】 来插入操作日志
 -  
最后是
下库房,先通过 Gateway 来和库房上下文进行交互,调用库房上下文暴露的接口来保存履约单下库房之后通过 【fulfillOrderLogDomainService】 来插入操作日志
 
@Component
@Slf4j
public class OrderFulfillCommandExecutor {
	// ...
    // 专门负责订单履约流程的编排,把这个流程按照战术建模的设计,完成落地开发
    @Transactional(rollbackFor = Exception.class)
    public void execute(OrderFulfillCommand orderFulfillCommand) {
        // 第一步,保存订单,需要去使用履约订单仓储/gateway来进行保存
        FulfillOrder fulfillOrder = fulfillOrderFactory.createFulfillOrder(orderFulfillCommand);
        log.info("创建fulfillOrder实体:{}", JSONObject.toJSONString(fulfillOrder));
        fulfillOrderGateway.save(fulfillOrder);
        fulfillOrderLogDomainService.saveOrderCreatedLog(fulfillOrder);
        // 第二步,风控拦截
         Boolean interceptResult = riskControlApiGateway.riskControlIntercept(fulfillOrder);
        log.info("风控拦截:{}", JSONObject.toJSONString(interceptResult));
        if (!interceptResult) {
            fulfillOrder.riskReject();
            // 如果被风控拦截了,此时就需要发布订单被拦截的领域事件,通知人工审核
            domainEventGateway.publishOrderInterceptedEvent(new OrderInterceptedEvent(fulfillOrder.getFulfillId().getFulfillId()));
            return;
        }else {
            fulfillOrder.riskPass();
        }
        // 第三步,预分仓
        Warehouse warehouse = warehouseDomainService.preAllocateWarehouse(fulfillOrder);
        log.info("预分仓:{}", JSONObject.toJSONString(fulfillOrder.getFulfillOrderWarehouse().getWarehouseId()));
        fulfillOrderLogDomainService.saveOrderWarehousedLog(fulfillOrder);
        // 第四步,分物流
        logisticsDomainService.allocateLogistics(fulfillOrder, warehouse);
        fulfillOrderLogDomainService.saveOrderLogisticsLog(fulfillOrder);
        log.info("分物流:{}",fulfillOrder.getLogisticsOrder().getLogisticsId());
        // 第五步,下发库房
        warehouseApiGateway.sendFulfillOrder(fulfillOrder, warehouse);
        fulfillOrderLogDomainService.saveOrderInStoredLog(fulfillOrder);
        log.info("下发库房");
    }
}
 
上边的履约流程总共有 5 个步骤,这里只挑选两个具有代表性(Gateway 分别和 Infrastructure、其他模块交互的两种情况)的步骤说一下:
这里先说一下第一步:保存订单,在保存订单的时候,需要两步:
1、通过 【fulfillOrderGateway】 与 Infrastructure 交互,进行订单的保存
2、通过 【fulfillOrderLogDomainService】 进行日志的插入操作
这里在 Gateway 中直接调用了 Infrastructure 层的 DAO 进行数据库操作,整个流程就结束了
@Component
public class FulfillOrderGatewayImpl implements FulfillOrderGateway {
	@Override
    public void save(FulfillOrder fulfillOrder) {
        FulfillOrderDO fulfillOrderDO = fulfillOrderDOConverter.convert(fulfillOrder);
        List<FulfillOrderItemDO> fulfillOrderItemDOs = mapStructSupport.convertToFulfillOrderItemDOs(fulfillOrder.getFulfillOrderItems());
        fulfillOrderDAO.save(fulfillOrderDO);
        fulfillOrderItemDAO.saveBatch(fulfillOrderItemDOs);
    }
}
 
接下来再看一下第二步:风控拦截,需要 2 步:
1、通过 【riskControlApiGateway】与风控模块进行交互,判断当前订单是否要被拦截
2、如果被拦截了,需要通过【domainEventGateway】发布【订单被拦截的事件】
这里在 Gateway 中直接就引入了风控模块的 API,通过 RPC 直接调用
@Component
public class RiskControlApiGatewayImpl implements RiskControlApiGateway {
    @DubboReference(version = "1.0.0", retries = 0)
    private RiskApi riskApi;
	@Override
    public Boolean riskControlIntercept(FulfillOrder fulfillOrder) {
        // 通过 Converter 将 fulfillOrder 转为发往风控拦截的请求 FulfillOrderRiskRequest
        FulfillOrderRiskRequest fulfillOrderRiskRequest = fulfillOrderRiskRequestConverter.convert(fulfillOrder);
        // 调用风控拦截的 API 判断是否拦截
        FulfillOrderRiskDTO fulfillOrderRiskDTO = riskApi.fulfillOrderRiskControlIntercept(fulfillOrderRiskRequest);
        if(fulfillOrderRiskDTO == null) {
            log.warn("风控调用失败");
            return false;
        }
        if(!fulfillOrderRiskDTO.getRiskResult()) {
            log.warn("风控检查拒绝通过");
            return false;
        }
        return true;
    }
}
 
上边代码比较多,可能看着不太清晰,这里我画张图:

这里还涉及一些实体对象的转换,监听器收到事件的时候,拿到的是消息,先将消息转为【OrderFulfillCommand】之后,再传入 Application 层,再将 【Command】转为【FulfillOrder】之后,传入 Domain 层,再转为【FulfillOrderDO】传入 Infrastructure 层
【OrderFulfillCommand】-> 【FulfillOrder】 使用 FulfillOrderFactory 来进行转换了,通过这个 Factory 创建一个履约单对象
【FulfillOrder】 ->【FulfillOrderDO】 使用 Converter 来进行转换,Converter 都放在了 Infrastructure 层
至此,订单履约流程的架构设计就完成了
那么可以发现,整个流程,从上到下,api -> application -> domain -> infrastructure,每个层的职责都是很清晰的,但是也正是因为职责划分的很清晰,从而导致代码在写起来的时候,需要比 MVC 架构多写很多的内容
因此对于中小型项目来说,盲目引入 DDD 只会导致项目的工作量剧增,DDD 的目的就是前期通过良好的分层、建模,后期可以降低维护成本



















