前言
一、DDD是什么
二、架构演进-六边形架构
三、编码实战
ddd-demo/
├── ddd-order-demo-api ------ 用户界面层-REST
│ ├── src
│ │ └── main
│ │ ├── java
│ │ │ └── cn
│ │ │ └── huolala
│ │ │ └── demo
│ │ │ ├── DemoApiApplication.java ------ starter
│ │ │ └── api
│ │ │ └── controller ------ 客户端接口
│ │ │ ├── UserController.java
│ │ │ └── OrderController.java
│ │ └── resources
│ │ └── application.yml
ddd-demo/
├── ddd-order-demo-application ------ 应用服务层,主要做业务编排,协调领域层做业务聚合
│ ├── src
│ │ └── main
│ │ └── java
│ │ └── cn
│ │ └── huolala
│ │ └── demo
│ │ └── application
│ │ ├── convertor ------ 模型转换器
│ │ │ ├── OrderConvertor.java
│ │ │ └── UserConvertor.java
│ │ ├── pojo
│ │ │ ├── cmd ------ 用户界面层写请求参数
│ │ │ │ ├── OrderChangeItemCmd.java
│ │ │ │ ├── OrderCreateCmd.java
│ │ │ │ └── co ------ cmd的子结构
│ │ │ │ └── OrderCreateProductItemCO.java
│ │ │ └── query ------ 用户界面层读请求参数
│ │ │ │ ├── UserQuery.java
│ │ │ │ └── qo ------ query的子结构
│ │ │ └── response ------ 用户界面层的响应模型
│ │ │ ├── OrderResponse.java
│ │ │ └── UserResponse.java
│ │ └── service ------ 应用服务层service,业务编排
│ │ ├── OrderService.java
│ │ └── UserService.java
ddd-demo/
├── ddd-order-demo-domain ------ 领域层,承载当前限界上下文的所有业务逻辑
│ ├── src
│ │ ├── main
│ │ │ ├── java
│ │ │ │ └── cn
│ │ │ │ └── huolala
│ │ │ │ └── demo
│ │ │ │ └── domain
│ │ │ │ ├── convertor
│ │ │ │ ├── infra ------ 基础设施相关接口
│ │ │ │ │ ├── mq ------ 基础设施-消息事件
│ │ │ │ │ │ └── OrderMessageProducer.java
│ │ │ │ │ ├── remote ------ 基础设施-远程调用
│ │ │ │ │ │ ├── IProductRemoteService.java
│ │ │ │ │ │ └── IScheduleRemoteService.java
│ │ │ │ │ └── repository ------ 基础设施-资源库
│ │ │ │ │ ├── IDeliveryAddressRepository.java
│ │ │ │ │ ├── IOrderRepository.java
│ │ │ │ │ └── IUserRepository.java
│ │ │ │ ├── model ------ 领域模型
│ │ │ │ │ ├── order ------ 子域-核心域
│ │ │ │ │ │ ├── DeliveryInfo.java
│ │ │ │ │ │ ├── LockInventoryResponse.java
│ │ │ │ │ │ ├── Order.java ------ 实体
│ │ │ │ │ │ ├── OrderDomainEvent.java
│ │ │ │ │ │ ├── OrderEvent.java
│ │ │ │ │ │ ├── OrderEventTypeEnum.java
│ │ │ │ │ │ ├── OrderItem.java
│ │ │ │ │ │ ├── OrderStateEnum.java
│ │ │ │ │ ├── product ------ 子域
│ │ │ │ │ │ └── Product.java
│ │ │ │ │ └── user ------ 子域
│ │ │ │ │ ├── User.java
│ │ │ │ │ └── UserStateEnum.java
│ │ │ │ └── service ------ 领域服务
│ │ │ │ ├── OrderDomainService.java
│ │ │ │ └── ProductDomainService.java
ddd-demo/
├── ddd-order-demo-infra-remote ------ 基础设施层-远程服务调用;访问外部限界上下文
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ └── java
│ │ └── cn
│ │ └── huolala
│ │ └── demo
│ │ └── remote
│ │ ├── client ------ 外部服务接口定义(REST)
│ │ │ ├── ProductApiClient.java
│ │ │ ├── ScheduleApiClient.java
│ │ │ ├── request ------ 外部服务调用请求参数
│ │ │ │ ├── BaseRemoteRequest.java
│ │ │ │ └── LockInventoryRequest.java
│ │ │ └── response ------ 外部服务调用响应体
│ │ │ ├── BaseRemoteResponse.java
│ │ │ └── ResponseMessageEnum.java
│ │ ├── convertor ------ 转换器,用于外部请求参数或响应体与领域模型的转换
│ │ │ └── OrderConvertor.java
│ │ ├── service ------ 防腐层,实现了领域层Remote接口,所有外部服务的调用入口
│ │ │ ├── ProductRemoteService.java
│ │ │ └── ScheduleRemoteService.java
├── ddd-order-demo-infra-repository ------ 基础设施层-资源库;数据模型与持久化相关
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ ├── java
│ │ │ └── cn
│ │ │ └── huolala
│ │ │ └── demo
│ │ │ ├── convertor ------ 转换器,用于数据模型和领域模型的转换
│ │ │ │ ├── DeliveryAddressConvertor.java
│ │ │ │ ├── OrderConvertor.java
│ │ │ │ ├── OrderItemConvertor.java
│ │ │ ├── entity ------ 数据模型
│ │ │ │ ├── DeliveryAddressEntity.java
│ │ │ │ ├── OrderEntity.java
│ │ │ │ ├── OrderItemEntity.java
│ │ │ └── repository ------ 资源库
│ │ │ ├── DeliveryAddressRepository.java
│ │ │ ├── OrderRepository.java
│ │ │ ├── OrderRepositoryES.java
│ │ │ └── mapper
│ │ │ ├── DeliveryAddressMapper.java
│ │ │ ├── OrderItemMapper.java
│ │ │ ├── OrderMapper.java
public class OrderController {
OrderService orderService;
public OrderResponse create(OrderCreateCmd cmd) {
return OrderConvertor.convertToResponse(orderService.createOrder(cmd));
}
}
public Order createOrder(OrderCreateCmd cmd) {
String orderNo = UUID.randomUUID().toString();
//从资源库获取用户
Optional<User> userOptional = userRepository.findById(cmd.getUserId());
User user = userOptional.orElseThrow(() -> new DemoBusinessException("用户不存在"));
//把参数转换为领域模型,可以定义convertor来进行转换
List<OrderItem> orderItemList = makeOrderItems(cmd.getProductItems(), orderNo);
//商品域-检查库存,product和OrderItem属于两个域,且使用了外部product服务,所以使用领域服务
orderDomainService.checkInventoryAndAssembleOrderItems(orderItemList);
//配送地址
Optional<DeliveryAddress> deliveryInfoOptional = deliveryAddressRepository.findById(cmd.getDeliveryAddressId());
DeliveryAddress deliveryAddress = deliveryInfoOptional.orElseThrow(() -> new DemoBusinessException("配送信息不存在"));
//创建订单,最好的方式是使用工厂
Order order = Order.create(orderNo, deliveryAddress, orderItemList, user.getUserId());
//调度域-锁定库存,用到了远程服务,所以放到了领域服务
orderDomainService.lockInventory(order);
//创建订单
orderRepository.createOrder(order);
//发布订单创建事件
orderMessageProducer.publish(order, OrderEventTypeEnum.INIT);
//返回领域模型,由用户界面层决定转为什么样的模型, 从架构层面限制模型滥用和模型滥转
return order;
}
/**
* 检查库存,组装订单项
* @param orderItems
* @return
*/
public void checkInventoryAndAssembleOrderItems(List<OrderItem> orderItems) {
if (CollectionUtils.isEmpty(orderItems)) {
throw new DemoBusinessException("未选择商品");
}
//从商品服务获取商品信息
List<Long> productIds = orderItems.stream().map(OrderItem::getProductId).collect( Collectors.toList());
List<Product> productList = productRemoteService.getProductInfos(productIds);
if (CollectionUtils.isEmpty(productList)) {
throw new DemoBusinessException("未查询到商品信息");
}
Map<Long, Product> productMap = productList.stream().collect(Collectors.toMap(Product::getProductId, p -> p));
//库存校验
for (OrderItem item : orderItems) {
Product product = productMap.get(item.getProductId());
if (product == null)
throw new DemoBusinessException("商品[" + item.getProductName() + "]不存在");
if (product.getInventoryCount() < item.getCount())
throw new DemoBusinessException("商品[" + product.getProductName() + "]库存不足");
//组装订单项信息
item.setPrice(product.getPrice());
item.setProductName(product.getProductName());
}
}
/**
* 锁定库存
*
* @param order
*/
public void lockInventory(Order order) {
Optional<LockInventoryResponse> lockInventoryDTOOptional = scheduleRemoteService.lockInventory(order);
LockInventoryResponse lockInventoryResponse = lockInventoryDTOOptional.orElseThrow(() -> new DemoBusinessException("库存锁定失败"));
if (lockInventoryResponse.getLockEndTime().before(new Date())) {
throw new DemoBusinessException("库存锁定失败");
}
order.setLockInventoryEndTime(lockInventoryResponse.getLockEndTime());
}
//……省略其他方法
/**
* 创建订单
* @param orderNo
* @param deliveryAddress
* @param orderItemList
* @param userId
* @return
*/
public static Order create(String orderNo, DeliveryAddress deliveryAddress, List<OrderItem> orderItemList, Long userId) {
Order order = new Order();
order.setOrderNo(orderNo);
order.setDeliveryAddress(deliveryAddress);
order.setOrderItemList(orderItemList);
order.setUserId(userId);
order.initialize();
return order;
}
//订单状态初始化
public void initialize() {
this.setStatus(OrderStateEnum.INIT.getValue());
this.setOrderTime(new Date());
calculateTotalPrice();
}
public void setTotalPrice(Long totalPrice) {
if (Objects.isNull(totalPrice) || totalPrice < 0)
throw new DemoBusinessException("Illegal totalPrice");
this.totalPrice = totalPrice;
}
public void setOrderNo(String orderNo) {
if (StringUtils.isBlank(orderNo))
throw new DemoBusinessException("orderNo can not be null");
this.orderNo = orderNo;
}
//……省略其他方法
public class ProductRemoteService implements IProductRemoteService {
ProductApiClient productApiClient;
public List<Product> getProductInfos(List<Long> productIds) {
BaseRemoteResponse<List<Product>> response = productApiClient.getProductInfos(productIds);
if (response == null || response.failed()) {
log.error("getProductInfos error,request:{},response:{}", productIds, response);
return Collections.emptyList();
}
return response.getData();
}
}
public class OrderRepository implements IOrderRepository {
OrderMapper orderMapper;
OrderItemMapper orderItemMapper;
private RedisTemplate<String, Object> redisTemplate;
private ElasticsearchTemplate<OrderEntityES, Long> orderESTemplate;
public Optional<Order> findById(long orderId) {
return Optional.ofNullable(OrderConvertor.convertToDO(elasticsearchTemplate.getById(orderId,OrderEntity.class)));
}
/**
* 创建订单
* @param order
*/
public void createOrder(Order order) {
//保存订单
OrderEntity orderEntity = OrderConvertor.convertToEntity(order);
orderMapper.insert(orderEntity);
order.setOrderId(orderEntity.getId());
//保存订单项
List<OrderItem> orderItemList = order.getOrderItemList();
orderItemList.forEach(orderItem -> {
orderItem.setOrderId(orderEntity.getId());
orderItemMapper.insert(OrderItemConvertor.INSTANCT.convertToEntity(orderItem));
});
}
}
四、货拉拉用户CRM改造实践
public MerchantBasicResponse createVisit(CreateVisitRequest request) {
EmployeeInfo info = EmployeeUtils.get();
//……省略部分代码
// 商户验证,存在才能新增拜访
MerchantEntity merchant = merchantBaseValidator.checkMerchantExist(request.getMerchantId());
// 只能拜访本人的商户
if (!Objects.equals(info.getHllerId(), merchant.getOwnerId())) {
log.warn("owner not current user, merchant: {}", merchant);
throw new ForceException(ErrorCodeEnum.OWNER_NOT_YOU);
}
//拜访定位异常判定
if (Objects.nonNull(merchant.getLongitude()) && Objects.nonNull(merchant.getLatitude())) {
double distance = ForceDistanceUtil.distance(merchant.getLongitude().doubleValue(),
merchant.getLatitude().doubleValue(), request.getLongitude().doubleValue(), request.getLatitude().doubleValue());
long maxVisitDistance = LaLaConfigClient.getAppLongConfigValue(MAX_VISIT_DISTANCE_KEY, DEFAULT_MAX_VISIT_DISTANCE);
log.info("visit distance: {}", distance);
// ……省略部分代码
}
// 张贴海报参数验证
VisitValidator.validateActivityType(request);
// 物资校验
validateMaterialDelivery(request);
// 坐标更新
if (Objects.nonNull(merchant.getDataType()) && merchant.getDataType() == 2
&& Objects.nonNull(merchant.getRelocation()) && Objects.equals(Boolean.FALSE, merchant.getRelocation())) {
merchant.setLongitude(request.getLongitude());
merchant.setLatitude(request.getLatitude());
merchant.setRelocation(true);
merchant.setLocation(request.getAddress());
merchantDao.updateById(merchant);
abnormalVisitPosition = false;
}
// 创建拜访实体
VisitEntity visit = VisitConvert.convertToEntity(request);
visit.setRegionId(merchant.getRegionId());
visit.setCityId(merchant.getCityId());
visit.setMarketId(merchant.getMarketId());
visit.setOpName(info.getUserName());
//密集拜访
LocalDateTime nowTime = LocalDateTime.now();
//往前推最大间隔
int minVisitCommitInterval = LaLaConfigClient.getAppIntConfigValue(MIN_VISIT_COMMIT_INTERVAL_KEY, DEFAULT_MIN_VISIT_COMMIT_INTERVAL);
LocalDateTime deadline = nowTime.minusSeconds(minVisitCommitInterval);
List<Visit> latestVisitList = visitDao.getLatestVisitList(info.getAccount(), deadline);
//密集拜访判定
intensiveVisit = CollUtil.isNotEmpty(latestVisitList);
visitDao.insert(visit);
// 商户状态校验
boolean isBuilding = Objects.equals(MerchantStatusEnum.BUILDING.getStatus(), merchant.getStatus());
if (isBuilding) {
VisitValidator.validateFirstVisit(request);
MerchantEntity entity = MerchantConvert.convertToCompleteEntity(request, visit.getVisitTime());
merchantDao.updateById(entity);
List<MerchantPictureEntity> list = MerchantConvert.convertToMerchantPictureEntity(request);
merchantPictureService.saveBatch(list);
} else {
MerchantEntity entity = new MerchantEntity();
entity.setId(request.getMerchantId());
entity.setLastVisitTime(visit.getVisitTime());
merchantDao.updateById(entity);
}
// 商户操作日志
List<MerchantOperateLogEntity> logs = new ArrayList<>();
//处理拜访质检标签
saveInterviewee(request, logs, visit, merchant, abnormalVisitPosition, intensiveVisit);
//拜访照片处理
List<VisitPictureEntity> pictures = new ArrayList<>();
pictures.addAll(convertToVisitPicture(visit.getId(), request.getFacadePictures(), PictureTypeEnum.FACADE));
pictures.addAll(convertToVisitPicture(visit.getId(), request.getIndoorPictures(), PictureTypeEnum.INDOOR));
if (null != request.getPosterPictures()) {
pictures.addAll(convertToVisitPicture(visit.getId(), request.getPosterPictures(), PictureTypeEnum.POSTER));
}
visitPictureDao.batchInsert(pictures);
// 商户建档审批流程
if (isBuilding) {
//……省略部分代码
}
// 保存商户操作日志
merchantOperateLogService.saveBatch(logs);
// 统计新增拜访数
MetricMonitor.monitor(MetricEnum.VISIT_CREATED_NUM_EVERY_DAY);
// 商户拜访跳转到商户列表页,返回商户变更信息
MerchantBasicResponse changeInfo = new MerchantBasicResponse();
changeInfo.setMerchantId(merchant.getId());
// 商户自动踢公时间
changeInfo.setAutoReleaseTime(merchant.getAutoReleaseTime());
// 今日已拜访
changeInfo.setHasVisitToday(Boolean.TRUE);
if (isBuilding) {
// 建档中,商户状态变更
changeInfo.setStatus(MerchantStatusEnum.APPROVAL.getStatus());
// 用车情况
changeInfo.setCarUsage(request.getCarUsage());
//……省略部分代码
}
// 拜访计划完成。
visitPlanService.completeVisitPlan(LocalDate.now(), info.getAccount(), request.getMerchantId());
return changeInfo;
}
public MerchantAppResponse createVisit(CreateVisitRequest request) {
// 商户私海检查
MerchantEntity merchant = merchantDomainService.checkMerchantIsPrivate(request.getMerchantId());
// 建档状态
boolean isBuilding = Objects.equals(MerchantStatusEnum.BUILDING.getStatus(), merchant.getStatus());
// 首次建档校验
VisitValidator.validateFirstVisit(merchant, request);
// 海报活动校验
VisitValidator.validateActivityType(request);
// 物资校验
materialDomainService.validateMaterialDelivery(request);
// 创建拜访
VisitEntity visit = VisitConvert.convertToEntity(request);
// 保存拜访照片
visitDomainService.saveVisit(visit, request);
// 记录商户操作日志,批量保存
List<MerchantOperateLogEntity> operateLogList = new ArrayList<>();
// 保存拜访用户相关信息,并记录日志
visitDomainService.saveVisitUserInfo(request, visit, merchant, operateLogList);
// 装配拜访物资发放信息,并记录日志
List<VisitUserMaterialEntity> visitUserMaterialList = visitDomainService.assembleVisitMaterialsAndRecordLog(
request, visit.getId(), merchant, operateLogList);
// 处理物资发放
materialDomainService.handleVisitMaterialDelivery(visitUserMaterialList, visit);
//更新拜访后的商户信息
merchantDomainService.updateMerchantInfoWithVisitAndRecordLog(merchant, visit, request, operateLogList);
// 拜访时间
List<Long> userIdList = request.getInterviewees().stream()
.map(IntervieweeVO::getUserId).collect(Collectors.toList());
userRepository.updateLastVisitTime(userIdList, request.getVisitTime());
// 构造response
MerchantAppResponse response = MerchantAppResponse.createBaseResponse(merchant.getId(),
merchant.getAutoReleaseTime(), true);
// 保存商户操作日志
merchantOperateLogRepository.saveBatch(operateLogList);
// 拜访计划
visitPlanRepository.completeVisitPlan(LocalDate.now(), EmployeeUtils.get().getAccount(), request.getMerchantId());
// 埋点
MetricMonitor.monitor(MetricEnum.VISIT_CREATED_NUM_EVERY_DAY);
return response;
}
五、总结
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721