信息体系开发实践 | 系列文章传送门
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 {
- @Override
- public 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 {
- @Autowired
- private IProductService productService;
- @Autowired
- private ISaleService saleService;
- @Autowired
- private SaleFactory saleFactory;
- @Autowired
- private 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获取Sale
- Payment 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 {
- @Autowired
- private 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
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |