Commit fdfca502e5859011cf8bf41d8dad3665de069f6d

Authored by YunaiV
1 parent f93e2579

test: 完善 PR 修复回归测试并启用商城历史用例

启用并修复商品、DIY 页面及模板、交易订单测试,补齐测试表结构与依赖。
增加 CRM 删除日志上下文、优惠券模板已删除场景的回归测试,并对齐相关注释。
关联 #1313、#1315、#1317、#1319、#1321、#1323。
Showing 15 changed files with 913 additions and 546 deletions
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/product/CrmProductCategoryServiceImpl.java
... ... @@ -118,6 +118,7 @@ public class CrmProductCategoryServiceImpl implements CrmProductCategoryService
118 118 if (crmProductService.getProductByCategoryId(id) > 0) {
119 119 throw exception(PRODUCT_CATEGORY_USED);
120 120 }
  121 +
121 122 // 2. 删除
122 123 productCategoryMapper.deleteById(id);
123 124  
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/product/CrmProductServiceImpl.java
... ... @@ -135,9 +135,9 @@ public class CrmProductServiceImpl implements CrmProductService {
135 135 success = CRM_PRODUCT_DELETE_SUCCESS)
136 136 @CrmPermission(bizType = CrmBizTypeEnum.CRM_PRODUCT, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
137 137 public void deleteProduct(Long id) {
138   - // 校验存在
  138 + // 1.1 校验存在
139 139 CrmProductDO product = validateProductExists(id);
140   - // 校验未被合同/商机引用,避免删除后产生悬空引用
  140 + // 1.2 校验未被合同/商机引用,避免删除后产生悬空引用
141 141 if (contractService.getContractProductCountByProductId(id) > 0) {
142 142 throw exception(PRODUCT_DELETE_FAIL_CONTRACT_EXISTS);
143 143 }
... ... @@ -145,10 +145,10 @@ public class CrmProductServiceImpl implements CrmProductService {
145 145 throw exception(PRODUCT_DELETE_FAIL_BUSINESS_EXISTS);
146 146 }
147 147  
148   - // 删除
  148 + // 2. 删除
149 149 productMapper.deleteById(id);
150 150  
151   - // 记录操作日志上下文
  151 + // 3. 记录操作日志上下文
152 152 LogRecordContext.putVariable("product", product);
153 153 }
154 154  
... ...
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/product/CrmProductCategoryServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.service.product;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO;
  5 +import cn.iocoder.yudao.module.crm.dal.mysql.product.CrmProductCategoryMapper;
  6 +import com.mzt.logapi.context.LogRecordContext;
  7 +import org.junit.jupiter.api.AfterEach;
  8 +import org.junit.jupiter.api.BeforeEach;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.mockito.InjectMocks;
  11 +import org.mockito.Mock;
  12 +
  13 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  14 +import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
  15 +import static org.junit.jupiter.api.Assertions.*;
  16 +import static org.mockito.ArgumentMatchers.anyLong;
  17 +import static org.mockito.Mockito.*;
  18 +
  19 +/**
  20 + * {@link CrmProductCategoryServiceImpl} 的单元测试类
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +public class CrmProductCategoryServiceImplTest extends BaseMockitoUnitTest {
  25 +
  26 + @InjectMocks
  27 + private CrmProductCategoryServiceImpl productCategoryService;
  28 +
  29 + @Mock
  30 + private CrmProductCategoryMapper productCategoryMapper;
  31 + @Mock
  32 + private CrmProductService crmProductService;
  33 +
  34 + @BeforeEach
  35 + public void setUp() {
  36 + LogRecordContext.putEmptySpan();
  37 + }
  38 +
  39 + @AfterEach
  40 + public void tearDown() {
  41 + LogRecordContext.clear();
  42 + }
  43 +
  44 + @Test
  45 + public void testDeleteProductCategory_success() {
  46 + // mock 数据
  47 + CrmProductCategoryDO category = new CrmProductCategoryDO().setId(1L).setName("芋道分类").setParentId(0L);
  48 + when(productCategoryMapper.selectById(1L)).thenReturn(category);
  49 + // mock 方法(无子分类、未被产品使用)
  50 + when(productCategoryMapper.selectCountByParentId(1L)).thenReturn(0L);
  51 + when(crmProductService.getProductByCategoryId(1L)).thenReturn(0L);
  52 +
  53 + // 调用
  54 + productCategoryService.deleteProductCategory(1L);
  55 + // 断言
  56 + verify(productCategoryMapper).deleteById(1L);
  57 + // 断言操作日志上下文,为删除前的分类
  58 + CrmProductCategoryDO logCategory = (CrmProductCategoryDO) LogRecordContext.getVariable("productCategory");
  59 + assertSame(category, logCategory);
  60 + assertEquals("芋道分类", logCategory.getName());
  61 + }
  62 +
  63 + @Test
  64 + public void testDeleteProductCategory_notExists() {
  65 + // 调用,并断言异常
  66 + assertServiceException(() -> productCategoryService.deleteProductCategory(1L), PRODUCT_CATEGORY_NOT_EXISTS);
  67 + // 断言
  68 + verify(productCategoryMapper, never()).deleteById(anyLong());
  69 + assertNull(LogRecordContext.getVariable("productCategory"));
  70 + }
  71 +
  72 + @Test
  73 + public void testDeleteProductCategory_existsChildren() {
  74 + // mock 数据
  75 + when(productCategoryMapper.selectById(1L)).thenReturn(new CrmProductCategoryDO().setId(1L).setName("芋道分类"));
  76 + // mock 方法(存在子分类)
  77 + when(productCategoryMapper.selectCountByParentId(1L)).thenReturn(1L);
  78 +
  79 + // 调用,并断言异常
  80 + assertServiceException(() -> productCategoryService.deleteProductCategory(1L), PRODUCT_CATEGORY_EXISTS_CHILDREN);
  81 + // 断言
  82 + verify(productCategoryMapper, never()).deleteById(anyLong());
  83 + verifyNoInteractions(crmProductService);
  84 + assertNull(LogRecordContext.getVariable("productCategory"));
  85 + }
  86 +
  87 + @Test
  88 + public void testDeleteProductCategory_used() {
  89 + // mock 数据
  90 + when(productCategoryMapper.selectById(1L)).thenReturn(new CrmProductCategoryDO().setId(1L).setName("芋道分类"));
  91 + // mock 方法(被产品使用)
  92 + when(productCategoryMapper.selectCountByParentId(1L)).thenReturn(0L);
  93 + when(crmProductService.getProductByCategoryId(1L)).thenReturn(1L);
  94 +
  95 + // 调用,并断言异常
  96 + assertServiceException(() -> productCategoryService.deleteProductCategory(1L), PRODUCT_CATEGORY_USED);
  97 + // 断言
  98 + verify(productCategoryMapper, never()).deleteById(anyLong());
  99 + assertNull(LogRecordContext.getVariable("productCategory"));
  100 + }
  101 +
  102 +}
... ...
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/product/CrmProductServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.service.product;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductDO;
  5 +import cn.iocoder.yudao.module.crm.dal.mysql.product.CrmProductMapper;
  6 +import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
  7 +import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
  8 +import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
  9 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  10 +import com.mzt.logapi.context.LogRecordContext;
  11 +import org.junit.jupiter.api.AfterEach;
  12 +import org.junit.jupiter.api.BeforeEach;
  13 +import org.junit.jupiter.api.Test;
  14 +import org.mockito.InjectMocks;
  15 +import org.mockito.Mock;
  16 +
  17 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  18 +import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
  19 +import static org.junit.jupiter.api.Assertions.*;
  20 +import static org.mockito.ArgumentMatchers.anyLong;
  21 +import static org.mockito.Mockito.*;
  22 +
  23 +/**
  24 + * {@link CrmProductServiceImpl} 的单元测试类
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +public class CrmProductServiceImplTest extends BaseMockitoUnitTest {
  29 +
  30 + @InjectMocks
  31 + private CrmProductServiceImpl productService;
  32 +
  33 + @Mock
  34 + private CrmProductMapper productMapper;
  35 + @Mock
  36 + private CrmProductCategoryService productCategoryService;
  37 + @Mock
  38 + private CrmContractService contractService;
  39 + @Mock
  40 + private CrmBusinessService businessService;
  41 + @Mock
  42 + private CrmPermissionService permissionService;
  43 + @Mock
  44 + private AdminUserApi adminUserApi;
  45 +
  46 + @BeforeEach
  47 + public void setUp() {
  48 + LogRecordContext.putEmptySpan();
  49 + }
  50 +
  51 + @AfterEach
  52 + public void tearDown() {
  53 + LogRecordContext.clear();
  54 + }
  55 +
  56 + @Test
  57 + public void testDeleteProduct_success() {
  58 + // mock 数据
  59 + CrmProductDO product = new CrmProductDO().setId(1L).setName("芋道产品");
  60 + when(productMapper.selectById(1L)).thenReturn(product);
  61 + // mock 方法(未被合同、商机引用)
  62 + when(contractService.getContractProductCountByProductId(1L)).thenReturn(0L);
  63 + when(businessService.getBusinessProductCountByProductId(1L)).thenReturn(0L);
  64 +
  65 + // 调用
  66 + productService.deleteProduct(1L);
  67 + // 断言
  68 + verify(productMapper).deleteById(1L);
  69 + // 断言操作日志上下文,为删除前的产品
  70 + CrmProductDO logProduct = (CrmProductDO) LogRecordContext.getVariable("product");
  71 + assertSame(product, logProduct);
  72 + assertEquals("芋道产品", logProduct.getName());
  73 + }
  74 +
  75 + @Test
  76 + public void testDeleteProduct_notExists() {
  77 + // 调用,并断言异常
  78 + assertServiceException(() -> productService.deleteProduct(1L), PRODUCT_NOT_EXISTS);
  79 + // 断言
  80 + verify(productMapper, never()).deleteById(anyLong());
  81 + assertNull(LogRecordContext.getVariable("product"));
  82 + }
  83 +
  84 + @Test
  85 + public void testDeleteProduct_contractExists() {
  86 + // mock 数据
  87 + when(productMapper.selectById(1L)).thenReturn(new CrmProductDO().setId(1L).setName("芋道产品"));
  88 + // mock 方法(被合同引用)
  89 + when(contractService.getContractProductCountByProductId(1L)).thenReturn(1L);
  90 +
  91 + // 调用,并断言异常
  92 + assertServiceException(() -> productService.deleteProduct(1L), PRODUCT_DELETE_FAIL_CONTRACT_EXISTS);
  93 + // 断言
  94 + verify(productMapper, never()).deleteById(anyLong());
  95 + assertNull(LogRecordContext.getVariable("product"));
  96 + }
  97 +
  98 + @Test
  99 + public void testDeleteProduct_businessExists() {
  100 + // mock 数据
  101 + when(productMapper.selectById(1L)).thenReturn(new CrmProductDO().setId(1L).setName("芋道产品"));
  102 + // mock 方法(被商机引用)
  103 + when(contractService.getContractProductCountByProductId(1L)).thenReturn(0L);
  104 + when(businessService.getBusinessProductCountByProductId(1L)).thenReturn(1L);
  105 +
  106 + // 调用,并断言异常
  107 + assertServiceException(() -> productService.deleteProduct(1L), PRODUCT_DELETE_FAIL_BUSINESS_EXISTS);
  108 + // 断言
  109 + verify(productMapper, never()).deleteById(anyLong());
  110 + assertNull(LogRecordContext.getVariable("product"));
  111 + }
  112 +
  113 +}
... ...
yudao-module-mall/yudao-module-product/pom.xml
... ... @@ -24,7 +24,7 @@
24 24 <version>${revision}</version>
25 25 </dependency>
26 26  
27   - <!-- trade-api:配送方式枚举 DeliveryTypeEnum 校验(A68) -->
  27 + <!-- trade-api:配送方式枚举 DeliveryTypeEnum 校验 -->
28 28 <dependency>
29 29 <groupId>cn.iocoder.boot</groupId>
30 30 <artifactId>yudao-module-trade-api</artifactId>
... ...
yudao-module-mall/yudao-module-product/src/test/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImplTest.java
... ... @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.product.service.spu;
3 3 import cn.hutool.core.date.DateUtil;
4 4 import cn.hutool.core.map.MapUtil;
5 5 import cn.hutool.core.util.RandomUtil;
6   -import cn.iocoder.yudao.framework.common.exception.ServiceException;
7 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
8 8 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
9 9 import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSkuSaveReqVO;
10 10 import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuPageReqVO;
... ... @@ -17,29 +17,37 @@ import cn.iocoder.yudao.module.product.service.category.ProductCategoryServiceIm
17 17 import cn.iocoder.yudao.module.product.service.property.ProductPropertyService;
18 18 import cn.iocoder.yudao.module.product.service.property.ProductPropertyValueService;
19 19 import cn.iocoder.yudao.module.product.service.sku.ProductSkuServiceImpl;
  20 +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
20 21 import com.google.common.collect.Lists;
21 22 import jakarta.annotation.Resource;
22   -import org.junit.jupiter.api.Assertions;
23   -import org.junit.jupiter.api.Disabled;
  23 +import jakarta.validation.ConstraintViolation;
  24 +import jakarta.validation.Validation;
  25 +import jakarta.validation.Validator;
24 26 import org.junit.jupiter.api.Test;
25 27 import org.springframework.context.annotation.Import;
26 28 import org.springframework.test.context.bean.override.mockito.MockitoBean;
27 29  
28 30 import java.math.RoundingMode;
29   -import java.util.ArrayList;
30   -import java.util.Date;
31   -import java.util.List;
32   -import java.util.Map;
  31 +import java.util.*;
  32 +import java.util.function.Consumer;
33 33 import java.util.stream.Collectors;
34 34 import java.util.stream.Stream;
35 35  
  36 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  37 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  38 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
  39 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
36 40 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
37 41 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
  42 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
38 43 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  44 +import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS;
  45 +import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_RECYCLE;
  46 +import static java.util.Collections.singletonList;
39 47 import static org.assertj.core.util.Lists.newArrayList;
40   -import static org.junit.jupiter.api.Assertions.assertEquals;
  48 +import static org.junit.jupiter.api.Assertions.*;
41 49 import static org.mockito.ArgumentMatchers.eq;
42   -import static org.mockito.Mockito.when;
  50 +import static org.mockito.Mockito.*;
43 51  
44 52 // TODO @芋艿:review 下单元测试
45 53  
... ... @@ -48,10 +56,12 @@ import static org.mockito.Mockito.when;
48 56 *
49 57 * @author 芋道源码
50 58 */
51   -@Disabled // TODO 芋艿:后续 fix 补充的单测
  59 +// DONE 芋艿:后续 fix 补充的单测
52 60 @Import(ProductSpuServiceImpl.class)
53 61 public class ProductSpuServiceImplTest extends BaseDbUnitTest {
54 62  
  63 + private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
  64 +
55 65 @Resource
56 66 private ProductSpuServiceImpl productSpuService;
57 67  
... ... @@ -84,18 +94,7 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
84 94 @Test
85 95 public void testCreateSpu_success() {
86 96 // 准备参数
87   - ProductSkuSaveReqVO skuCreateOrUpdateReqVO = randomPojo(ProductSkuSaveReqVO.class, o->{
88   - // 限制范围为正整数
89   - o.setCostPrice(generaInt());
90   - o.setPrice(generaInt());
91   - o.setMarketPrice(generaInt());
92   - o.setStock(generaInt());
93   - o.setFirstBrokeragePrice(generaInt());
94   - o.setSecondBrokeragePrice(generaInt());
95   - // 限制分数为两位数
96   - o.setWeight(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP));
97   - o.setVolume(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP));
98   - });
  97 + ProductSkuSaveReqVO skuCreateOrUpdateReqVO = randomSkuSaveReqVO();
99 98 ProductSpuSaveReqVO createReqVO = randomPojo(ProductSpuSaveReqVO.class,o->{
100 99 o.setCategoryId(generateId());
101 100 o.setBrandId(generateId());
... ... @@ -104,291 +103,215 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
104 103 o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
105 104 o.setSkus(newArrayList(skuCreateOrUpdateReqVO,skuCreateOrUpdateReqVO,skuCreateOrUpdateReqVO));
106 105 });
  106 + // mock 方法(分类层级)
107 107 when(categoryService.getCategoryLevel(eq(createReqVO.getCategoryId()))).thenReturn(2);
108   - Long spu = productSpuService.createSpu(createReqVO);
109   - ProductSpuDO productSpuDO = productSpuMapper.selectById(spu);
110   - assertPojoEquals(createReqVO, productSpuDO);
  108 +
  109 + // 调用
  110 + Long spuId = productSpuService.createSpu(createReqVO);
  111 + // 断言:销量、浏览量为初始值
  112 + ProductSpuDO productSpuDO = productSpuMapper.selectById(spuId);
  113 + assertPojoEquals(createReqVO, productSpuDO, "salesCount", "browseCount");
  114 + assertEquals(ProductSpuStatusEnum.ENABLE.getStatus(), productSpuDO.getStatus());
  115 + assertEquals(0, productSpuDO.getSalesCount());
  116 + assertEquals(0, productSpuDO.getBrowseCount());
  117 + // 断言:基于 SKU 初始化价格、库存
  118 + assertEquals(skuCreateOrUpdateReqVO.getPrice(), productSpuDO.getPrice());
  119 + assertEquals(skuCreateOrUpdateReqVO.getMarketPrice(), productSpuDO.getMarketPrice());
  120 + assertEquals(skuCreateOrUpdateReqVO.getCostPrice(), productSpuDO.getCostPrice());
  121 + assertEquals(skuCreateOrUpdateReqVO.getStock() * 3, productSpuDO.getStock());
  122 + // 断言:创建 SKU
  123 + verify(productSkuService).createSkuList(eq(spuId), eq(createReqVO.getSkus()));
111 124 }
112 125  
113 126 @Test
114 127 public void testUpdateSpu_success() {
  128 + // mock 数据
  129 + ProductSpuDO dbSpu = randomSpuDO();
  130 + productSpuMapper.insert(dbSpu);
115 131 // 准备参数
116   - ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class,o->{
117   - o.setCategoryId(generateId());
118   - o.setBrandId(generateId());
119   - o.setDeliveryTemplateId(generateId());
120   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
121   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
122   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
123   - o.setPrice(generaInt()); // 限制范围为正整数
124   - o.setMarketPrice(generaInt()); // 限制范围为正整数
125   - o.setCostPrice(generaInt()); // 限制范围为正整数
126   - o.setStock(generaInt()); // 限制范围为正整数
127   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
128   - o.setSalesCount(generaInt()); // 限制范围为正整数
129   - o.setBrowseCount(generaInt()); // 限制范围为正整数
130   - });
131   - productSpuMapper.insert(createReqVO);
132   - // 准备参数
133   - ProductSkuSaveReqVO skuCreateOrUpdateReqVO = randomPojo(ProductSkuSaveReqVO.class, o->{
134   - // 限制范围为正整数
135   - o.setCostPrice(generaInt());
136   - o.setPrice(generaInt());
137   - o.setMarketPrice(generaInt());
138   - o.setStock(generaInt());
139   - o.setFirstBrokeragePrice(generaInt());
140   - o.setSecondBrokeragePrice(generaInt());
141   - // 限制分数为两位数
142   - o.setWeight(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP));
143   - o.setVolume(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP));
144   - });
145   - // 准备参数
  132 + ProductSkuSaveReqVO skuCreateOrUpdateReqVO = randomSkuSaveReqVO();
146 133 ProductSpuSaveReqVO reqVO = randomPojo(ProductSpuSaveReqVO.class, o -> {
147   - o.setId(createReqVO.getId()); // 设置更新的 ID
  134 + o.setId(dbSpu.getId()); // 设置更新的 ID
148 135 o.setCategoryId(generateId());
149 136 o.setBrandId(generateId());
150 137 o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
151 138 o.setGiveIntegral(generaInt()); // 限制范围为正整数
152 139 o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
153   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
154 140 o.setSalesCount(generaInt()); // 限制范围为正整数
155 141 o.setBrowseCount(generaInt()); // 限制范围为正整数
156 142 o.setSkus(newArrayList(skuCreateOrUpdateReqVO,skuCreateOrUpdateReqVO,skuCreateOrUpdateReqVO));
157 143 });
  144 + // mock 方法(分类层级)
158 145 when(categoryService.getCategoryLevel(eq(reqVO.getCategoryId()))).thenReturn(2);
  146 +
159 147 // 调用
160 148 productSpuService.updateSpu(reqVO);
161   - // 校验是否更新正确
  149 + // 校验是否更新正确:状态保持不变
162 150 ProductSpuDO spu = productSpuMapper.selectById(reqVO.getId()); // 获取最新的
163 151 assertPojoEquals(reqVO, spu);
  152 + assertEquals(dbSpu.getStatus(), spu.getStatus());
  153 + assertEquals(skuCreateOrUpdateReqVO.getStock() * 3, spu.getStock());
  154 + // 断言:更新 SKU
  155 + verify(productSkuService).updateSkuList(eq(reqVO.getId()), eq(reqVO.getSkus()));
164 156 }
165 157  
166 158 @Test
167   - public void testValidateSpuExists_exception() {
  159 + public void testUpdateSpu_notExists() {
  160 + // 准备参数
168 161 ProductSpuSaveReqVO reqVO = randomPojo(ProductSpuSaveReqVO.class);
169   - // 调用
170   - Assertions.assertThrows(ServiceException.class, () -> productSpuService.updateSpu(reqVO));
  162 +
  163 + // 调用, 并断言异常
  164 + assertServiceException(() -> productSpuService.updateSpu(reqVO), SPU_NOT_EXISTS);
171 165 }
172 166  
173 167 @Test
174   - void deleteSpu() {
175   - // 准备参数
176   - ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class,o->{
177   - o.setCategoryId(generateId());
178   - o.setBrandId(generateId());
179   - o.setDeliveryTemplateId(generateId());
180   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
181   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
182   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
183   - o.setPrice(generaInt()); // 限制范围为正整数
184   - o.setMarketPrice(generaInt()); // 限制范围为正整数
185   - o.setCostPrice(generaInt()); // 限制范围为正整数
186   - o.setStock(generaInt()); // 限制范围为正整数
187   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
188   - o.setSalesCount(generaInt()); // 限制范围为正整数
189   - o.setBrowseCount(generaInt()); // 限制范围为正整数
190   - o.setStatus(-1); // 加入回收站才可删除
191   - });
192   - productSpuMapper.insert(createReqVO);
  168 + public void testValidateDeliveryTypes_success() {
  169 + // 调用,并断言:快递发货、用户自提,及其组合
  170 + assertEquals(Collections.emptySet(), validateDeliveryTypes(singletonList(DeliveryTypeEnum.EXPRESS.getType())));
  171 + assertEquals(Collections.emptySet(), validateDeliveryTypes(singletonList(DeliveryTypeEnum.PICK_UP.getType())));
  172 + assertEquals(Collections.emptySet(), validateDeliveryTypes(Arrays.asList(
  173 + DeliveryTypeEnum.EXPRESS.getType(), DeliveryTypeEnum.PICK_UP.getType())));
  174 + }
  175 +
  176 + @Test
  177 + public void testValidateDeliveryTypes_invalid() {
  178 + // 调用,并断言:不在枚举范围内
  179 + Set<String> invalidMessages = Collections.singleton("配送方式不正确");
  180 + assertEquals(invalidMessages, validateDeliveryTypes(singletonList(0)));
  181 + assertEquals(invalidMessages, validateDeliveryTypes(singletonList(3)));
  182 + assertEquals(invalidMessages, validateDeliveryTypes(singletonList(-1)));
  183 + assertEquals(invalidMessages, validateDeliveryTypes(Arrays.asList(DeliveryTypeEnum.EXPRESS.getType(), 3)));
  184 + assertEquals(invalidMessages, validateDeliveryTypes(Arrays.asList(DeliveryTypeEnum.EXPRESS.getType(), null)));
  185 + // 调用,并断言:为空
  186 + Set<String> emptyMessages = Collections.singleton("配送方式不能为空");
  187 + assertEquals(emptyMessages, validateDeliveryTypes(null));
  188 + assertEquals(emptyMessages, validateDeliveryTypes(Collections.emptyList()));
  189 + }
  190 +
  191 + @Test
  192 + public void testDeleteSpu_success() {
  193 + // mock 数据
  194 + ProductSpuDO dbSpu = randomSpuDO(o -> o.setStatus(ProductSpuStatusEnum.RECYCLE.getStatus())); // 加入回收站才可删除
  195 + productSpuMapper.insert(dbSpu);
193 196  
194 197 // 调用
195   - productSpuService.deleteSpu(createReqVO.getId());
  198 + productSpuService.deleteSpu(dbSpu.getId());
  199 + // 断言
  200 + assertNull(productSpuMapper.selectById(dbSpu.getId()));
  201 + verify(productSkuService).deleteSkuBySpuId(eq(dbSpu.getId()));
  202 + }
  203 +
  204 + @Test
  205 + public void testDeleteSpu_notRecycle() {
  206 + // mock 数据
  207 + ProductSpuDO dbSpu = randomSpuDO(o -> o.setStatus(ProductSpuStatusEnum.DISABLE.getStatus()));
  208 + productSpuMapper.insert(dbSpu);
196 209  
197   - Assertions.assertNull(productSpuMapper.selectById(createReqVO.getId()));
  210 + // 调用, 并断言异常
  211 + assertServiceException(() -> productSpuService.deleteSpu(dbSpu.getId()), SPU_NOT_RECYCLE);
  212 + // 断言
  213 + assertNotNull(productSpuMapper.selectById(dbSpu.getId()));
  214 + verify(productSkuService, never()).deleteSkuBySpuId(any());
198 215 }
199 216  
200 217 @Test
201   - void getSpu() {
202   - // 准备参数
203   - ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class,o->{
204   - o.setCategoryId(generateId());
205   - o.setBrandId(generateId());
206   - o.setDeliveryTemplateId(generateId());
207   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
208   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
209   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
210   - o.setPrice(generaInt()); // 限制范围为正整数
211   - o.setMarketPrice(generaInt()); // 限制范围为正整数
212   - o.setCostPrice(generaInt()); // 限制范围为正整数
213   - o.setStock(generaInt()); // 限制范围为正整数
214   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
215   - o.setSalesCount(generaInt()); // 限制范围为正整数
216   - o.setBrowseCount(generaInt()); // 限制范围为正整数
217   - });
218   - productSpuMapper.insert(createReqVO);
  218 + public void testGetSpu() {
  219 + // mock 数据
  220 + ProductSpuDO dbSpu = randomSpuDO();
  221 + productSpuMapper.insert(dbSpu);
219 222  
220   - ProductSpuDO spu = productSpuService.getSpu(createReqVO.getId());
221   - assertPojoEquals(createReqVO, spu);
  223 + // 调用
  224 + ProductSpuDO spu = productSpuService.getSpu(dbSpu.getId());
  225 + // 断言
  226 + assertPojoEquals(dbSpu, spu);
222 227 }
223 228  
224 229 @Test
225   - void getSpuList() {
226   - // 准备参数
227   - ArrayList<ProductSpuDO> createReqVOs = Lists.newArrayList(randomPojo(ProductSpuDO.class,o->{
228   - o.setCategoryId(generateId());
229   - o.setBrandId(generateId());
230   - o.setDeliveryTemplateId(generateId());
231   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
232   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
233   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
234   - o.setPrice(generaInt()); // 限制范围为正整数
235   - o.setMarketPrice(generaInt()); // 限制范围为正整数
236   - o.setCostPrice(generaInt()); // 限制范围为正整数
237   - o.setStock(generaInt()); // 限制范围为正整数
238   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
239   - o.setSalesCount(generaInt()); // 限制范围为正整数
240   - o.setBrowseCount(generaInt()); // 限制范围为正整数
241   - }), randomPojo(ProductSpuDO.class,o->{
242   - o.setCategoryId(generateId());
243   - o.setBrandId(generateId());
244   - o.setDeliveryTemplateId(generateId());
245   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
246   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
247   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
248   - o.setPrice(generaInt()); // 限制范围为正整数
249   - o.setMarketPrice(generaInt()); // 限制范围为正整数
250   - o.setCostPrice(generaInt()); // 限制范围为正整数
251   - o.setStock(generaInt()); // 限制范围为正整数
252   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
253   - o.setSalesCount(generaInt()); // 限制范围为正整数
254   - o.setBrowseCount(generaInt()); // 限制范围为正整数
255   - }));
256   - productSpuMapper.insertBatch(createReqVOs);
  230 + public void testGetSpuList() {
  231 + // mock 数据
  232 + ArrayList<ProductSpuDO> dbSpus = Lists.newArrayList(randomSpuDO(), randomSpuDO());
  233 + productSpuMapper.insertBatch(dbSpus);
  234 + // 准备参数:倒序传入
  235 + List<Long> ids = Arrays.asList(dbSpus.get(1).getId(), dbSpus.get(0).getId());
257 236  
258 237 // 调用
259   - List<ProductSpuDO> spuList = productSpuService.getSpuList(createReqVOs.stream().map(ProductSpuDO::getId).collect(Collectors.toList()));
260   - Assertions.assertIterableEquals(createReqVOs, spuList);
  238 + List<ProductSpuDO> spuList = productSpuService.getSpuList(ids);
  239 + // 断言:按照 ids 顺序返回
  240 + assertEquals(ids, convertList(spuList, ProductSpuDO::getId));
  241 + assertPojoEquals(dbSpus.get(1), spuList.get(0));
  242 + assertPojoEquals(dbSpus.get(0), spuList.get(1));
261 243 }
262 244  
263 245 @Test
264   - void getSpuPage_alarmStock_empty() {
  246 + public void testGetSpuPage_alertStockEmpty() {
  247 + // mock 数据
  248 + ArrayList<ProductSpuDO> dbSpus = Lists.newArrayList(randomSpuDO(o -> o.setStock(11)),
  249 + randomSpuDO(o -> o.setStock(11)));
  250 + productSpuMapper.insertBatch(dbSpus);
265 251 // 准备参数
266   - ArrayList<ProductSpuDO> createReqVOs = Lists.newArrayList(randomPojo(ProductSpuDO.class,o->{
267   - o.setCategoryId(generateId());
268   - o.setBrandId(generateId());
269   - o.setDeliveryTemplateId(generateId());
270   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
271   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
272   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
273   - o.setPrice(generaInt()); // 限制范围为正整数
274   - o.setMarketPrice(generaInt()); // 限制范围为正整数
275   - o.setCostPrice(generaInt()); // 限制范围为正整数
276   - o.setStock(11); // 限制范围为正整数
277   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
278   - o.setSalesCount(generaInt()); // 限制范围为正整数
279   - o.setBrowseCount(generaInt()); // 限制范围为正整数
280   - }), randomPojo(ProductSpuDO.class,o->{
281   - o.setCategoryId(generateId());
282   - o.setBrandId(generateId());
283   - o.setDeliveryTemplateId(generateId());
284   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
285   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
286   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
287   - o.setPrice(generaInt()); // 限制范围为正整数
288   - o.setMarketPrice(generaInt()); // 限制范围为正整数
289   - o.setCostPrice(generaInt()); // 限制范围为正整数
290   - o.setStock(11); // 限制范围为正整数
291   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
292   - o.setSalesCount(generaInt()); // 限制范围为正整数
293   - o.setBrowseCount(generaInt()); // 限制范围为正整数
294   - }));
295   - productSpuMapper.insertBatch(createReqVOs);
296   - // 调用
297 252 ProductSpuPageReqVO productSpuPageReqVO = new ProductSpuPageReqVO();
298 253 productSpuPageReqVO.setTabType(ProductSpuPageReqVO.ALERT_STOCK);
299 254  
  255 + // 调用
300 256 PageResult<ProductSpuDO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
301   -
302   - PageResult<Object> result = PageResult.empty();
303   - Assertions.assertIterableEquals(result.getList(), spuPage.getList());
304   - assertEquals(spuPage.getTotal(), result.getTotal());
  257 + // 断言
  258 + assertEquals(0, spuPage.getTotal());
  259 + assertEquals(0, spuPage.getList().size());
305 260 }
306 261  
307 262 @Test
308   - void getSpuPage_alarmStock() {
  263 + public void testGetSpuPage_alertStock() {
  264 + // mock 数据
  265 + ArrayList<ProductSpuDO> dbSpus = Lists.newArrayList(randomSpuDO(o -> o.setStock(5)),
  266 + randomSpuDO(o -> o.setStock(9)));
  267 + productSpuMapper.insertBatch(dbSpus);
  268 + // 测试 stock 不匹配
  269 + productSpuMapper.insert(randomSpuDO(o -> o.setStock(11)));
  270 + // 测试 status 不匹配(回收站)
  271 + productSpuMapper.insert(randomSpuDO(o -> o.setStock(5).setStatus(ProductSpuStatusEnum.RECYCLE.getStatus())));
309 272 // 准备参数
310   - ArrayList<ProductSpuDO> createReqVOs = Lists.newArrayList(randomPojo(ProductSpuDO.class,o->{
311   - o.setCategoryId(generateId());
312   - o.setBrandId(generateId());
313   - o.setDeliveryTemplateId(generateId());
314   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
315   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
316   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
317   - o.setPrice(generaInt()); // 限制范围为正整数
318   - o.setMarketPrice(generaInt()); // 限制范围为正整数
319   - o.setCostPrice(generaInt()); // 限制范围为正整数
320   - o.setStock(5); // 限制范围为正整数
321   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
322   - o.setSalesCount(generaInt()); // 限制范围为正整数
323   - o.setBrowseCount(generaInt()); // 限制范围为正整数
324   - }), randomPojo(ProductSpuDO.class,o->{
325   - o.setCategoryId(generateId());
326   - o.setBrandId(generateId());
327   - o.setDeliveryTemplateId(generateId());
328   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
329   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
330   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
331   - o.setPrice(generaInt()); // 限制范围为正整数
332   - o.setMarketPrice(generaInt()); // 限制范围为正整数
333   - o.setCostPrice(generaInt()); // 限制范围为正整数
334   - o.setStock(9); // 限制范围为正整数
335   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
336   - o.setSalesCount(generaInt()); // 限制范围为正整数
337   - o.setBrowseCount(generaInt()); // 限制范围为正整数
338   - }));
339   - productSpuMapper.insertBatch(createReqVOs);
340   - // 调用
341 273 ProductSpuPageReqVO productSpuPageReqVO = new ProductSpuPageReqVO();
342 274 productSpuPageReqVO.setTabType(ProductSpuPageReqVO.ALERT_STOCK);
  275 +
  276 + // 调用
343 277 PageResult<ProductSpuDO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
344   - assertEquals(createReqVOs.size(), spuPage.getTotal());
  278 + // 断言
  279 + assertEquals(dbSpus.size(), spuPage.getTotal());
  280 + assertEquals(convertSet(dbSpus, ProductSpuDO::getId), convertSet(spuPage.getList(), ProductSpuDO::getId));
345 281 }
346 282  
347 283 @Test
348   - void testGetSpuPage() {
349   - // 准备参数
350   - ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class,o->{
351   - o.setCategoryId(generateId());
352   - o.setBrandId(generateId());
353   - o.setDeliveryTemplateId(generateId());
354   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
355   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
356   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
357   - o.setPrice(generaInt()); // 限制范围为正整数
358   - o.setMarketPrice(generaInt()); // 限制范围为正整数
359   - o.setCostPrice(generaInt()); // 限制范围为正整数
360   - o.setStock(generaInt()); // 限制范围为正整数
361   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
362   - o.setSalesCount(generaInt()); // 限制范围为正整数
363   - o.setBrowseCount(generaInt()); // 限制范围为正整数
  284 + public void testGetSpuPage() {
  285 + // mock 数据
  286 + ProductSpuDO dbSpu = randomSpuDO(o -> { // 等会查询到
  287 + o.setName("芋道商品");
  288 + o.setCategoryId(1L);
  289 + o.setStatus(ProductSpuStatusEnum.ENABLE.getStatus());
  290 + o.setCreateTime(buildTime(2023, 2, 10));
364 291 });
365   -
  292 + productSpuMapper.insert(dbSpu);
  293 + // 测试 name 不匹配
  294 + productSpuMapper.insert(cloneIgnoreId(dbSpu, o -> o.setName("土豆商品")));
  295 + // 测试 categoryId 不匹配
  296 + productSpuMapper.insert(cloneIgnoreId(dbSpu, o -> o.setCategoryId(2L)));
  297 + // 测试 createTime 不匹配
  298 + productSpuMapper.insert(cloneIgnoreId(dbSpu, o -> o.setCreateTime(buildTime(2023, 3, 10))));
  299 + // 测试 status 不匹配(出售中)
  300 + productSpuMapper.insert(cloneIgnoreId(dbSpu, o -> o.setStatus(ProductSpuStatusEnum.DISABLE.getStatus())));
  301 + productSpuMapper.insert(cloneIgnoreId(dbSpu, o -> o.setStatus(ProductSpuStatusEnum.RECYCLE.getStatus())));
366 302 // 准备参数
367   - productSpuMapper.insert(createReqVO);
368   - // 测试 status 不匹配
369   - productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setStatus(ProductSpuStatusEnum.DISABLE.getStatus())));
370   - productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setStatus(ProductSpuStatusEnum.RECYCLE.getStatus())));
371   - // 测试 SpecType 不匹配
372   - productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setSpecType(true)));
373   - // 测试 BrandId 不匹配
374   - productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setBrandId(generateId())));
375   - // 测试 CategoryId 不匹配
376   - productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setCategoryId(generateId())));
377   -
378   - // 调用
379 303 ProductSpuPageReqVO productSpuPageReqVO = new ProductSpuPageReqVO();
380   - // 查询条件 按需打开
381   - //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.ALERT_STOCK);
382   - //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.RECYCLE_BIN);
383   - //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.FOR_SALE);
384   - //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.IN_WAREHOUSE);
385   - //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.SOLD_OUT);
386   - //productSpuPageReqVO.setName(createReqVO.getName());
387   - //productSpuPageReqVO.setCategoryId(createReqVO.getCategoryId());
  304 + productSpuPageReqVO.setName("芋道");
  305 + productSpuPageReqVO.setCategoryId(1L);
  306 + productSpuPageReqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
  307 + productSpuPageReqVO.setTabType(ProductSpuPageReqVO.FOR_SALE);
388 308  
  309 + // 调用
389 310 PageResult<ProductSpuDO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
390   -
  311 + // 断言
391 312 assertEquals(1, spuPage.getTotal());
  313 + assertEquals(1, spuPage.getList().size());
  314 + assertPojoEquals(dbSpu, spuPage.getList().get(0));
392 315 }
393 316  
394 317 /**
... ... @@ -427,23 +350,49 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
427 350 // 准备参数
428 351 Map<Long, Integer> stockIncrCounts = MapUtil.builder(1L, 10).put(2L, -20).build();
429 352 // mock 方法(数据)
430   - productSpuMapper.insert(randomPojo(ProductSpuDO.class, o ->{
431   - o.setCategoryId(generateId());
432   - o.setBrandId(generateId());
433   - o.setDeliveryTemplateId(generateId());
434   - o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围
435   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
436   - o.setVirtualSalesCount(generaInt()); // 限制范围为正整数
437   - o.setPrice(generaInt()); // 限制范围为正整数
438   - o.setMarketPrice(generaInt()); // 限制范围为正整数
439   - o.setCostPrice(generaInt()); // 限制范围为正整数
440   - o.setStock(generaInt()); // 限制范围为正整数
441   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
442   - o.setSalesCount(generaInt()); // 限制范围为正整数
443   - o.setBrowseCount(generaInt()); // 限制范围为正整数
444   - o.setId(1L).setStock(20);
445   - }));
446   - productSpuMapper.insert(randomPojo(ProductSpuDO.class, o -> {
  353 + productSpuMapper.insert(randomSpuDO(o -> o.setId(1L).setStock(20)));
  354 + productSpuMapper.insert(randomSpuDO(o -> o.setId(2L).setStock(30)));
  355 +
  356 + // 调用
  357 + productSpuService.updateSpuStock(stockIncrCounts);
  358 + // 断言
  359 + assertEquals(productSpuService.getSpu(1L).getStock(), 30);
  360 + assertEquals(productSpuService.getSpu(2L).getStock(), 10);
  361 + }
  362 +
  363 + /**
  364 + * 校验 {@link ProductSpuSaveReqVO#getDeliveryTypes()} 字段
  365 + *
  366 + * @param deliveryTypes 配送方式数组
  367 + * @return 校验不通过的提示
  368 + */
  369 + private static Set<String> validateDeliveryTypes(List<Integer> deliveryTypes) {
  370 + ProductSpuSaveReqVO reqVO = new ProductSpuSaveReqVO();
  371 + reqVO.setDeliveryTypes(deliveryTypes);
  372 + Set<ConstraintViolation<ProductSpuSaveReqVO>> violations = VALIDATOR.validateProperty(reqVO, "deliveryTypes");
  373 + return convertSet(violations, ConstraintViolation::getMessage);
  374 + }
  375 +
  376 + // ========== 随机对象 ==========
  377 +
  378 + private ProductSkuSaveReqVO randomSkuSaveReqVO() {
  379 + return randomPojo(ProductSkuSaveReqVO.class, o->{
  380 + // 限制范围为正整数
  381 + o.setCostPrice(generaInt());
  382 + o.setPrice(generaInt());
  383 + o.setMarketPrice(generaInt());
  384 + o.setStock(generaInt());
  385 + o.setFirstBrokeragePrice(generaInt());
  386 + o.setSecondBrokeragePrice(generaInt());
  387 + // 限制分数为两位数
  388 + o.setWeight(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP));
  389 + o.setVolume(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP));
  390 + });
  391 + }
  392 +
  393 + @SafeVarargs
  394 + private final ProductSpuDO randomSpuDO(Consumer<ProductSpuDO>... consumers) {
  395 + Consumer<ProductSpuDO> consumer = o -> {
447 396 o.setCategoryId(generateId());
448 397 o.setBrandId(generateId());
449 398 o.setDeliveryTemplateId(generateId());
... ... @@ -454,17 +403,10 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
454 403 o.setMarketPrice(generaInt()); // 限制范围为正整数
455 404 o.setCostPrice(generaInt()); // 限制范围为正整数
456 405 o.setStock(generaInt()); // 限制范围为正整数
457   - o.setGiveIntegral(generaInt()); // 限制范围为正整数
458 406 o.setSalesCount(generaInt()); // 限制范围为正整数
459 407 o.setBrowseCount(generaInt()); // 限制范围为正整数
460   - o.setId(2L).setStock(30);
461   - }));
462   -
463   - // 调用
464   - productSpuService.updateSpuStock(stockIncrCounts);
465   - // 断言
466   - assertEquals(productSpuService.getSpu(1L).getStock(), 30);
467   - assertEquals(productSpuService.getSpu(2L).getStock(), 10);
  408 + };
  409 + return randomPojo(ProductSpuDO.class, ArrayUtils.append(consumer, consumers));
468 410 }
469 411  
470 412 }
... ...
yudao-module-mall/yudao-module-product/src/test/resources/sql/create_tables.sql
... ... @@ -28,13 +28,10 @@ CREATE TABLE IF NOT EXISTS `product_spu` (
28 28 `keyword` varchar(256) NOT NULL COMMENT '关键字',
29 29 `introduction` varchar(256) NOT NULL COMMENT '商品简介',
30 30 `description` text NOT NULL COMMENT '商品详情',
31   - `bar_code` varchar(64) NOT NULL COMMENT '条形码',
32 31 `category_id` bigint NOT NULL COMMENT '商品分类编号',
33   - `brand_id` int DEFAULT NULL COMMENT '商品品牌编号',
  32 + `brand_id` bigint DEFAULT NULL COMMENT '商品品牌编号',
34 33 `pic_url` varchar(256) NOT NULL COMMENT '商品封面图',
35 34 `slider_pic_urls` varchar(2000) DEFAULT '' COMMENT '商品轮播图地址\n 数组,以逗号分隔\n 最多上传15张',
36   - `video_url` varchar(256) DEFAULT NULL COMMENT '商品视频',
37   - `unit` tinyint NOT NULL COMMENT '单位',
38 35 `sort` int NOT NULL DEFAULT '0' COMMENT '排序字段',
39 36 `status` tinyint NOT NULL COMMENT '商品状态: 0 上架(开启) 1 下架(禁用)-1 回收',
40 37 `spec_type` bit(1) NOT NULL COMMENT '规格类型:0 单规格 1 多规格',
... ... @@ -42,16 +39,10 @@ CREATE TABLE IF NOT EXISTS `product_spu` (
42 39 `market_price` int NOT NULL COMMENT '市场价,单位使用:分',
43 40 `cost_price` int NOT NULL DEFAULT '-1' COMMENT '成本价,单位: 分',
44 41 `stock` int NOT NULL DEFAULT '0' COMMENT '库存',
  42 + `delivery_types` varchar(255) NOT NULL COMMENT '配送方式数组',
45 43 `delivery_template_id` bigint NOT NULL COMMENT '物流配置模板编号',
46   - `recommend_hot` bit(1) NOT NULL COMMENT '是否热卖推荐: 0 默认 1 热卖',
47   - `recommend_benefit` bit(1) NOT NULL COMMENT '是否优惠推荐: 0 默认 1 优选',
48   - `recommend_best` bit(1) NOT NULL COMMENT '是否精品推荐: 0 默认 1 精品',
49   - `recommend_new` bit(1) NOT NULL COMMENT '是否新品推荐: 0 默认 1 新品',
50   - `recommend_good` bit(1) NOT NULL COMMENT '是否优品推荐',
51 44 `give_integral` int NOT NULL COMMENT '赠送积分',
52   - `give_coupon_template_ids` varchar(512) DEFAULT '' COMMENT '赠送的优惠劵编号的数组',
53 45 `sub_commission_type` bit(1) NOT NULL COMMENT '分销类型',
54   - `activity_orders` varchar(16) NOT NULL DEFAULT '' COMMENT '活动显示排序0=默认, 1=秒杀,2=砍价,3=拼团',
55 46 `sales_count` int DEFAULT '0' COMMENT '商品销量',
56 47 `virtual_sales_count` int DEFAULT '0' COMMENT '虚拟销量',
57 48 `browse_count` int DEFAULT '0' COMMENT '商品点击量',
... ...
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java
... ... @@ -102,7 +102,7 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
102 102 }
103 103 // 删除
104 104 diyTemplateMapper.deleteById(id);
105   - // 级联删除模板下自动生成的默认页面(createDiyTemplate 的「首页/我的」),避免页面列表残留孤儿页
  105 + // 删除模板下的页面
106 106 diyPageService.getDiyPageByTemplateId(id)
107 107 .forEach(page -> diyPageService.deleteDiyPage(page.getId()));
108 108 }
... ...
yudao-module-mall/yudao-module-promotion/src/test/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.service.coupon;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
  5 +import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
  6 +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
  7 +import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
  8 +import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
  9 +import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper;
  10 +import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponTemplateMapper;
  11 +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
  12 +import jakarta.annotation.Resource;
  13 +import org.junit.jupiter.api.Test;
  14 +import org.springframework.context.annotation.Import;
  15 +import org.springframework.jdbc.core.JdbcTemplate;
  16 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  17 +
  18 +import javax.sql.DataSource;
  19 +import java.util.function.Consumer;
  20 +
  21 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  22 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
  23 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  24 +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_DELETE_FAIL_USED;
  25 +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NOT_EXISTS;
  26 +import static org.junit.jupiter.api.Assertions.*;
  27 +
  28 +/**
  29 + * {@link CouponServiceImpl} 的单元测试类
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Import({CouponServiceImpl.class, CouponTemplateServiceImpl.class})
  34 +public class CouponServiceImplTest extends BaseDbUnitTest {
  35 +
  36 + @Resource
  37 + private CouponServiceImpl couponService;
  38 +
  39 + @Resource
  40 + private CouponMapper couponMapper;
  41 + @Resource
  42 + private CouponTemplateMapper couponTemplateMapper;
  43 +
  44 + @Resource
  45 + private DataSource dataSource;
  46 +
  47 + @MockitoBean
  48 + private MemberUserApi memberUserApi;
  49 + @MockitoBean
  50 + private ProductCategoryApi productCategoryApi;
  51 + @MockitoBean
  52 + private ProductSpuApi productSpuApi;
  53 +
  54 + @Test
  55 + public void testDeleteCoupon_success() {
  56 + // mock 数据
  57 + CouponTemplateDO dbTemplate = randomCouponTemplateDO(o -> o.setTakeCount(5));
  58 + couponTemplateMapper.insert(dbTemplate);
  59 + CouponDO dbCoupon = randomCouponDO(o -> o.setTemplateId(dbTemplate.getId())
  60 + .setStatus(CouponStatusEnum.UNUSED.getStatus()));
  61 + couponMapper.insert(dbCoupon);
  62 +
  63 + // 调用
  64 + couponService.deleteCoupon(dbCoupon.getId());
  65 + // 断言:优惠劵被删除,模板领取数量 -1
  66 + assertNull(couponMapper.selectById(dbCoupon.getId()));
  67 + assertEquals(4, couponTemplateMapper.selectById(dbTemplate.getId()).getTakeCount());
  68 + }
  69 +
  70 + @Test
  71 + public void testDeleteCoupon_templateDeleted() {
  72 + // mock 数据(模板已删除)
  73 + CouponTemplateDO dbTemplate = randomCouponTemplateDO(o -> o.setTakeCount(5));
  74 + couponTemplateMapper.insert(dbTemplate);
  75 + couponTemplateMapper.deleteById(dbTemplate.getId());
  76 + CouponDO dbCoupon = randomCouponDO(o -> o.setTemplateId(dbTemplate.getId())
  77 + .setStatus(CouponStatusEnum.UNUSED.getStatus()));
  78 + couponMapper.insert(dbCoupon);
  79 +
  80 + // 调用
  81 + couponService.deleteCoupon(dbCoupon.getId());
  82 + // 断言:优惠劵被删除,已删除模板的领取数量不变
  83 + assertNull(couponMapper.selectById(dbCoupon.getId()));
  84 + assertEquals(5, selectTemplateTakeCount(dbTemplate.getId()));
  85 + }
  86 +
  87 + @Test
  88 + public void testDeleteCoupon_templateNotExists() {
  89 + // mock 数据(模板不存在)
  90 + CouponDO dbCoupon = randomCouponDO(o -> o.setTemplateId(randomLongId())
  91 + .setStatus(CouponStatusEnum.EXPIRE.getStatus()));
  92 + couponMapper.insert(dbCoupon);
  93 +
  94 + // 调用
  95 + couponService.deleteCoupon(dbCoupon.getId());
  96 + // 断言:优惠劵被删除
  97 + assertNull(couponMapper.selectById(dbCoupon.getId()));
  98 + }
  99 +
  100 + @Test
  101 + public void testDeleteCoupon_used() {
  102 + // mock 数据
  103 + CouponTemplateDO dbTemplate = randomCouponTemplateDO(o -> o.setTakeCount(5));
  104 + couponTemplateMapper.insert(dbTemplate);
  105 + CouponDO dbCoupon = randomCouponDO(o -> o.setTemplateId(dbTemplate.getId())
  106 + .setStatus(CouponStatusEnum.USED.getStatus()));
  107 + couponMapper.insert(dbCoupon);
  108 +
  109 + // 调用, 并断言异常
  110 + assertServiceException(() -> couponService.deleteCoupon(dbCoupon.getId()), COUPON_DELETE_FAIL_USED);
  111 + // 断言:优惠劵未删除,模板领取数量不变
  112 + CouponDO coupon = couponMapper.selectById(dbCoupon.getId());
  113 + assertNotNull(coupon);
  114 + assertEquals(CouponStatusEnum.USED.getStatus(), coupon.getStatus());
  115 + assertEquals(5, couponTemplateMapper.selectById(dbTemplate.getId()).getTakeCount());
  116 + }
  117 +
  118 + @Test
  119 + public void testDeleteCoupon_notExists() {
  120 + // 准备参数
  121 + Long id = randomLongId();
  122 +
  123 + // 调用, 并断言异常
  124 + assertServiceException(() -> couponService.deleteCoupon(id), COUPON_NOT_EXISTS);
  125 + }
  126 +
  127 + /**
  128 + * 查询模板的领取数量,包括已删除的模板
  129 + *
  130 + * @param id 模板编号
  131 + * @return 领取数量
  132 + */
  133 + private Integer selectTemplateTakeCount(Long id) {
  134 + return new JdbcTemplate(dataSource).queryForObject(
  135 + "SELECT take_count FROM promotion_coupon_template WHERE id = ?", Integer.class, id);
  136 + }
  137 +
  138 + // ========== 随机对象 ==========
  139 +
  140 + @SafeVarargs
  141 + private static CouponTemplateDO randomCouponTemplateDO(Consumer<CouponTemplateDO>... consumers) {
  142 + return randomPojo(CouponTemplateDO.class, consumers);
  143 + }
  144 +
  145 + @SafeVarargs
  146 + private static CouponDO randomCouponDO(Consumer<CouponDO>... consumers) {
  147 + return randomPojo(CouponDO.class, consumers);
  148 + }
  149 +
  150 +}
... ...
yudao-module-mall/yudao-module-promotion/src/test/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImplTest.java
... ... @@ -7,18 +7,19 @@ import cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.page.DiyPagePag
7 7 import cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.page.DiyPageUpdateReqVO;
8 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.diy.DiyPageDO;
9 9 import cn.iocoder.yudao.module.promotion.dal.mysql.diy.DiyPageMapper;
10   -import org.junit.jupiter.api.Disabled;
11 10 import org.junit.jupiter.api.Test;
12 11 import org.springframework.context.annotation.Import;
13 12  
14 13 import jakarta.annotation.Resource;
15 14  
16 15 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
  16 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
17 17 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
18 18 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
19 19 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
20 20 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
21 21 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  22 +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.DIY_PAGE_NAME_USED;
22 23 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.DIY_PAGE_NOT_EXISTS;
23 24 import static org.junit.jupiter.api.Assertions.*;
24 25  
... ... @@ -27,7 +28,7 @@ import static org.junit.jupiter.api.Assertions.*;
27 28 *
28 29 * @author owen
29 30 */
30   -@Disabled // TODO 芋艿:后续 fix 补充的单测
  31 +// DONE 芋艿:后续 fix 补充的单测
31 32 @Import(DiyPageServiceImpl.class)
32 33 public class DiyPageServiceImplTest extends BaseDbUnitTest {
33 34  
... ... @@ -49,6 +50,21 @@ public class DiyPageServiceImplTest extends BaseDbUnitTest {
49 50 // 校验记录的属性是否正确
50 51 DiyPageDO diyPage = diyPageMapper.selectById(diyPageId);
51 52 assertPojoEquals(reqVO, diyPage);
  53 + assertEquals("{}", diyPage.getProperty());
  54 + }
  55 +
  56 + @Test
  57 + public void testCreateDiyPage_nameUsed() {
  58 + // mock 数据(独立页面)
  59 + diyPageMapper.insert(randomPojo(DiyPageDO.class, o -> o.setTemplateId(null).setName("芋道首页")));
  60 + // 准备参数
  61 + DiyPageCreateReqVO reqVO = randomPojo(DiyPageCreateReqVO.class, o -> {
  62 + o.setTemplateId(null);
  63 + o.setName("芋道首页");
  64 + });
  65 +
  66 + // 调用, 并断言异常
  67 + assertServiceException(() -> diyPageService.createDiyPage(reqVO), DIY_PAGE_NAME_USED, "芋道首页");
52 68 }
53 69  
54 70 @Test
... ... @@ -101,21 +117,24 @@ public class DiyPageServiceImplTest extends BaseDbUnitTest {
101 117 }
102 118  
103 119 @Test
104   - @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
  120 + // DONE 请修改 null 为需要的值,然后删除 @Disabled 注解
105 121 public void testGetDiyPagePage() {
106 122 // mock 数据
107 123 DiyPageDO dbDiyPage = randomPojo(DiyPageDO.class, o -> { // 等会查询到
108   - o.setName(null);
109   - o.setCreateTime(null);
  124 + o.setTemplateId(null); // 独立页面
  125 + o.setName("芋道首页");
  126 + o.setCreateTime(buildTime(2023, 2, 10));
110 127 });
111 128 diyPageMapper.insert(dbDiyPage);
112 129 // 测试 name 不匹配
113   - diyPageMapper.insert(cloneIgnoreId(dbDiyPage, o -> o.setName(null)));
  130 + diyPageMapper.insert(cloneIgnoreId(dbDiyPage, o -> o.setName("土豆首页")));
114 131 // 测试 createTime 不匹配
115   - diyPageMapper.insert(cloneIgnoreId(dbDiyPage, o -> o.setCreateTime(null)));
  132 + diyPageMapper.insert(cloneIgnoreId(dbDiyPage, o -> o.setCreateTime(buildTime(2023, 3, 10))));
  133 + // 测试 templateId 不匹配(模板下的页面,不在独立页面列表)
  134 + diyPageMapper.insert(cloneIgnoreId(dbDiyPage, o -> o.setTemplateId(1L)));
116 135 // 准备参数
117 136 DiyPagePageReqVO reqVO = new DiyPagePageReqVO();
118   - reqVO.setName(null);
  137 + reqVO.setName("芋道");
119 138 reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
120 139  
121 140 // 调用
... ...
yudao-module-mall/yudao-module-promotion/src/test/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImplTest.java
... ... @@ -5,30 +5,40 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
5 5 import cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.template.DiyTemplateCreateReqVO;
6 6 import cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.template.DiyTemplatePageReqVO;
7 7 import cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.template.DiyTemplateUpdateReqVO;
  8 +import cn.iocoder.yudao.module.promotion.dal.dataobject.diy.DiyPageDO;
8 9 import cn.iocoder.yudao.module.promotion.dal.dataobject.diy.DiyTemplateDO;
  10 +import cn.iocoder.yudao.module.promotion.dal.mysql.diy.DiyPageMapper;
9 11 import cn.iocoder.yudao.module.promotion.dal.mysql.diy.DiyTemplateMapper;
10   -import org.junit.jupiter.api.Disabled;
  12 +import jakarta.annotation.Resource;
11 13 import org.junit.jupiter.api.Test;
12 14 import org.springframework.context.annotation.Import;
  15 +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
13 16  
14   -import jakarta.annotation.Resource;
  17 +import java.util.List;
  18 +import java.util.function.Consumer;
15 19  
  20 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  21 +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
16 22 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
  23 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
17 24 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
18 25 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
19 26 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
20 27 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
21 28 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
22 29 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.DIY_TEMPLATE_NOT_EXISTS;
  30 +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.DIY_TEMPLATE_USED_CANNOT_DELETE;
23 31 import static org.junit.jupiter.api.Assertions.*;
  32 +import static org.mockito.ArgumentMatchers.anyLong;
  33 +import static org.mockito.Mockito.doCallRealMethod;
24 34  
25 35 /**
26 36 * {@link DiyTemplateServiceImpl} 的单元测试类
27 37 *
28 38 * @author owen
29 39 */
30   -@Disabled // TODO 芋艿:后续 fix 补充的单测
31   -@Import(DiyTemplateServiceImpl.class)
  40 +// DONE 芋艿:后续 fix 补充的单测
  41 +@Import({DiyTemplateServiceImpl.class, DiyPageServiceImpl.class})
32 42 public class DiyTemplateServiceImplTest extends BaseDbUnitTest {
33 43  
34 44 @Resource
... ... @@ -36,6 +46,11 @@ public class DiyTemplateServiceImplTest extends BaseDbUnitTest {
36 46  
37 47 @Resource
38 48 private DiyTemplateMapper diyTemplateMapper;
  49 + @Resource
  50 + private DiyPageMapper diyPageMapper;
  51 +
  52 + @MockitoSpyBean
  53 + private DiyPageService diyPageService;
39 54  
40 55 @Test
41 56 public void testCreateDiyTemplate_success() {
... ... @@ -49,6 +64,15 @@ public class DiyTemplateServiceImplTest extends BaseDbUnitTest {
49 64 // 校验记录的属性是否正确
50 65 DiyTemplateDO diyTemplate = diyTemplateMapper.selectById(diyTemplateId);
51 66 assertPojoEquals(reqVO, diyTemplate);
  67 + assertEquals("{}", diyTemplate.getProperty());
  68 + // 校验默认页面:首页、我的
  69 + List<DiyPageDO> pages = diyPageMapper.selectListByTemplateId(diyTemplateId);
  70 + assertEquals(asSet("首页", "我的"), convertSet(pages, DiyPageDO::getName));
  71 + String remark = String.format("模板【%s】自动创建", reqVO.getName());
  72 + pages.forEach(page -> {
  73 + assertEquals(remark, page.getRemark());
  74 + assertEquals("{}", page.getProperty());
  75 + });
52 76 }
53 77  
54 78 @Test
... ... @@ -79,9 +103,20 @@ public class DiyTemplateServiceImplTest extends BaseDbUnitTest {
79 103  
80 104 @Test
81 105 public void testDeleteDiyTemplate_success() {
82   - // mock 数据
83   - DiyTemplateDO dbDiyTemplate = randomPojo(DiyTemplateDO.class);
  106 + // mock 数据(模板)
  107 + DiyTemplateDO dbDiyTemplate = randomPojo(DiyTemplateDO.class, o -> o.setUsed(false));
84 108 diyTemplateMapper.insert(dbDiyTemplate);// @Sql: 先插入出一条存在的数据
  109 + DiyTemplateDO otherDiyTemplate = randomPojo(DiyTemplateDO.class, o -> o.setUsed(false));
  110 + diyTemplateMapper.insert(otherDiyTemplate);
  111 + // mock 数据(页面):该模板下的全部页面,包括默认页面及后续新增页面
  112 + diyPageMapper.insert(randomDiyPageDO(o -> o.setTemplateId(dbDiyTemplate.getId()).setName("首页")));
  113 + diyPageMapper.insert(randomDiyPageDO(o -> o.setTemplateId(dbDiyTemplate.getId()).setName("我的")));
  114 + diyPageMapper.insert(randomDiyPageDO(o -> o.setTemplateId(dbDiyTemplate.getId()).setName("活动页")));
  115 + // mock 数据(页面):其它模板下的页面、独立页面
  116 + DiyPageDO otherTemplatePage = randomDiyPageDO(o -> o.setTemplateId(otherDiyTemplate.getId()));
  117 + diyPageMapper.insert(otherTemplatePage);
  118 + DiyPageDO standalonePage = randomDiyPageDO(o -> o.setTemplateId(null));
  119 + diyPageMapper.insert(standalonePage);
85 120 // 准备参数
86 121 Long id = dbDiyTemplate.getId();
87 122  
... ... @@ -89,6 +124,11 @@ public class DiyTemplateServiceImplTest extends BaseDbUnitTest {
89 124 diyTemplateService.deleteDiyTemplate(id);
90 125 // 校验数据不存在了
91 126 assertNull(diyTemplateMapper.selectById(id));
  127 + assertEquals(0, diyPageMapper.selectListByTemplateId(id).size());
  128 + // 校验其它模板、其它模板下的页面、独立页面保留
  129 + assertNotNull(diyTemplateMapper.selectById(otherDiyTemplate.getId()));
  130 + assertNotNull(diyPageMapper.selectById(otherTemplatePage.getId()));
  131 + assertNotNull(diyPageMapper.selectById(standalonePage.getId()));
92 132 }
93 133  
94 134 @Test
... ... @@ -101,37 +141,65 @@ public class DiyTemplateServiceImplTest extends BaseDbUnitTest {
101 141 }
102 142  
103 143 @Test
104   - @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
  144 + public void testDeleteDiyTemplate_used() {
  145 + // mock 数据
  146 + DiyTemplateDO dbDiyTemplate = randomPojo(DiyTemplateDO.class, o -> o.setUsed(true));
  147 + diyTemplateMapper.insert(dbDiyTemplate);// @Sql: 先插入出一条存在的数据
  148 + DiyPageDO dbDiyPage = randomDiyPageDO(o -> o.setTemplateId(dbDiyTemplate.getId()));
  149 + diyPageMapper.insert(dbDiyPage);
  150 + // 准备参数
  151 + Long id = dbDiyTemplate.getId();
  152 +
  153 + // 调用, 并断言异常
  154 + assertServiceException(() -> diyTemplateService.deleteDiyTemplate(id), DIY_TEMPLATE_USED_CANNOT_DELETE);
  155 + // 校验模板、页面都未删除
  156 + assertNotNull(diyTemplateMapper.selectById(id));
  157 + assertNotNull(diyPageMapper.selectById(dbDiyPage.getId()));
  158 + }
  159 +
  160 + @Test
  161 + public void testDeleteDiyTemplate_deletePageFailRollback() {
  162 + // mock 数据
  163 + DiyTemplateDO dbDiyTemplate = randomPojo(DiyTemplateDO.class, o -> o.setUsed(false));
  164 + diyTemplateMapper.insert(dbDiyTemplate);// @Sql: 先插入出一条存在的数据
  165 + diyPageMapper.insert(randomDiyPageDO(o -> o.setTemplateId(dbDiyTemplate.getId())));
  166 + diyPageMapper.insert(randomDiyPageDO(o -> o.setTemplateId(dbDiyTemplate.getId())));
  167 + // 准备参数
  168 + Long id = dbDiyTemplate.getId();
  169 + // mock 方法(第 1 个页面真实删除,第 2 个页面删除失败)
  170 + doCallRealMethod().doThrow(new IllegalStateException("删除页面失败"))
  171 + .when(diyPageService).deleteDiyPage(anyLong());
  172 +
  173 + // 调用, 并断言异常
  174 + assertThrows(IllegalStateException.class, () -> diyTemplateService.deleteDiyTemplate(id));
  175 + // 校验模板、页面都已回滚
  176 + assertNotNull(diyTemplateMapper.selectById(id));
  177 + assertEquals(2, diyPageMapper.selectListByTemplateId(id).size());
  178 + }
  179 +
  180 + @Test
  181 + // DONE 请修改 null 为需要的值,然后删除 @Disabled 注解
105 182 public void testGetDiyTemplatePage() {
106 183 // mock 数据
107 184 DiyTemplateDO dbDiyTemplate = randomPojo(DiyTemplateDO.class, o -> { // 等会查询到
108   - o.setName(null);
109   - o.setUsed(null);
110   - o.setUsedTime(null);
111   - o.setRemark(null);
112   - o.setPreviewPicUrls(null);
113   - o.setProperty(null);
114   - o.setCreateTime(null);
  185 + o.setName("芋道商城");
  186 + o.setUsed(true);
  187 + o.setUsedTime(buildTime(2023, 2, 10));
  188 + o.setCreateTime(buildTime(2023, 2, 10));
115 189 });
116 190 diyTemplateMapper.insert(dbDiyTemplate);
117 191 // 测试 name 不匹配
118   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setName(null)));
  192 + diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setName("土豆商城")));
119 193 // 测试 used 不匹配
120   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setUsed(null)));
  194 + diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setUsed(false)));
121 195 // 测试 usedTime 不匹配
122   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setUsedTime(null)));
123   - // 测试 remark 不匹配
124   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setRemark(null)));
125   - // 测试 previewPicUrls 不匹配
126   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setPreviewPicUrls(null)));
127   - // 测试 property 不匹配
128   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setProperty(null)));
  196 + diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setUsedTime(buildTime(2023, 3, 10))));
129 197 // 测试 createTime 不匹配
130   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setCreateTime(null)));
  198 + diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setCreateTime(buildTime(2023, 3, 10))));
131 199 // 准备参数
132 200 DiyTemplatePageReqVO reqVO = new DiyTemplatePageReqVO();
133   - reqVO.setName(null);
134   - reqVO.setUsed(null);
  201 + reqVO.setName("芋道");
  202 + reqVO.setUsed(true);
135 203 reqVO.setUsedTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
136 204 reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
137 205  
... ... @@ -144,33 +212,25 @@ public class DiyTemplateServiceImplTest extends BaseDbUnitTest {
144 212 }
145 213  
146 214 @Test
147   - @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
148   - public void testGetDiyTemplateList() {
  215 + // DONE 请修改 null 为需要的值,然后删除 @Disabled 注解(DiyTemplateService 无列表查询方法,原 testGetDiyTemplateList 改为测试 getUsedDiyTemplate)
  216 + public void testGetUsedDiyTemplate() {
149 217 // mock 数据
150   - DiyTemplateDO dbDiyTemplate = randomPojo(DiyTemplateDO.class, o -> { // 等会查询到
151   - o.setName(null);
152   - o.setUsed(null);
153   - o.setUsedTime(null);
154   - o.setRemark(null);
155   - o.setPreviewPicUrls(null);
156   - o.setProperty(null);
157   - o.setCreateTime(null);
158   - });
  218 + DiyTemplateDO dbDiyTemplate = randomPojo(DiyTemplateDO.class, o -> o.setUsed(true));
159 219 diyTemplateMapper.insert(dbDiyTemplate);
160   - // 测试 name 不匹配
161   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setName(null)));
162 220 // 测试 used 不匹配
163   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setUsed(null)));
164   - // 测试 usedTime 不匹配
165   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setUsedTime(null)));
166   - // 测试 remark 不匹配
167   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setRemark(null)));
168   - // 测试 previewPicUrls 不匹配
169   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setPreviewPicUrls(null)));
170   - // 测试 property 不匹配
171   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setProperty(null)));
172   - // 测试 createTime 不匹配
173   - diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setCreateTime(null)));
  221 + diyTemplateMapper.insert(cloneIgnoreId(dbDiyTemplate, o -> o.setName("土豆商城").setUsed(false)));
  222 +
  223 + // 调用
  224 + DiyTemplateDO usedDiyTemplate = diyTemplateService.getUsedDiyTemplate();
  225 + // 断言
  226 + assertPojoEquals(dbDiyTemplate, usedDiyTemplate);
  227 + }
  228 +
  229 + // ========== 随机对象 ==========
  230 +
  231 + @SafeVarargs
  232 + private static DiyPageDO randomDiyPageDO(Consumer<DiyPageDO>... consumers) {
  233 + return randomPojo(DiyPageDO.class, consumers);
174 234 }
175 235  
176 236 }
... ...
yudao-module-mall/yudao-module-promotion/src/test/resources/sql/create_tables.sql
... ... @@ -277,8 +277,8 @@ CREATE TABLE IF NOT EXISTS &quot;promotion_diy_template&quot;
277 277 (
278 278 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
279 279 "name" varchar NOT NULL,
280   - "used" bit NOT NULL,
281   - "used_time" varchar,
  280 + "used" bit NOT NULL DEFAULT FALSE,
  281 + "used_time" datetime,
282 282 "remark" varchar,
283 283 "preview_pic_urls" varchar,
284 284 "property" varchar NOT NULL,
... ... @@ -293,7 +293,7 @@ CREATE TABLE IF NOT EXISTS &quot;promotion_diy_template&quot;
293 293 CREATE TABLE IF NOT EXISTS "promotion_diy_page"
294 294 (
295 295 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
296   - "template_id" bigint NOT NULL,
  296 + "template_id" bigint,
297 297 "name" varchar NOT NULL,
298 298 "remark" varchar,
299 299 "preview_pic_urls" varchar,
... ... @@ -303,6 +303,6 @@ CREATE TABLE IF NOT EXISTS &quot;promotion_diy_page&quot;
303 303 "updater" varchar DEFAULT '',
304 304 "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
305 305 "deleted" bit NOT NULL DEFAULT FALSE,
306   - "tenant_id" bigint NOT NULL,
  306 + "tenant_id" bigint NOT NULL DEFAULT 0,
307 307 PRIMARY KEY ("id")
308 308 ) COMMENT '装修页面';
... ...
yudao-module-mall/yudao-module-trade/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java
1 1 package cn.iocoder.yudao.module.trade.service.order;
2 2  
3 3 import cn.hutool.core.util.IdUtil;
4   -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
5 5 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  6 +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
6 7 import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
7   -import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
  8 +import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
8 9 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
9 10 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
  11 +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
10 12 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
11 13 import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
12   -import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
13   -import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
14   -import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
15   -import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
16   -import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.express.DeliveryExpressCreateReqVO;
  14 +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
  15 +import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
17 16 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
  17 +import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO;
  18 +import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementReqVO;
  19 +import cn.iocoder.yudao.module.trade.dal.dataobject.cart.CartDO;
  20 +import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
18 21 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
  22 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
19 23 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderItemMapper;
20 24 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
21 25 import cn.iocoder.yudao.module.trade.dal.redis.no.TradeNoRedisDAO;
22 26 import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
  27 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum;
23 28 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
24 29 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
25   -import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderConfig;
  30 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
26 31 import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
27   -import cn.iocoder.yudao.module.trade.service.cart.CartServiceImpl;
  32 +import cn.iocoder.yudao.module.trade.framework.order.core.aop.TradeOrderLogAspect;
  33 +import cn.iocoder.yudao.module.trade.service.cart.CartService;
28 34 import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
29   -import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressServiceImpl;
30   -import cn.iocoder.yudao.module.trade.service.message.TradeMessageServiceImpl;
  35 +import cn.iocoder.yudao.module.trade.service.delivery.DeliveryPickUpStoreService;
  36 +import cn.iocoder.yudao.module.trade.service.message.TradeMessageService;
31 37 import cn.iocoder.yudao.module.trade.service.order.handler.TradeOrderHandler;
32   -import cn.iocoder.yudao.module.trade.service.price.TradePriceServiceImpl;
33   -import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculator;
  38 +import cn.iocoder.yudao.module.trade.service.price.TradePriceService;
  39 +import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
34 40 import jakarta.annotation.Resource;
  41 +import org.junit.jupiter.api.AfterEach;
35 42 import org.junit.jupiter.api.BeforeEach;
36   -import org.junit.jupiter.api.Disabled;
37 43 import org.junit.jupiter.api.Test;
38 44 import org.springframework.context.annotation.Import;
  45 +import org.springframework.mock.web.MockHttpServletRequest;
39 46 import org.springframework.test.context.bean.override.mockito.MockitoBean;
  47 +import org.springframework.test.util.ReflectionTestUtils;
  48 +import org.springframework.web.context.request.RequestContextHolder;
  49 +import org.springframework.web.context.request.ServletRequestAttributes;
40 50  
41 51 import java.time.Duration;
  52 +import java.util.Arrays;
  53 +import java.util.List;
42 54  
  55 +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
43 56 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
44 57 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
45 58 import static org.junit.jupiter.api.Assertions.*;
46   -import static org.mockito.ArgumentMatchers.anyString;
47   -import static org.mockito.ArgumentMatchers.eq;
  59 +import static org.mockito.ArgumentMatchers.*;
  60 +import static org.mockito.Mockito.verify;
48 61 import static org.mockito.Mockito.when;
49 62  
50 63 /**
... ... @@ -53,10 +66,8 @@ import static org.mockito.Mockito.when;
53 66 * @author LeeYan9
54 67 * @since 2022-09-07
55 68 */
56   -@Disabled // TODO 芋艿:后续 fix 补充的单测
57   -@Import({TradeOrderUpdateServiceImpl.class, TradeOrderConfig.class, CartServiceImpl.class, TradePriceServiceImpl.class,
58   - DeliveryExpressServiceImpl.class, TradeMessageServiceImpl.class
59   -})
  69 +// DONE 芋艿:后续 fix 补充的单测
  70 +@Import(TradeOrderUpdateServiceImpl.class)
60 71 public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
61 72  
62 73 @Resource
... ... @@ -68,34 +79,33 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
68 79 private TradeOrderItemMapper tradeOrderItemMapper;
69 80  
70 81 @MockitoBean
71   - private MemberUserApi memberUserApi;
  82 + private TradeNoRedisDAO tradeNoRedisDAO;
72 83 @MockitoBean
73   - private ProductSpuApi productSpuApi;
  84 + private TradeOrderHandler tradeOrderHandler;
74 85 @MockitoBean
75   - private ProductSkuApi productSkuApi;
  86 + private CartService cartService;
76 87 @MockitoBean
77   - private ProductCommentApi productCommentApi;
78   - // @MockitoBean
79   -// private PriceApi priceApi;
  88 + private TradePriceService tradePriceService;
80 89 @MockitoBean
81   - private PayOrderApi payOrderApi;
  90 + private DeliveryExpressService deliveryExpressService;
82 91 @MockitoBean
83   - private MemberAddressApi addressApi;
  92 + private TradeMessageService tradeMessageService;
84 93 @MockitoBean
85   - private CouponApi couponApi;
86   -
  94 + private DeliveryPickUpStoreService pickUpStoreService;
87 95 @MockitoBean
88   - private TradeOrderProperties tradeOrderProperties;
  96 + private PayOrderApi payOrderApi;
89 97 @MockitoBean
90   - private TradeNoRedisDAO tradeNoRedisDAO;
  98 + private MemberAddressApi addressApi;
91 99 @MockitoBean
92   - private TradeOrderHandler tradeOrderHandler;
  100 + private ProductCommentApi productCommentApi;
93 101 @MockitoBean
94   - private TradePriceCalculator tradePriceCalculator;
  102 + private SocialClientApi socialClientApi;
95 103 @MockitoBean
96   - private NotifyMessageSendApi notifyMessageSendApi;
  104 + private PayRefundApi payRefundApi;
97 105 @MockitoBean
98   - private DeliveryExpressService deliveryExpressService;
  106 + private CombinationRecordApi combinationRecordApi;
  107 + @MockitoBean
  108 + private TradeOrderProperties tradeOrderProperties;
99 109  
100 110 @BeforeEach
101 111 public void setUp() {
... ... @@ -104,159 +114,134 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
104 114 when(tradeNoRedisDAO.generate(anyString())).thenReturn(IdUtil.randomUUID());
105 115 }
106 116  
107   -// @Test
108   -// public void testCreateTradeOrder_success() {
109   -// // 准备参数
110   -// Long userId = 100L;
111   -// String userIp = "127.0.0.1";
112   -//// AppTradeOrderCreateReqVO reqVO = new AppTradeOrderCreateReqVO()
113   -//// .setAddressId(10L).setCouponId(101L).setRemark("我是备注").setFromCart(true)
114   -//// .setItems(Arrays.asList(new AppTradeOrderCreateReqVO.Item().setSkuId(1L).setCount(3),
115   -//// new AppTradeOrderCreateReqVO.Item().setSkuId(2L).setCount(4)));
116   -// AppTradeOrderCreateReqVO reqVO = null;
117   -// // TODO 芋艿:重新高下
118   -// // mock 方法(商品 SKU 检查)
119   -// ProductSkuRespDTO sku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(1L).setSpuId(11L)
120   -// .setPrice(50).setStock(100)
121   -// .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(111L).setValueId(222L))));
122   -// ProductSkuRespDTO sku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(2L).setSpuId(21L)
123   -// .setPrice(20).setStock(50))
124   -// .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(333L).setValueId(444L)));
125   -// when(productSkuApi.getSkuList(eq(asSet(1L, 2L)))).thenReturn(Arrays.asList(sku01, sku02));
126   -// // mock 方法(商品 SPU 检查)
127   -// ProductSpuRespDTO spu01 = randomPojo(ProductSpuRespDTO.class, o -> o.setId(11L)
128   -// .setStatus(ProductSpuStatusEnum.ENABLE.getStatus()).setName("商品 1"));
129   -// ProductSpuRespDTO spu02 = randomPojo(ProductSpuRespDTO.class, o -> o.setId(21L)
130   -// .setStatus(ProductSpuStatusEnum.ENABLE.getStatus()));
131   -// when(productSpuApi.getSpuList(eq(asSet(11L, 21L)))).thenReturn(Arrays.asList(spu01, spu02));
132   -// // mock 方法(用户收件地址的校验)
133   -// MemberAddressRespDTO addressRespDTO = new MemberAddressRespDTO().setId(10L).setUserId(userId).setName("芋艿")
134   -// .setMobile("15601691300").setAreaId(3306).setDetailAddress("土豆村");
135   -// when(addressApi.getAddress(eq(10L), eq(userId))).thenReturn(addressRespDTO);
136   -// // mock 方法(价格计算)
137   -// PriceCalculateRespDTO.OrderItem priceOrderItem01 = new PriceCalculateRespDTO.OrderItem()
138   -// .setSpuId(11L).setSkuId(1L).setCount(3).setOriginalPrice(150).setOriginalUnitPrice(50)
139   -// .setDiscountPrice(20).setPayPrice(130).setOrderPartPrice(7).setOrderDividePrice(35);
140   -// PriceCalculateRespDTO.OrderItem priceOrderItem02 = new PriceCalculateRespDTO.OrderItem()
141   -// .setSpuId(21L).setSkuId(2L).setCount(4).setOriginalPrice(80).setOriginalUnitPrice(20)
142   -// .setDiscountPrice(40).setPayPrice(40).setOrderPartPrice(15).setOrderDividePrice(25);
143   -// PriceCalculateRespDTO.Order priceOrder = new PriceCalculateRespDTO.Order()
144   -// .setTotalPrice(230).setDiscountPrice(0).setCouponPrice(30)
145   -// .setPointPrice(10).setDeliveryPrice(20).setPayPrice(80).setCouponId(101L).setCouponPrice(30)
146   -// .setItems(Arrays.asList(priceOrderItem01, priceOrderItem02));
147   -// when(priceApi.calculatePrice(argThat(priceCalculateReqDTO -> {
148   -// assertEquals(priceCalculateReqDTO.getUserId(), 100L);
149   -// assertEquals(priceCalculateReqDTO.getCouponId(), 101L);
150   -// assertEquals(priceCalculateReqDTO.getItems().get(0).getSkuId(), 1L);
151   -// assertEquals(priceCalculateReqDTO.getItems().get(0).getCount(), 3);
152   -// assertEquals(priceCalculateReqDTO.getItems().get(1).getSkuId(), 2L);
153   -// assertEquals(priceCalculateReqDTO.getItems().get(1).getCount(), 4);
154   -// return true;
155   -// }))).thenReturn(new PriceCalculateRespDTO().setOrder(priceOrder));
156   -// // mock 方法(创建支付单)
157   -// when(payOrderApi.createOrder(argThat(createReqDTO -> {
158   -// assertEquals(createReqDTO.getAppId(), 888L);
159   -// assertEquals(createReqDTO.getUserIp(), userIp);
160   -// assertNotNull(createReqDTO.getMerchantOrderId()); // 由于 tradeOrderId 后生成,只能校验非空
161   -// assertEquals(createReqDTO.getSubject(), "商品 1 等多件");
162   -// assertNull(createReqDTO.getBody());
163   -// assertEquals(createReqDTO.getPrice(), 80);
164   -// assertNotNull(createReqDTO.getExpireTime());
165   -// return true;
166   -// }))).thenReturn(1000L);
167   -//
168   -// // 调用方法
169   -// TradeOrderDO order = tradeOrderUpdateService.createOrder(userId, userIp, reqVO, null);
170   -// // 断言 TradeOrderDO 订单
171   -// List<TradeOrderDO> tradeOrderDOs = tradeOrderMapper.selectList();
172   -// assertEquals(tradeOrderDOs.size(), 1);
173   -// TradeOrderDO tradeOrderDO = tradeOrderDOs.get(0);
174   -// assertEquals(tradeOrderDO.getId(), order.getId());
175   -// assertNotNull(tradeOrderDO.getNo());
176   -// assertEquals(tradeOrderDO.getType(), TradeOrderTypeEnum.NORMAL.getType());
177   -// assertEquals(tradeOrderDO.getTerminal(), TerminalEnum.H5.getTerminal());
178   -// assertEquals(tradeOrderDO.getUserId(), userId);
179   -// assertEquals(tradeOrderDO.getUserIp(), userIp);
180   -// assertEquals(tradeOrderDO.getStatus(), TradeOrderStatusEnum.UNPAID.getStatus());
181   -// assertEquals(tradeOrderDO.getProductCount(), 7);
182   -// assertNull(tradeOrderDO.getFinishTime());
183   -// assertNull(tradeOrderDO.getCancelTime());
184   -// assertNull(tradeOrderDO.getCancelType());
185   -// assertEquals(tradeOrderDO.getUserRemark(), "我是备注");
186   -// assertNull(tradeOrderDO.getRemark());
187   -// assertFalse(tradeOrderDO.getPayStatus());
188   -// assertNull(tradeOrderDO.getPayTime());
189   -// assertEquals(tradeOrderDO.getTotalPrice(), 230);
190   -// assertEquals(tradeOrderDO.getDiscountPrice(), 0);
191   -// assertEquals(tradeOrderDO.getAdjustPrice(), 0);
192   -// assertEquals(tradeOrderDO.getPayPrice(), 80);
193   -// assertEquals(tradeOrderDO.getPayOrderId(), 1000L);
194   -// assertNull(tradeOrderDO.getPayChannelCode());
195   -// assertNull(tradeOrderDO.getLogisticsId());
196   -// assertNull(tradeOrderDO.getDeliveryTime());
197   -// assertNull(tradeOrderDO.getReceiveTime());
198   -// assertEquals(tradeOrderDO.getReceiverName(), "芋艿");
199   -// assertEquals(tradeOrderDO.getReceiverMobile(), "15601691300");
200   -// assertEquals(tradeOrderDO.getReceiverAreaId(), 3306);
201   -// assertEquals(tradeOrderDO.getReceiverDetailAddress(), "土豆村");
202   -// assertEquals(tradeOrderDO.getRefundStatus(), TradeOrderRefundStatusEnum.NONE.getStatus());
203   -// assertEquals(tradeOrderDO.getRefundPrice(), 0);
204   -// assertEquals(tradeOrderDO.getCouponPrice(), 30);
205   -// assertEquals(tradeOrderDO.getPointPrice(), 10);
206   -// // 断言 TradeOrderItemDO 订单(第 1 个)
207   -// List<TradeOrderItemDO> tradeOrderItemDOs = tradeOrderItemMapper.selectList();
208   -// assertEquals(tradeOrderItemDOs.size(), 2);
209   -// TradeOrderItemDO tradeOrderItemDO01 = tradeOrderItemDOs.get(0);
210   -// assertNotNull(tradeOrderItemDO01.getId());
211   -// assertEquals(tradeOrderItemDO01.getUserId(), userId);
212   -// assertEquals(tradeOrderItemDO01.getOrderId(), order.getId());
213   -// assertEquals(tradeOrderItemDO01.getSpuId(), 11L);
214   -// assertEquals(tradeOrderItemDO01.getSkuId(), 1L);
215   -// assertEquals(tradeOrderItemDO01.getProperties().size(), 1);
216   -// assertEquals(tradeOrderItemDO01.getProperties().get(0).getPropertyId(), 111L);
217   -// assertEquals(tradeOrderItemDO01.getProperties().get(0).getValueId(), 222L);
218   -// //assertEquals(tradeOrderItemDO01.getSpuName(), sku01.getSpuName()); TODO 找不到spuName
219   -// assertEquals(tradeOrderItemDO01.getPicUrl(), sku01.getPicUrl());
220   -// assertEquals(tradeOrderItemDO01.getCount(), 3);
221   -//// assertEquals(tradeOrderItemDO01.getOriginalPrice(), 150);
222   -// assertEquals(tradeOrderItemDO01.getPrice(), 50);
223   -// assertEquals(tradeOrderItemDO01.getDiscountPrice(), 20);
224   -// assertEquals(tradeOrderItemDO01.getPayPrice(), 130);
225   -// assertEquals(tradeOrderItemDO01.getAfterSaleStatus(), TradeOrderItemAfterSaleStatusEnum.NONE.getStatus());
226   -// // 断言 TradeOrderItemDO 订单(第 2 个)
227   -// TradeOrderItemDO tradeOrderItemDO02 = tradeOrderItemDOs.get(1);
228   -// assertNotNull(tradeOrderItemDO02.getId());
229   -// assertEquals(tradeOrderItemDO02.getUserId(), userId);
230   -// assertEquals(tradeOrderItemDO02.getOrderId(), order.getId());
231   -// assertEquals(tradeOrderItemDO02.getSpuId(), 21L);
232   -// assertEquals(tradeOrderItemDO02.getSkuId(), 2L);
233   -// assertEquals(tradeOrderItemDO02.getProperties().size(), 1);
234   -// assertEquals(tradeOrderItemDO02.getProperties().get(0).getPropertyId(), 333L);
235   -// assertEquals(tradeOrderItemDO02.getProperties().get(0).getValueId(), 444L);
236   -// //assertEquals(tradeOrderItemDO02.getSpuName(), sku02.getSpuName()); TODO 找不到spuName
237   -// assertEquals(tradeOrderItemDO02.getPicUrl(), sku02.getPicUrl());
238   -// assertEquals(tradeOrderItemDO02.getCount(), 4);
239   -//// assertEquals(tradeOrderItemDO02.getOriginalPrice(), 80);
240   -// assertEquals(tradeOrderItemDO02.getPrice(), 20);
241   -// assertEquals(tradeOrderItemDO02.getDiscountPrice(), 40);
242   -// assertEquals(tradeOrderItemDO02.getPayPrice(), 40);
243   -// assertEquals(tradeOrderItemDO02.getAfterSaleStatus(), TradeOrderItemAfterSaleStatusEnum.NONE.getStatus());
244   -// // 校验调用
245   -// verify(productSkuApi).updateSkuStock(argThat(updateStockReqDTO -> {
246   -// assertEquals(updateStockReqDTO.getItems().size(), 2);
247   -// assertEquals(updateStockReqDTO.getItems().get(0).getId(), 1L);
248   -// assertEquals(updateStockReqDTO.getItems().get(0).getIncrCount(), 3);
249   -// assertEquals(updateStockReqDTO.getItems().get(1).getId(), 2L);
250   -// assertEquals(updateStockReqDTO.getItems().get(1).getIncrCount(), 4);
251   -// return true;
252   -// }));
253   -// verify(couponApi).useCoupon(argThat(reqDTO -> {
254   -// assertEquals(reqDTO.getId(), reqVO.getCouponId());
255   -// assertEquals(reqDTO.getUserId(), userId);
256   -// assertEquals(reqDTO.getOrderId(), order.getId());
257   -// return true;
258   -// }));
259   -// }
  117 + @AfterEach
  118 + public void tearDown() {
  119 + RequestContextHolder.resetRequestAttributes();
  120 + // 清理订单日志的上下文,单测没有 TradeOrderLogAspect 切面清理
  121 + ReflectionTestUtils.invokeMethod(TradeOrderLogAspect.class, "clear");
  122 + }
  123 +
  124 + @Test
  125 + public void testCreateOrder_success() {
  126 + // DONE 芋艿:重新高下
  127 + // 准备参数
  128 + Long userId = 100L;
  129 + String userIp = "127.0.0.1";
  130 + AppTradeOrderCreateReqVO reqVO = new AppTradeOrderCreateReqVO();
  131 + reqVO.setRemark("我是备注");
  132 + reqVO.setItems(Arrays.asList(new AppTradeOrderSettlementReqVO.Item().setCartId(1L),
  133 + new AppTradeOrderSettlementReqVO.Item().setCartId(2L)));
  134 + reqVO.setCouponId(101L);
  135 + reqVO.setPointStatus(false);
  136 + reqVO.setDeliveryType(DeliveryTypeEnum.EXPRESS.getType());
  137 + reqVO.setAddressId(10L);
  138 + // mock 请求(用户 IP、终端)
  139 + MockHttpServletRequest request = new MockHttpServletRequest();
  140 + request.setRemoteAddr(userIp);
  141 + request.addHeader(WebFrameworkUtils.HEADER_TERMINAL, TerminalEnum.H5.getTerminal());
  142 + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
  143 + // mock 方法(购物车)
  144 + when(cartService.getCartList(eq(userId), eq(asSet(1L, 2L)))).thenReturn(Arrays.asList(
  145 + new CartDO().setId(1L).setUserId(userId).setSpuId(11L).setSkuId(1L).setCount(3).setSelected(true),
  146 + new CartDO().setId(2L).setUserId(userId).setSpuId(21L).setSkuId(2L).setCount(4).setSelected(true)));
  147 + // mock 方法(价格计算)
  148 + TradePriceCalculateRespBO.OrderItem priceOrderItem01 = new TradePriceCalculateRespBO.OrderItem()
  149 + .setSpuId(11L).setSkuId(1L).setCount(3).setCartId(1L).setSelected(true).setSpuName("商品 1")
  150 + .setPrice(50).setDiscountPrice(20).setDeliveryPrice(10).setCouponPrice(10).setPointPrice(5)
  151 + .setVipPrice(0).setPayPrice(105);
  152 + TradePriceCalculateRespBO.OrderItem priceOrderItem02 = new TradePriceCalculateRespBO.OrderItem()
  153 + .setSpuId(21L).setSkuId(2L).setCount(4).setCartId(2L).setSelected(true).setSpuName("商品 2")
  154 + .setPrice(20).setDiscountPrice(10).setDeliveryPrice(10).setCouponPrice(20).setPointPrice(5)
  155 + .setVipPrice(0).setPayPrice(55);
  156 + TradePriceCalculateRespBO.Price price = new TradePriceCalculateRespBO.Price()
  157 + .setTotalPrice(230).setDiscountPrice(30).setDeliveryPrice(20).setCouponPrice(30)
  158 + .setPointPrice(10).setVipPrice(0).setPayPrice(160);
  159 + when(tradePriceService.calculateOrderPrice(argThat(calculateReqBO -> {
  160 + assertEquals(userId, calculateReqBO.getUserId());
  161 + assertEquals(101L, calculateReqBO.getCouponId());
  162 + assertEquals(2, calculateReqBO.getItems().size());
  163 + assertEquals(1L, calculateReqBO.getItems().get(0).getSkuId());
  164 + assertEquals(3, calculateReqBO.getItems().get(0).getCount());
  165 + assertEquals(2L, calculateReqBO.getItems().get(1).getSkuId());
  166 + assertEquals(4, calculateReqBO.getItems().get(1).getCount());
  167 + return true;
  168 + }))).thenReturn(new TradePriceCalculateRespBO().setType(TradeOrderTypeEnum.NORMAL.getType())
  169 + .setPrice(price).setItems(Arrays.asList(priceOrderItem01, priceOrderItem02)));
  170 + // mock 方法(用户收件地址)
  171 + when(addressApi.getAddress(eq(10L), eq(userId))).thenReturn(new MemberAddressRespDTO().setId(10L)
  172 + .setUserId(userId).setName("芋艿").setMobile("15601691300").setAreaId(3306).setDetailAddress("土豆村"));
  173 + // mock 方法(创建支付单)
  174 + when(payOrderApi.createOrder(argThat(createReqDTO -> {
  175 + assertEquals("mall", createReqDTO.getAppKey());
  176 + assertEquals(userIp, createReqDTO.getUserIp());
  177 + assertEquals(userId, createReqDTO.getUserId());
  178 + assertNotNull(createReqDTO.getMerchantOrderId()); // 由于 tradeOrderId 后生成,只能校验非空
  179 + assertEquals("商品 1", createReqDTO.getSubject());
  180 + assertEquals(160, createReqDTO.getPrice());
  181 + assertNotNull(createReqDTO.getExpireTime());
  182 + return true;
  183 + }))).thenReturn(1000L);
  184 +
  185 + // 调用
  186 + TradeOrderDO order = tradeOrderUpdateService.createOrder(userId, reqVO);
  187 + // 断言 TradeOrderDO 订单
  188 + List<TradeOrderDO> tradeOrderDOs = tradeOrderMapper.selectList();
  189 + assertEquals(1, tradeOrderDOs.size());
  190 + TradeOrderDO tradeOrderDO = tradeOrderDOs.get(0);
  191 + assertEquals(order.getId(), tradeOrderDO.getId());
  192 + assertNotNull(tradeOrderDO.getNo());
  193 + assertEquals(TradeOrderTypeEnum.NORMAL.getType(), tradeOrderDO.getType());
  194 + assertEquals(TerminalEnum.H5.getTerminal(), tradeOrderDO.getTerminal());
  195 + assertEquals(userId, tradeOrderDO.getUserId());
  196 + assertEquals(userIp, tradeOrderDO.getUserIp());
  197 + assertEquals(TradeOrderStatusEnum.UNPAID.getStatus(), tradeOrderDO.getStatus());
  198 + assertEquals(7, tradeOrderDO.getProductCount());
  199 + assertEquals("我是备注", tradeOrderDO.getUserRemark());
  200 + assertFalse(tradeOrderDO.getPayStatus());
  201 + assertNull(tradeOrderDO.getPayTime());
  202 + assertEquals(1000L, tradeOrderDO.getPayOrderId());
  203 + assertEquals(230, tradeOrderDO.getTotalPrice());
  204 + assertEquals(30, tradeOrderDO.getDiscountPrice());
  205 + assertEquals(20, tradeOrderDO.getDeliveryPrice());
  206 + assertEquals(0, tradeOrderDO.getAdjustPrice());
  207 + assertEquals(160, tradeOrderDO.getPayPrice());
  208 + assertEquals(DeliveryTypeEnum.EXPRESS.getType(), tradeOrderDO.getDeliveryType());
  209 + assertEquals("芋艿", tradeOrderDO.getReceiverName());
  210 + assertEquals("15601691300", tradeOrderDO.getReceiverMobile());
  211 + assertEquals(3306, tradeOrderDO.getReceiverAreaId());
  212 + assertEquals("土豆村", tradeOrderDO.getReceiverDetailAddress());
  213 + assertEquals(TradeOrderRefundStatusEnum.NONE.getStatus(), tradeOrderDO.getRefundStatus());
  214 + assertEquals(0, tradeOrderDO.getRefundPrice());
  215 + assertEquals(101L, tradeOrderDO.getCouponId());
  216 + assertEquals(30, tradeOrderDO.getCouponPrice());
  217 + assertEquals(10, tradeOrderDO.getPointPrice());
  218 + // 断言 TradeOrderItemDO 订单项
  219 + List<TradeOrderItemDO> tradeOrderItemDOs = tradeOrderItemMapper.selectListByOrderId(order.getId());
  220 + assertEquals(2, tradeOrderItemDOs.size());
  221 + assertOrderItem(tradeOrderItemDOs.get(0), order, priceOrderItem01);
  222 + assertOrderItem(tradeOrderItemDOs.get(1), order, priceOrderItem02);
  223 + // 断言调用
  224 + verify(tradeOrderHandler).beforeOrderCreate(any(), any());
  225 + verify(tradeOrderHandler).afterOrderCreate(any(), any());
  226 + verify(cartService).deleteCart(eq(userId), eq(asSet(1L, 2L)));
  227 + }
  228 +
  229 + private static void assertOrderItem(TradeOrderItemDO orderItem, TradeOrderDO order,
  230 + TradePriceCalculateRespBO.OrderItem priceOrderItem) {
  231 + assertNotNull(orderItem.getId());
  232 + assertEquals(order.getUserId(), orderItem.getUserId());
  233 + assertEquals(order.getId(), orderItem.getOrderId());
  234 + assertEquals(priceOrderItem.getSpuId(), orderItem.getSpuId());
  235 + assertEquals(priceOrderItem.getSpuName(), orderItem.getSpuName()); // DONE 找不到spuName
  236 + assertEquals(priceOrderItem.getSkuId(), orderItem.getSkuId());
  237 + assertEquals(priceOrderItem.getCartId(), orderItem.getCartId());
  238 + assertEquals(priceOrderItem.getCount(), orderItem.getCount());
  239 + assertEquals(priceOrderItem.getPrice(), orderItem.getPrice());
  240 + assertEquals(priceOrderItem.getDiscountPrice(), orderItem.getDiscountPrice());
  241 + assertEquals(priceOrderItem.getPayPrice(), orderItem.getPayPrice());
  242 + assertEquals(TradeOrderItemAfterSaleStatusEnum.NONE.getStatus(), orderItem.getAfterSaleStatus());
  243 + assertFalse(orderItem.getCommentStatus());
  244 + }
260 245  
261 246 @Test
262 247 public void testUpdateOrderPaid() {
... ... @@ -282,6 +267,7 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
282 267 assertTrue(dbOrder.getPayStatus());
283 268 assertNotNull(dbOrder.getPayTime());
284 269 assertEquals(dbOrder.getPayChannelCode(), "wx_pub");
  270 + verify(tradeOrderHandler).afterPayOrder(argThat(o -> o.getId().equals(id)), anyList());
285 271 }
286 272  
287 273 @Test
... ... @@ -294,15 +280,12 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
294 280 o.setDeliveryType(DeliveryTypeEnum.EXPRESS.getType());
295 281 });
296 282 tradeOrderMapper.insert(order);
297   -
298   - DeliveryExpressCreateReqVO expressCreateReqVO = new DeliveryExpressCreateReqVO();
299   - expressCreateReqVO.setCode("code").setName("Name").setLogo("logo").setSort(0).setStatus(CommonStatusEnum.ENABLE.getStatus());
300   - Long deliveryExpressId = deliveryExpressService.createDeliveryExpress(expressCreateReqVO);
301 283 // 准备参数
302 284 TradeOrderDeliveryReqVO deliveryReqVO = new TradeOrderDeliveryReqVO().setId(1L)
303   - .setLogisticsId(deliveryExpressId).setLogisticsNo("100");
304   -
305   - // mock 方法(支付单)
  285 + .setLogisticsId(20L).setLogisticsNo("100");
  286 + // mock 方法(物流公司)
  287 + when(deliveryExpressService.validateDeliveryExpress(eq(20L))).thenReturn(
  288 + randomPojo(DeliveryExpressDO.class, o -> o.setId(20L)));
306 289  
307 290 // 调用
308 291 tradeOrderUpdateService.deliveryOrder(deliveryReqVO);
... ... @@ -311,6 +294,9 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
311 294 assertEquals(dbOrder.getStatus(), TradeOrderStatusEnum.DELIVERED.getStatus());
312 295 assertPojoEquals(dbOrder, deliveryReqVO);
313 296 assertNotNull(dbOrder.getDeliveryTime());
  297 + // 断言调用(站内信)
  298 + verify(tradeMessageService).sendMessageWhenDeliveryOrder(argThat(reqBO ->
  299 + reqBO.getOrderId().equals(1L) && reqBO.getUserId().equals(order.getUserId())));
314 300 }
315 301  
316 302 @Test
... ... @@ -324,7 +310,6 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
324 310 // 准备参数
325 311 Long id = 1L;
326 312 Long userId = 10L;
327   - // mock 方法(支付单)
328 313  
329 314 // 调用
330 315 tradeOrderUpdateService.receiveOrderByMember(userId, id);
... ... @@ -332,6 +317,7 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
332 317 TradeOrderDO dbOrder = tradeOrderMapper.selectById(1L);
333 318 assertEquals(dbOrder.getStatus(), TradeOrderStatusEnum.COMPLETED.getStatus());
334 319 assertNotNull(dbOrder.getReceiveTime());
  320 + verify(tradeOrderHandler).afterReceiveOrder(argThat(o -> o.getId().equals(id)));
335 321 }
336 322  
337 323 }
... ...
yudao-module-mall/yudao-module-trade/src/test/resources/sql/create_tables.sql
... ... @@ -49,12 +49,15 @@ CREATE TABLE IF NOT EXISTS &quot;trade_order&quot;
49 49 "refund_point" int NULL,
50 50 "vip_price" int NULL,
51 51 "give_coupons_map" varchar NULL,
  52 + "give_coupon_template_counts" varchar NULL,
  53 + "give_coupon_ids" varchar NULL,
52 54 "seckill_activity_id" long NULL,
53 55 "bargain_activity_id" long NULL,
54 56 "bargain_record_id" long NULL,
55 57 "combination_activity_id" long NULL,
56 58 "combination_head_id" long NULL,
57 59 "combination_record_id" long NULL,
  60 + "point_activity_id" bigint NULL,
58 61 "creator" varchar DEFAULT '',
59 62 "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
60 63 "updater" varchar DEFAULT '',
... ...
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletTransactionMapper.java
... ... @@ -39,7 +39,7 @@ public interface PayWalletTransactionMapper extends BaseMapperX&lt;PayWalletTransac
39 39 }
40 40  
41 41 default Integer selectPriceSum(Long walletId, Integer type, LocalDateTime[] createTime) {
42   - // SQL sum 查询;支出流水的 price 为负值,汇总取正值(与钱包 total_expense 的正值语义一致)
  42 + // SQL sum 查询;支出流水的 price 为负值,取反后汇总为正值
43 43 String sumColumn = Objects.equals(type, TYPE_EXPENSE) ? "SUM(-price) AS priceSum" : "SUM(price) AS priceSum";
44 44 List<Map<String, Object>> result = selectMaps(new QueryWrapperX<PayWalletTransactionDO>()
45 45 .select(sumColumn)
... ...