ISDP010_基于DDD架构实现收银用例主乐成场景

打印 上一主题 下一主题

主题 759|帖子 759|积分 2277

信息体系开发实践 | 系列文章传送门
ISDP001_课程概述
ISDP002_Maven上_创建Maven项目
ISDP003_Maven下_Maven项目依靠设置
ISDP004_创建SpringBoot3项目
ISDP005_Spring组件与自动装配
ISDP006_逻辑架构设计
ISDP007_Springboot日志设置与单元测试
ISDP008_SpringBoot Controller接口文档与测试
ISDP009_基于DDD架构设计ISDP的处理销售用例
ISDP010_基于DDD架构实现收银用例主乐成场景
1 面向DDD重构mis-pos模块

重要阐明:由于代码量增加,且常常需要重构。条记将难以展示项目完整代码。本章条记开始只展示部分代码。完整代码详见条记最后项目堆栈分支代码。
参考上篇分析与设计制品,参考DDD架构,重构的mis-pos模块的架构分层。
根据DDD架构分为application、domain、infrastructure三个包。

2 底子办法层

底子办法层暂时还没有写太多的类。只是添加了SaleFactory用于实例化Sale。
引入Hutool工具类,用于天生订单的雪花ID。
  1.         <dependency>
  2.             <groupId>cn.hutool</groupId>
  3.             <artifactId>hutool-all</artifactId>
  4.             <version>5.8.20</version>
  5.         </dependency>
复制代码
编写SaleFactory,实例化Sale并设置初始化值。
  1. package edu.scau.mis.pos.infrastructure.factory;
  2. import cn.hutool.core.util.IdUtil;
  3. import edu.scau.mis.pos.domain.entity.Sale;
  4. import edu.scau.mis.pos.domain.enums.SaleStatusEnum;
  5. import org.springframework.stereotype.Component;
  6. import java.math.BigDecimal;
  7. import java.util.Date;
  8. /**
  9. * Sale工厂类
  10. */
  11. @Component
  12. public class SaleFactory {
  13.     public Sale initSale()
  14.     {
  15.         Sale sale = new Sale();
  16.         sale.setSaleNo("so-" + IdUtil.getSnowflakeNextId());
  17.         sale.setSaleStatus(SaleStatusEnum.CREATED);
  18.         sale.setTotalAmount(BigDecimal.ZERO);
  19.         sale.setTotalQuantity(0);
  20.         sale.setSaleTime(new Date());
  21.         return sale;
  22.     }
  23. }
复制代码
3 领域层

在DDD架构中,领域层是重点关注层。
为了简化Setter和getter编写,引入了Lombok。
  1.         <dependency>
  2.             <groupId>org.projectlombok</groupId>
  3.             <artifactId>lombok</artifactId>
  4.         </dependency>
复制代码
3.1 SaleProduct实体类

SaleProduct实体类包含了业务逻辑方法getSubTotal,计算每个订单明细的小计。
  1. package edu.scau.mis.pos.domain.entity;
  2. import edu.scau.mis.pos.domain.enums.SaleProductStatusEnum;
  3. import lombok.Data;
  4. import java.math.BigDecimal;
  5. /**
  6. * 订单-产品明细实体类
  7. */
  8. @Data
  9. public class SaleProduct {
  10.     private Long saleProductId;
  11.     private Long saleId;
  12.     private Long productId;
  13.     private Product product;
  14.     private Integer saleQuantity;
  15.     private BigDecimal salePrice;
  16.     private SaleProductStatusEnum saleProductStatus;
  17.     /**
  18.      * 计算小计
  19.      * @return
  20.      */
  21.     public BigDecimal getSubTotal() {
  22.         return salePrice.multiply(new BigDecimal(saleQuantity));
  23.     }
  24. }
复制代码
3.2 付出实体类

付出类暂时还没有写业务逻辑方法。后期思量通过适配器,毗连第三方付出。
  1. package edu.scau.mis.pos.domain.entity;
  2. import com.fasterxml.jackson.annotation.JsonFormat;
  3. import edu.scau.mis.pos.domain.enums.PaymentStatusEnum;
  4. import edu.scau.mis.pos.domain.enums.PaymentStrategyEnum;
  5. import lombok.Data;
  6. import java.math.BigDecimal;
  7. import java.util.Date;
  8. /**
  9. * 支付实体类
  10. */
  11. @Data
  12. public class Payment {
  13.     private Long paymentId;
  14.     private Long paymentSaleId;
  15.     private PaymentStrategyEnum paymentStrategy;
  16.     private String paymentNo;
  17.     private BigDecimal paymentAmount;
  18.     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  19.     private Date paymentTime;
  20.     private PaymentStatusEnum paymentStatus;
  21. }
复制代码
3.3 Sale聚合根

Sale类是收银领域层的聚合根。
该类内聚了两个业务逻辑方法,分别为添加订单明细和计算总金额。
  1. package edu.scau.mis.pos.domain.entity;
  2. import com.fasterxml.jackson.annotation.JsonFormat;
  3. import edu.scau.mis.pos.domain.enums.SaleStatusEnum;
  4. import lombok.Data;
  5. import java.math.BigDecimal;
  6. import java.util.ArrayList;
  7. import java.util.Date;
  8. import java.util.List;
  9. /**
  10. * 销售实体类
  11. * 聚合根
  12. */
  13. @Data
  14. public class Sale {
  15.     private Long saleId;
  16.     private String saleNo;
  17.     private BigDecimal totalAmount;
  18.     private Integer totalQuantity;
  19.     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  20.     private Date saleTime;
  21.     private Payment payment;
  22.     private List<SaleProduct> saleProducts = new ArrayList<>();
  23.     private SaleStatusEnum saleStatus;
  24.     /**
  25.      * 计算总金额
  26.      * @return
  27.      */
  28.     public BigDecimal getTotal(){
  29.         totalAmount = BigDecimal.ZERO;
  30.         totalQuantity = 0;
  31.         for(SaleProduct saleProduct: saleProducts){
  32.             totalAmount = totalAmount.add(saleProduct.getSubTotal());
  33.             totalQuantity = totalQuantity + saleProduct.getSaleQuantity();
  34.         }
  35.         return totalAmount;
  36.     }
  37.     /**
  38.      * 添加订单明细
  39.      * @param product
  40.      * @param saleQuantity
  41.      * @return
  42.      */
  43.     public List<SaleProduct> makeLineItem(Product product, Integer saleQuantity) {
  44.         // 判断商品是否已录入,未录入则新增。已录入则修改数量。
  45.         if(!isEntered(product.getProductSn(),saleQuantity)){
  46.             SaleProduct saleProduct = new SaleProduct();
  47.             saleProduct.setProduct(product);
  48.             saleProduct.setSaleQuantity(saleQuantity);
  49.             saleProduct.setSalePrice(product.getProductPrice());
  50.             saleProducts.add(saleProduct);
  51.         }
  52.         return saleProducts;
  53.     }
  54.     /**
  55.      * 判断商品是否已录入
  56.      * 业务逻辑:如果已录入,则修改数量,否则添加saleLineItem
  57.      * @param productSn
  58.      * @param saleQuantity
  59.      * @return
  60.      */
  61.     private boolean isEntered(String productSn, Integer saleQuantity){
  62.         boolean flag = false;
  63.         for(SaleProduct sp : saleProducts){
  64.             if(productSn.equals(sp.getProduct().getProductSn())) {
  65.                 flag = true;
  66.                 Integer quantityOriginal = sp.getSaleQuantity();
  67.                 sp.setSaleQuantity(quantityOriginal + saleQuantity);
  68.             }
  69.         }
  70.         return flag;
  71.     }
  72. }
复制代码
3.4 领域服务类SaleService

主要用于天生付出功能。ISDP项目POS体系设计支持挂单功能。
  1. package edu.scau.mis.pos.domain.service.impl;
  2. import cn.hutool.core.util.IdUtil;
  3. import edu.scau.mis.pos.domain.entity.Payment;
  4. import edu.scau.mis.pos.domain.entity.Sale;
  5. import edu.scau.mis.pos.domain.enums.PaymentStatusEnum;
  6. import edu.scau.mis.pos.domain.enums.PaymentStrategyEnum;
  7. import edu.scau.mis.pos.domain.service.ISaleService;
  8. import org.springframework.stereotype.Service;
  9. import java.math.BigDecimal;
  10. import java.util.Date;
  11. /**
  12. * 领域服务
  13. */
  14. @Service
  15. public class SaleServiceImpl implements ISaleService {
  16.     @Override
  17.     public Payment makePayment(Sale sale, String paymentStrategy, BigDecimal paymentAmount) {
  18.         Payment payment = new Payment();
  19.         payment.setPaymentStrategy(PaymentStrategyEnum.valueOf(paymentStrategy));
  20.         payment.setPaymentNo(paymentStrategy + "-" + IdUtil.getSnowflakeNextId());
  21.         payment.setPaymentAmount(paymentAmount);
  22.         payment.setPaymentTime(new Date());
  23.         payment.setPaymentStatus(PaymentStatusEnum.PAID);
  24.         // TODO: 根据不同支付策略调用不同外部接口
  25.         return payment;
  26.     }
  27. }
复制代码
3.5 其他

Domain层还有堆栈、枚举等包。由于暂时还没有利用数据库和Redis,堆栈代码暂时没写。
写了一些枚举类。由于只是讲授项目,没有设计过多状态。
  1. package edu.scau.mis.pos.domain.enums;
  2. /**
  3. * 订单状态枚举
  4. */
  5. public enum SaleStatusEnum {
  6.     CREATED("0","已预订"),
  7.     SUBMITTED("1","已提交"),
  8.     PAID("2","已支付");
  9.     private String value;
  10.     private String label;
  11.     SaleStatusEnum(String value, String label) {
  12.         this.value = value;
  13.         this.label = label;
  14.     }
  15.     public String getLabel() {
  16.         return label;
  17.     }
  18.     public String getValue() {
  19.         return value;
  20.     }
  21.     /**
  22.      * 根据匹配value的值获取Label
  23.      *
  24.      * @param value
  25.      * @return
  26.      */
  27.     public static String getLabelByValue(String value){
  28.         for (SaleStatusEnum s : SaleStatusEnum.values()) {
  29.             if(value.equals(s.getValue())){
  30.                 return s.getLabel();
  31.             }
  32.         }
  33.         return "";
  34.     }
  35.     /**
  36.      * 获取StatusEnum
  37.      *
  38.      * @param value
  39.      * @return
  40.      */
  41.     public static SaleStatusEnum getStatusEnum(String value){
  42.         for (SaleStatusEnum s : SaleStatusEnum.values()) {
  43.             if(value.equals(s.getValue())){
  44.                 return s;
  45.             }
  46.         }
  47.         return null;
  48.     }
  49. }
复制代码
付出策略枚举类
  1. package edu.scau.mis.pos.domain.enums;
  2. /**
  3. * 支付策略枚举
  4. */
  5. public enum PaymentStrategyEnum {
  6.     WECHAT("wechat","微信支付"),
  7.     ALIPAY("alipay","支付宝"),
  8.     CASH("cash","现金");
  9.     private String value;
  10.     private String label;
  11.     PaymentStrategyEnum(String value, String label) {
  12.         this.value = value;
  13.         this.label = label;
  14.     }
  15.     public String getLabel() {
  16.         return label;
  17.     }
  18.     public String getValue() {
  19.         return value;
  20.     }
  21.     /**
  22.      * 根据匹配value的值获取Label
  23.      *
  24.      * @param value
  25.      * @return
  26.      */
  27.     public static String getLabelByValue(String value){
  28.         for (PaymentStrategyEnum s : PaymentStrategyEnum.values()) {
  29.             if(value.equals(s.getValue())){
  30.                 return s.getLabel();
  31.             }
  32.         }
  33.         return "";
  34.     }
  35.     /**
  36.      * 获取StatusEnum
  37.      *
  38.      * @param value
  39.      * @return
  40.      */
  41.     public static PaymentStrategyEnum getStrategyEnum(String value){
  42.         for (PaymentStrategyEnum s : PaymentStrategyEnum.values()) {
  43.             if(value.equals(s.getValue())){
  44.                 return s;
  45.             }
  46.         }
  47.         return null;
  48.     }
  49. }
复制代码
4 应用层

4.1 应用服务类SaleApplicationService

编写SaleApplicationService类。该类主要负责跨领域协作。
现在主要就两个领域Sale(SaleProduct、Payment)和Product(Category)。
如果利用微服务,可以分别针对这两个领域创建两个微服务模块。
  1. package edu.scau.mis.pos.application.service;
  2. import edu.scau.mis.pos.application.assembler.SaleAssembler;
  3. import edu.scau.mis.pos.application.dto.command.EnterItemCommand;
  4. import edu.scau.mis.pos.application.dto.command.MakePaymentCommand;
  5. import edu.scau.mis.pos.application.dto.vo.*;
  6. import edu.scau.mis.pos.domain.entity.Payment;
  7. import edu.scau.mis.pos.domain.entity.Product;
  8. import edu.scau.mis.pos.domain.entity.Sale;
  9. import edu.scau.mis.pos.domain.entity.SaleProduct;
  10. import edu.scau.mis.pos.domain.enums.SaleStatusEnum;
  11. import edu.scau.mis.pos.domain.service.IProductService;
  12. import edu.scau.mis.pos.infrastructure.factory.SaleFactory;
  13. import edu.scau.mis.pos.domain.service.ISaleService;
  14. import org.springframework.beans.factory.annotation.Autowired;
  15. import org.springframework.stereotype.Service;
  16. import java.util.List;
  17. @Service
  18. public class SaleApplicationService {
  19.     @Autowired
  20.     private IProductService productService;
  21.     @Autowired
  22.     private ISaleService saleService;
  23.     @Autowired
  24.     private SaleFactory saleFactory;
  25.     @Autowired
  26.     private SaleAssembler saleAssembler;
  27.     private Sale currentSale; // 后期改成Redis缓存CurrentSale
  28.     /**
  29.      * 开始一次新销售
  30.      * @return
  31.      */
  32.     public SaleVo makeNewSale(){
  33.         SaleVo saleVo = new SaleVo();
  34.         currentSale = saleFactory.initSale();
  35.         // TODO:引入Redis缓存
  36.         return saleAssembler.toSaleVo(currentSale);
  37.     }
  38.     /**
  39.      * 录入商品
  40.      * @param command
  41.      * @return
  42.      */
  43.     public SaleAndProductListVo enterItem(EnterItemCommand command){
  44.         SaleAndProductListVo saleAndProductListVo = new SaleAndProductListVo();
  45.         Product product = productService.selectProductBySn(command.getProductSn());
  46.         List<SaleProduct> saleProducts = currentSale.makeLineItem(product, command.getSaleQuantity());
  47.         currentSale.getTotal();
  48.         List<SaleProductVo> saleProductVoList = saleProducts.stream()
  49.                 .map(saleProduct -> new SaleProductVo(saleProduct.getProduct().getProductSn(), saleProduct.getProduct().getProductName(), saleProduct.getSalePrice(), saleProduct.getSaleQuantity()))
  50.                 .toList();
  51.         saleAndProductListVo.setSaleVo(saleAssembler.toSaleVo(currentSale));
  52.         saleAndProductListVo.setSaleProductVoList(saleProductVoList);
  53.         return saleAndProductListVo;
  54.     }
  55.     /**
  56.      * 结束销售
  57.      * 计算优惠、持久化订单等
  58.      * @return
  59.      */
  60.     public SaleVo endSale(){
  61.         currentSale.setSaleStatus(SaleStatusEnum.SUBMITTED);
  62.         // TODO: 持久化Sale和SaleProduct,添加事务注解
  63.         return saleAssembler.toSaleVo(currentSale);
  64.     }
  65.     /**
  66.      * 完成支付
  67.      * @param command
  68.      * @return
  69.      */
  70.     public SaleAndPaymentVo makePayment(MakePaymentCommand command){
  71.         SaleAndPaymentVo saleAndPaymentVo = new SaleAndPaymentVo();
  72.         // TODO: 挂单--根据saleNo获取Sale
  73.         Payment payment = saleService.makePayment(currentSale,command.getPaymentStrategy(), command.getPaymentAmount());
  74.         currentSale.setPayment(payment);
  75.         currentSale.setSaleStatus(SaleStatusEnum.PAID);
  76.         // TODO: 持久化Sale和Payment,添加事务注解
  77.         // payment.setPaymentSaleId(sale.getSaleId());
  78.         saleAndPaymentVo.setSaleVo(saleAssembler.toSaleVo(currentSale));
  79.         saleAndPaymentVo.setPaymentVo(saleAssembler.toPaymentVo(payment));
  80.         return saleAndPaymentVo;
  81.     }
  82. }
复制代码
4.2 数据传输对象DTO

ISDP项目采用CQRS思想,该层编写大量的数据传输对象DTO。条记只展示部分代码。详细参加项目堆栈。
EnterItemCommand参考代码如下。
后期将利用Redis缓存currentSale,设计saleNo作为key。保留saleNo备用。
  1. package edu.scau.mis.pos.application.dto.command;
  2. import lombok.Data;
  3. import java.io.Serializable;
  4. /**
  5. * 输入订单明细命令
  6. */
  7. @Data
  8. public class EnterItemCommand implements Serializable {
  9.     private String saleNo;
  10.     private String productSn;
  11.     private Integer saleQuantity;
  12. }
复制代码
MakePaymentCommand代码参考如下:
同上,saleNo暂时不需要。
  1. package edu.scau.mis.pos.application.dto.command;
  2. import lombok.Data;
  3. import java.io.Serializable;
  4. import java.math.BigDecimal;
  5. /**
  6. * 创建支付命令
  7. */
  8. @Data
  9. public class MakePaymentCommand implements Serializable {
  10.     private String saleNo;
  11.     private BigDecimal paymentAmount;
  12.     private String paymentStrategy;
  13. }
复制代码
SaleVo类
  1. package edu.scau.mis.pos.application.dto.vo;
  2. import lombok.Data;
  3. import java.io.Serializable;
  4. import java.math.BigDecimal;
  5. import java.util.Date;
  6. @Data
  7. public class SaleVo implements Serializable {
  8.     private String saleNo;
  9.     private BigDecimal totalAmount;
  10.     private Integer totalQuantity;
  11.     private Date saleTime;
  12.     private String saleStatus;
  13. }
复制代码
SaleAndPaymentVo
  1. package edu.scau.mis.pos.application.dto.vo;
  2. import lombok.Data;
  3. import java.io.Serializable;
  4. @Data
  5. public class SaleAndPaymentVo implements Serializable {
  6.     private SaleVo saleVo;
  7.     private PaymentVo paymentVo;
  8. }
复制代码
4.3 对象转换器SaleAssembler

面向接口层主要利用DTO对象,因此不可避免涉及到DTO与领域对象的转换。
  1. package edu.scau.mis.pos.application.assembler;
  2. import edu.scau.mis.pos.application.dto.vo.PaymentVo;
  3. import edu.scau.mis.pos.application.dto.vo.SaleVo;
  4. import edu.scau.mis.pos.domain.entity.Payment;
  5. import edu.scau.mis.pos.domain.entity.Sale;
  6. import org.springframework.stereotype.Component;
  7. /**
  8. * 订单转换器
  9. * 实现DTO与Entity的转换
  10. */
  11. @Component
  12. public class SaleAssembler {
  13.     public SaleVo toSaleVo(Sale sale){
  14.         SaleVo saleVo = new SaleVo();
  15.         saleVo.setSaleNo(sale.getSaleNo());
  16.         saleVo.setTotalAmount(sale.getTotalAmount());
  17.         saleVo.setTotalQuantity(sale.getTotalQuantity());
  18.         saleVo.setSaleTime(sale.getSaleTime());
  19.         saleVo.setSaleStatus(sale.getSaleStatus().getLabel());
  20.         return saleVo;
  21.     }
  22.     public PaymentVo toPaymentVo(Payment payment){
  23.         PaymentVo paymentVo = new PaymentVo();
  24.         paymentVo.setPaymentId(payment.getPaymentId());
  25.         paymentVo.setPaymentSaleId(payment.getPaymentSaleId());
  26.         paymentVo.setPaymentNo(payment.getPaymentNo());
  27.         paymentVo.setPaymentAmount(payment.getPaymentAmount());
  28.         paymentVo.setPaymentTime(payment.getPaymentTime());
  29.         paymentVo.setPaymentStrategy(payment.getPaymentStrategy().getLabel());
  30.         paymentVo.setPaymentStatus(payment.getPaymentStatus().getLabel());
  31.         return paymentVo;
  32.     }
  33. }
复制代码
5 接口层

5.1 Controller接口

SaleController参考如下:
  1. package edu.scau.mis.web.controller;
  2. import edu.scau.mis.pos.application.dto.command.EnterItemCommand;
  3. import edu.scau.mis.pos.application.dto.command.MakePaymentCommand;
  4. import edu.scau.mis.pos.application.dto.vo.SaleAndPaymentVo;
  5. import edu.scau.mis.pos.application.dto.vo.SaleAndProductListVo;
  6. import edu.scau.mis.pos.application.dto.vo.SaleVo;
  7. import edu.scau.mis.pos.application.service.SaleApplicationService;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.http.ResponseEntity;
  10. import org.springframework.web.bind.annotation.*;
  11. @RestController
  12. @RequestMapping("/sale")
  13. public class SaleController {
  14.     @Autowired
  15.     private SaleApplicationService saleApplicationService;
  16.     @GetMapping("/makeNewSale")
  17.     public ResponseEntity<SaleVo> makeNewSale()
  18.     {
  19.         return ResponseEntity.ok(saleApplicationService.makeNewSale());
  20.     }
  21.     @PostMapping("/enterItem")
  22.     public ResponseEntity<SaleAndProductListVo> enterItem(@RequestBody  EnterItemCommand enterItemCommand)
  23.     {
  24.         return ResponseEntity.ok(saleApplicationService.enterItem(enterItemCommand));
  25.     }
  26.     @GetMapping("/endSale")
  27.     public ResponseEntity<SaleVo> endSale()
  28.     {
  29.         return ResponseEntity.ok(saleApplicationService.endSale());
  30.     }
  31.     @PostMapping("/makePayment")
  32.     public ResponseEntity<SaleAndPaymentVo> makePayment(@RequestBody MakePaymentCommand makePaymentCommand)
  33.     {
  34.         return ResponseEntity.ok(saleApplicationService.makePayment(makePaymentCommand));
  35.     }
  36. }
复制代码
5.2 接口测试

利用Knife4j对SaleController接口举行测试,简朴验证后端业务逻辑。
5.2.1 makeNewSale接口

该接口现在只是初始化currentSale数据。

5.2.2 enterItem接口

接口吸收产品编号和订购数量。
接口返回订单和订购商品聚集的json数据。

5.2.3 endSale接口

该接口暂时未写太多业务逻辑,只是提交订单,更新订单状。
后期将会从redis中清除缓存currentSale,然后恒久化currentSale数据。

5.2.4 makePayment接口

接口吸收付出金额和付出方式两个参数。
接口返回订单和付出json数据。

本章条记基于上篇的分析与设计模子,编写DDD架构底子办法层、领域层、应用层和接口层的代码。实现了收银用例的4个主要步骤makeNewSale、enterItem、endSale和makePayment。
下一篇条记将应用适配器模式调用付出宝沙箱付出接口。
本条记项目堆栈地点:
https://gitcode.com/tiger2704/isdp-boot3/tree/isdp010

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

梦见你的名字

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表