欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > ISDP010_基于DDD架构实现收银用例主成功场景

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

2025/4/22 7:29:25 来源:https://blog.csdn.net/tiger2704/article/details/144678961  浏览:    关键词:ISDP010_基于DDD架构实现收银用例主成功场景

信息系统开发实践 | 系列文章传送门
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。

        <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.20</version></dependency>

编写SaleFactory,实例化Sale并设置初始化值。

package edu.scau.mis.pos.infrastructure.factory;import cn.hutool.core.util.IdUtil;
import edu.scau.mis.pos.domain.entity.Sale;
import edu.scau.mis.pos.domain.enums.SaleStatusEnum;
import org.springframework.stereotype.Component;import java.math.BigDecimal;
import java.util.Date;/*** Sale工厂类*/
@Component
public class SaleFactory {public Sale initSale(){Sale sale = new Sale();sale.setSaleNo("so-" + IdUtil.getSnowflakeNextId());sale.setSaleStatus(SaleStatusEnum.CREATED);sale.setTotalAmount(BigDecimal.ZERO);sale.setTotalQuantity(0);sale.setSaleTime(new Date());return sale;}}

3 领域层

在DDD架构中,领域层是重点关注层。

为了简化Setter和getter编写,引入了Lombok。

        <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>

3.1 SaleProduct实体类

SaleProduct实体类包含了业务逻辑方法getSubTotal,计算每个订单明细的小计。

package edu.scau.mis.pos.domain.entity;import edu.scau.mis.pos.domain.enums.SaleProductStatusEnum;
import lombok.Data;import java.math.BigDecimal;/*** 订单-产品明细实体类*/
@Data
public class SaleProduct {private Long saleProductId;private Long saleId;private Long productId;private Product product;private Integer saleQuantity;private BigDecimal salePrice;private SaleProductStatusEnum saleProductStatus;/*** 计算小计* @return*/public BigDecimal getSubTotal() {return salePrice.multiply(new BigDecimal(saleQuantity));}
}

3.2 支付实体类

支付类暂时还没有写业务逻辑方法。后期考虑通过适配器,连接第三方支付。

package edu.scau.mis.pos.domain.entity;import com.fasterxml.jackson.annotation.JsonFormat;
import edu.scau.mis.pos.domain.enums.PaymentStatusEnum;
import edu.scau.mis.pos.domain.enums.PaymentStrategyEnum;
import lombok.Data;import java.math.BigDecimal;
import java.util.Date;/*** 支付实体类*/
@Data
public class Payment {private Long paymentId;private Long paymentSaleId;private PaymentStrategyEnum paymentStrategy;private String paymentNo;private BigDecimal paymentAmount;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date paymentTime;private PaymentStatusEnum paymentStatus;}

3.3 Sale聚合根

Sale类是收银领域层的聚合根。

该类内聚了两个业务逻辑方法,分别为添加订单明细和计算总金额。

package edu.scau.mis.pos.domain.entity;import com.fasterxml.jackson.annotation.JsonFormat;
import edu.scau.mis.pos.domain.enums.SaleStatusEnum;
import lombok.Data;import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;/*** 销售实体类* 聚合根*/
@Data
public class Sale {private Long saleId;private String saleNo;private BigDecimal totalAmount;private Integer totalQuantity;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date saleTime;private Payment payment;private List<SaleProduct> saleProducts = new ArrayList<>();private SaleStatusEnum saleStatus;/*** 计算总金额* @return*/public BigDecimal getTotal(){totalAmount = BigDecimal.ZERO;totalQuantity = 0;for(SaleProduct saleProduct: saleProducts){totalAmount = totalAmount.add(saleProduct.getSubTotal());totalQuantity = totalQuantity + saleProduct.getSaleQuantity();}return totalAmount;}/*** 添加订单明细* @param product* @param saleQuantity* @return*/public List<SaleProduct> makeLineItem(Product product, Integer saleQuantity) {// 判断商品是否已录入,未录入则新增。已录入则修改数量。if(!isEntered(product.getProductSn(),saleQuantity)){SaleProduct saleProduct = new SaleProduct();saleProduct.setProduct(product);saleProduct.setSaleQuantity(saleQuantity);saleProduct.setSalePrice(product.getProductPrice());saleProducts.add(saleProduct);}return saleProducts;}/*** 判断商品是否已录入* 业务逻辑:如果已录入,则修改数量,否则添加saleLineItem* @param productSn* @param saleQuantity* @return*/private boolean isEntered(String productSn, Integer saleQuantity){boolean flag = false;for(SaleProduct sp : saleProducts){if(productSn.equals(sp.getProduct().getProductSn())) {flag = true;Integer quantityOriginal = sp.getSaleQuantity();sp.setSaleQuantity(quantityOriginal + saleQuantity);}}return flag;}
}

3.4 领域服务类SaleService

主要用于生成支付功能。ISDP项目POS系统设计支持挂单功能。

package edu.scau.mis.pos.domain.service.impl;import cn.hutool.core.util.IdUtil;
import edu.scau.mis.pos.domain.entity.Payment;
import edu.scau.mis.pos.domain.entity.Sale;
import edu.scau.mis.pos.domain.enums.PaymentStatusEnum;
import edu.scau.mis.pos.domain.enums.PaymentStrategyEnum;
import edu.scau.mis.pos.domain.service.ISaleService;
import org.springframework.stereotype.Service;import java.math.BigDecimal;
import java.util.Date;/*** 领域服务*/
@Service
public class SaleServiceImpl implements ISaleService {@Overridepublic Payment makePayment(Sale sale, String paymentStrategy, BigDecimal paymentAmount) {Payment payment = new Payment();payment.setPaymentStrategy(PaymentStrategyEnum.valueOf(paymentStrategy));payment.setPaymentNo(paymentStrategy + "-" + IdUtil.getSnowflakeNextId());payment.setPaymentAmount(paymentAmount);payment.setPaymentTime(new Date());payment.setPaymentStatus(PaymentStatusEnum.PAID);// TODO: 根据不同支付策略调用不同外部接口return payment;}
}

3.5 其他

Domain层还有仓库、枚举等包。由于暂时还没有使用数据库和Redis,仓库代码暂时没写。

写了一些枚举类。由于只是教学项目,没有设计过多状态。

package edu.scau.mis.pos.domain.enums;/*** 订单状态枚举*/
public enum SaleStatusEnum {CREATED("0","已预订"),SUBMITTED("1","已提交"),PAID("2","已支付");private String value;private String label;SaleStatusEnum(String value, String label) {this.value = value;this.label = label;}public String getLabel() {return label;}public String getValue() {return value;}/*** 根据匹配value的值获取Label** @param value* @return*/public static String getLabelByValue(String value){for (SaleStatusEnum s : SaleStatusEnum.values()) {if(value.equals(s.getValue())){return s.getLabel();}}return "";}/*** 获取StatusEnum** @param value* @return*/public static SaleStatusEnum getStatusEnum(String value){for (SaleStatusEnum s : SaleStatusEnum.values()) {if(value.equals(s.getValue())){return s;}}return null;}
}

支付策略枚举类

package edu.scau.mis.pos.domain.enums;/*** 支付策略枚举*/
public enum PaymentStrategyEnum {WECHAT("wechat","微信支付"),ALIPAY("alipay","支付宝"),CASH("cash","现金");private String value;private String label;PaymentStrategyEnum(String value, String label) {this.value = value;this.label = label;}public String getLabel() {return label;}public String getValue() {return value;}/*** 根据匹配value的值获取Label** @param value* @return*/public static String getLabelByValue(String value){for (PaymentStrategyEnum s : PaymentStrategyEnum.values()) {if(value.equals(s.getValue())){return s.getLabel();}}return "";}/*** 获取StatusEnum** @param value* @return*/public static PaymentStrategyEnum getStrategyEnum(String value){for (PaymentStrategyEnum s : PaymentStrategyEnum.values()) {if(value.equals(s.getValue())){return s;}}return null;}
}

4 应用层

4.1 应用服务类SaleApplicationService

编写SaleApplicationService类。该类主要负责跨领域协作。

目前主要就两个领域Sale(SaleProduct、Payment)和Product(Category)。

如果使用微服务,可以分别针对这两个领域创建两个微服务模块。

package edu.scau.mis.pos.application.service;import edu.scau.mis.pos.application.assembler.SaleAssembler;
import edu.scau.mis.pos.application.dto.command.EnterItemCommand;
import edu.scau.mis.pos.application.dto.command.MakePaymentCommand;
import edu.scau.mis.pos.application.dto.vo.*;
import edu.scau.mis.pos.domain.entity.Payment;
import edu.scau.mis.pos.domain.entity.Product;
import edu.scau.mis.pos.domain.entity.Sale;
import edu.scau.mis.pos.domain.entity.SaleProduct;
import edu.scau.mis.pos.domain.enums.SaleStatusEnum;
import edu.scau.mis.pos.domain.service.IProductService;
import edu.scau.mis.pos.infrastructure.factory.SaleFactory;
import edu.scau.mis.pos.domain.service.ISaleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class SaleApplicationService {@Autowiredprivate IProductService productService;@Autowiredprivate ISaleService saleService;@Autowiredprivate SaleFactory saleFactory;@Autowiredprivate SaleAssembler saleAssembler;private Sale currentSale; // 后期改成Redis缓存CurrentSale/*** 开始一次新销售* @return*/public SaleVo makeNewSale(){SaleVo saleVo = new SaleVo();currentSale = saleFactory.initSale();// TODO:引入Redis缓存return saleAssembler.toSaleVo(currentSale);}/*** 录入商品* @param command* @return*/public SaleAndProductListVo enterItem(EnterItemCommand command){SaleAndProductListVo saleAndProductListVo = new SaleAndProductListVo();Product product = productService.selectProductBySn(command.getProductSn());List<SaleProduct> saleProducts = currentSale.makeLineItem(product, command.getSaleQuantity());currentSale.getTotal();List<SaleProductVo> saleProductVoList = saleProducts.stream().map(saleProduct -> new SaleProductVo(saleProduct.getProduct().getProductSn(), saleProduct.getProduct().getProductName(), saleProduct.getSalePrice(), saleProduct.getSaleQuantity())).toList();saleAndProductListVo.setSaleVo(saleAssembler.toSaleVo(currentSale));saleAndProductListVo.setSaleProductVoList(saleProductVoList);return saleAndProductListVo;}/*** 结束销售* 计算优惠、持久化订单等* @return*/public SaleVo endSale(){currentSale.setSaleStatus(SaleStatusEnum.SUBMITTED);// TODO: 持久化Sale和SaleProduct,添加事务注解return saleAssembler.toSaleVo(currentSale);}/*** 完成支付* @param command* @return*/public SaleAndPaymentVo makePayment(MakePaymentCommand command){SaleAndPaymentVo saleAndPaymentVo = new SaleAndPaymentVo();// TODO: 挂单--根据saleNo获取SalePayment payment = saleService.makePayment(currentSale,command.getPaymentStrategy(), command.getPaymentAmount());currentSale.setPayment(payment);currentSale.setSaleStatus(SaleStatusEnum.PAID);// TODO: 持久化Sale和Payment,添加事务注解// payment.setPaymentSaleId(sale.getSaleId());saleAndPaymentVo.setSaleVo(saleAssembler.toSaleVo(currentSale));saleAndPaymentVo.setPaymentVo(saleAssembler.toPaymentVo(payment));return saleAndPaymentVo;}
}

4.2 数据传输对象DTO

ISDP项目采用CQRS思想,该层编写大量的数据传输对象DTO。笔记只展示部分代码。详细参加项目仓库。

EnterItemCommand参考代码如下。

后期将使用Redis缓存currentSale,设计saleNo作为key。保留saleNo备用。

package edu.scau.mis.pos.application.dto.command;import lombok.Data;import java.io.Serializable;/*** 输入订单明细命令*/
@Data
public class EnterItemCommand implements Serializable {private String saleNo;private String productSn;private Integer saleQuantity;
}

MakePaymentCommand代码参考如下:

同上,saleNo暂时不需要。

package edu.scau.mis.pos.application.dto.command;import lombok.Data;import java.io.Serializable;
import java.math.BigDecimal;/*** 创建支付命令*/
@Data
public class MakePaymentCommand implements Serializable {private String saleNo;private BigDecimal paymentAmount;private String paymentStrategy;
}

SaleVo类

package edu.scau.mis.pos.application.dto.vo;import lombok.Data;import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class SaleVo implements Serializable {private String saleNo;private BigDecimal totalAmount;private Integer totalQuantity;private Date saleTime;private String saleStatus;
}

SaleAndPaymentVo

package edu.scau.mis.pos.application.dto.vo;import lombok.Data;import java.io.Serializable;
@Data
public class SaleAndPaymentVo implements Serializable {private SaleVo saleVo;private PaymentVo paymentVo;
}

4.3 对象转换器SaleAssembler

面向接口层主要使用DTO对象,因此不可避免涉及到DTO与领域对象的转换。

package edu.scau.mis.pos.application.assembler;import edu.scau.mis.pos.application.dto.vo.PaymentVo;
import edu.scau.mis.pos.application.dto.vo.SaleVo;
import edu.scau.mis.pos.domain.entity.Payment;
import edu.scau.mis.pos.domain.entity.Sale;
import org.springframework.stereotype.Component;/*** 订单转换器* 实现DTO与Entity的转换*/@Component
public class SaleAssembler {public SaleVo toSaleVo(Sale sale){SaleVo saleVo = new SaleVo();saleVo.setSaleNo(sale.getSaleNo());saleVo.setTotalAmount(sale.getTotalAmount());saleVo.setTotalQuantity(sale.getTotalQuantity());saleVo.setSaleTime(sale.getSaleTime());saleVo.setSaleStatus(sale.getSaleStatus().getLabel());return saleVo;}public PaymentVo toPaymentVo(Payment payment){PaymentVo paymentVo = new PaymentVo();paymentVo.setPaymentId(payment.getPaymentId());paymentVo.setPaymentSaleId(payment.getPaymentSaleId());paymentVo.setPaymentNo(payment.getPaymentNo());paymentVo.setPaymentAmount(payment.getPaymentAmount());paymentVo.setPaymentTime(payment.getPaymentTime());paymentVo.setPaymentStrategy(payment.getPaymentStrategy().getLabel());paymentVo.setPaymentStatus(payment.getPaymentStatus().getLabel());return paymentVo;}
}

5 接口层

5.1 Controller接口

SaleController参考如下:

package edu.scau.mis.web.controller;import edu.scau.mis.pos.application.dto.command.EnterItemCommand;
import edu.scau.mis.pos.application.dto.command.MakePaymentCommand;
import edu.scau.mis.pos.application.dto.vo.SaleAndPaymentVo;
import edu.scau.mis.pos.application.dto.vo.SaleAndProductListVo;
import edu.scau.mis.pos.application.dto.vo.SaleVo;
import edu.scau.mis.pos.application.service.SaleApplicationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/sale")
public class SaleController {@Autowiredprivate SaleApplicationService saleApplicationService;@GetMapping("/makeNewSale")public ResponseEntity<SaleVo> makeNewSale(){return ResponseEntity.ok(saleApplicationService.makeNewSale());}@PostMapping("/enterItem")public ResponseEntity<SaleAndProductListVo> enterItem(@RequestBody  EnterItemCommand enterItemCommand){return ResponseEntity.ok(saleApplicationService.enterItem(enterItemCommand));}@GetMapping("/endSale")public ResponseEntity<SaleVo> endSale(){return ResponseEntity.ok(saleApplicationService.endSale());}@PostMapping("/makePayment")public ResponseEntity<SaleAndPaymentVo> makePayment(@RequestBody MakePaymentCommand makePaymentCommand){return ResponseEntity.ok(saleApplicationService.makePayment(makePaymentCommand));}
}

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

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词