Commit 094330778556ce4b9fbde3293fff670865b7555c

Authored by YunaiV
1 parent 2f5984af

fix(mall): 修复全局比例分佣商品预估佣金为 0,对应 https://t.zsxq.com/Lsu8i

yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java
... ... @@ -38,7 +38,6 @@ import java.util.*;
38 38 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
39 39 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue;
40 40 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMinValue;
41   -import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
42 41 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH;
43 42  
44 43 /**
... ... @@ -143,7 +142,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
143 142 int calculatePrice(Integer basePrice, Integer percent, Integer fixedPrice) {
144 143 // 1. 优先使用固定佣金
145 144 if (fixedPrice != null && fixedPrice >= 0) {
146   - return ObjectUtil.defaultIfNull(fixedPrice, 0);
  145 + return fixedPrice;
147 146 }
148 147 // 2. 根据比例计算佣金
149 148 if (basePrice != null && basePrice > 0 && percent != null && percent > 0) {
... ... @@ -329,7 +328,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
329 328 return respVO;
330 329 }
331 330 // 2.2 校验用户是否有分销资格
332   - respVO.setEnabled(brokerageUserService.getUserBrokerageEnabled(getLoginUserId()));
  331 + respVO.setEnabled(brokerageUserService.getUserBrokerageEnabled(userId));
333 332 if (BooleanUtil.isFalse(respVO.getEnabled())) {
334 333 return respVO;
335 334 }
... ... @@ -339,22 +338,22 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
339 338 return respVO;
340 339 }
341 340  
342   - // 3.1 商品单独分佣模式
343   - Integer fixedMinPrice = 0;
344   - Integer fixedMaxPrice = 0;
345   - Integer spuMinPrice = 0;
346   - Integer spuMaxPrice = 0;
  341 + // 3.1 获取商品 SKU 列表
347 342 List<ProductSkuRespDTO> skuList = productSkuApi.getSkuListBySpuId(ListUtil.of(spuId));
348 343 if (BooleanUtil.isTrue(spu.getSubCommissionType())) {
349   - fixedMinPrice = getMinValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice);
350   - fixedMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice);
351   - // 3.2 全局分佣模式(根据商品价格比例计算)
  344 + // 3.2.1 商品独立分销模式:直接取 SKU 固定佣金
  345 + // 注意:固定佣金允许为 0,表示商家主动设为零佣金
  346 + Integer fixedMinPrice = getMinValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice);
  347 + Integer fixedMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice);
  348 + respVO.setBrokerageMinPrice(calculatePrice(null, tradeConfig.getBrokerageFirstPercent(), fixedMinPrice))
  349 + .setBrokerageMaxPrice(calculatePrice(null, tradeConfig.getBrokerageFirstPercent(), fixedMaxPrice));
352 350 } else {
353   - spuMinPrice = getMinValue(skuList, ProductSkuRespDTO::getPrice);
354   - spuMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getPrice);
  351 + // 3.2.2 全局比例模式:固定佣金传 null,避免被默认值 0 提前拦截
  352 + Integer spuMinPrice = getMinValue(skuList, ProductSkuRespDTO::getPrice);
  353 + Integer spuMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getPrice);
  354 + respVO.setBrokerageMinPrice(calculatePrice(spuMinPrice, tradeConfig.getBrokerageFirstPercent(), null))
  355 + .setBrokerageMaxPrice(calculatePrice(spuMaxPrice, tradeConfig.getBrokerageFirstPercent(), null));
355 356 }
356   - respVO.setBrokerageMinPrice(calculatePrice(spuMinPrice, tradeConfig.getBrokerageFirstPercent(), fixedMinPrice));
357   - respVO.setBrokerageMaxPrice(calculatePrice(spuMaxPrice, tradeConfig.getBrokerageFirstPercent(), fixedMaxPrice));
358 357 return respVO;
359 358 }
360 359  
... ...
yudao-module-mall/yudao-module-trade/src/test/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImplTest.java
1 1 package cn.iocoder.yudao.module.trade.service.brokerage;
2 2  
  3 +import cn.hutool.core.collection.ListUtil;
3 4 import cn.hutool.core.util.NumberUtil;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  7 +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
  8 +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
  9 +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
  10 +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
6 11 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO;
  12 +import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO;
7 13 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageRecordDO;
  14 +import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
8 15 import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageRecordMapper;
  16 +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
  17 +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum;
9 18 import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
10 19 import jakarta.annotation.Resource;
11   -import org.junit.jupiter.api.Disabled;
12 20 import org.junit.jupiter.api.Test;
13 21 import org.springframework.context.annotation.Import;
14 22 import org.springframework.test.context.bean.override.mockito.MockitoBean;
15 23  
16 24 import java.math.RoundingMode;
  25 +import java.util.List;
17 26  
18   -import static cn.hutool.core.util.RandomUtil.randomEle;
19   -import static cn.hutool.core.util.RandomUtil.randomInt;
20 27 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
  28 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
21 29 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
22 30 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
23   -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomInteger;
24 31 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
25 32 import static org.junit.jupiter.api.Assertions.assertEquals;
  33 +import static org.junit.jupiter.api.Assertions.assertTrue;
  34 +import static org.mockito.Mockito.when;
26 35  
27   -// TODO @芋艿:单测后续看看
28 36 /**
29 37 * {@link BrokerageRecordServiceImpl} 的单元测试类
30 38 *
31 39 * @author owen
32 40 */
33   -@Disabled // TODO 芋艿:后续 fix 补充的单测
34 41 @Import(BrokerageRecordServiceImpl.class)
35 42 public class BrokerageRecordServiceImplTest extends BaseDbUnitTest {
36 43  
... ... @@ -43,31 +50,43 @@ public class BrokerageRecordServiceImplTest extends BaseDbUnitTest {
43 50 private TradeConfigService tradeConfigService;
44 51 @MockitoBean
45 52 private BrokerageUserService brokerageUserService;
  53 + @MockitoBean
  54 + private ProductSpuApi productSpuApi;
  55 + @MockitoBean
  56 + private ProductSkuApi productSkuApi;
46 57  
47 58 @Test
48   - @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
49 59 public void testGetBrokerageRecordPage() {
50 60 // mock 数据
51 61 BrokerageRecordDO dbBrokerageRecord = randomPojo(BrokerageRecordDO.class, o -> { // 等会查询到
52   - o.setUserId(null);
53   - o.setBizType(null);
54   - o.setStatus(null);
55   - o.setCreateTime(null);
  62 + o.setUserId(1L);
  63 + o.setBizType(BrokerageRecordBizTypeEnum.ORDER.getType());
  64 + o.setStatus(BrokerageRecordStatusEnum.SETTLEMENT.getStatus());
  65 + o.setSourceUserLevel(1);
  66 + o.setSourceUserId(100L);
  67 + o.setCreateTime(buildTime(2023, 2, 10));
  68 + o.setDeleted(false);
56 69 });
57 70 brokerageRecordMapper.insert(dbBrokerageRecord);
58 71 // 测试 userId 不匹配
59   - brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord, o -> o.setUserId(null)));
  72 + brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord, o -> o.setUserId(2L)));
60 73 // 测试 bizType 不匹配
61   - brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord, o -> o.setBizType(null)));
  74 + brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord,
  75 + o -> o.setBizType(BrokerageRecordBizTypeEnum.WITHDRAW.getType())));
62 76 // 测试 status 不匹配
63   - brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord, o -> o.setStatus(null)));
  77 + brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord,
  78 + o -> o.setStatus(BrokerageRecordStatusEnum.CANCEL.getStatus())));
  79 + // 测试 sourceUserLevel 不匹配
  80 + brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord, o -> o.setSourceUserLevel(2)));
64 81 // 测试 createTime 不匹配
65   - brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord, o -> o.setCreateTime(null)));
  82 + brokerageRecordMapper.insert(cloneIgnoreId(dbBrokerageRecord,
  83 + o -> o.setCreateTime(buildTime(2023, 3, 1))));
66 84 // 准备参数
67 85 BrokerageRecordPageReqVO reqVO = new BrokerageRecordPageReqVO();
68   - reqVO.setUserId(null);
69   - reqVO.setBizType(null);
70   - reqVO.setStatus(null);
  86 + reqVO.setUserId(1L);
  87 + reqVO.setBizType(BrokerageRecordBizTypeEnum.ORDER.getType());
  88 + reqVO.setStatus(BrokerageRecordStatusEnum.SETTLEMENT.getStatus());
  89 + reqVO.setSourceUserLevel(1);
71 90 reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
72 91  
73 92 // 调用
... ... @@ -81,37 +100,128 @@ public class BrokerageRecordServiceImplTest extends BaseDbUnitTest {
81 100 @Test
82 101 public void testCalculatePrice_useFixedPrice() {
83 102 // mock 数据
84   - Integer payPrice = randomInteger();
85   - Integer percent = randomInt(1, 101);
86   - Integer fixedPrice = randomInt();
  103 + Integer payPrice = 1000;
  104 + Integer percent = 10;
  105 + Integer fixedPrice = 88;
87 106 // 调用
88 107 int brokerage = brokerageRecordService.calculatePrice(payPrice, percent, fixedPrice);
89 108 // 断言
90   - assertEquals(brokerage, fixedPrice);
  109 + assertEquals(fixedPrice, brokerage);
91 110 }
92 111  
93 112 @Test
94 113 public void testCalculatePrice_usePercent() {
95   - // mock 数据
96   - Integer payPrice = randomInteger();
97   - Integer percent = randomInt(1, 101);
98   - Integer fixedPrice = randomEle(new Integer[]{0, null});
99   - System.out.println("fixedPrice=" + fixedPrice);
  114 + // mock 数据:fixedPrice 为 null 时,按比例计算佣金
  115 + Integer payPrice = 1000;
  116 + Integer percent = 10;
  117 + Integer fixedPrice = null;
100 118 // 调用
101 119 int brokerage = brokerageRecordService.calculatePrice(payPrice, percent, fixedPrice);
102 120 // 断言
103   - assertEquals(brokerage, NumberUtil.div(NumberUtil.mul(payPrice, percent), 100, 0, RoundingMode.DOWN).intValue());
  121 + assertEquals(NumberUtil.div(NumberUtil.mul(payPrice, percent), 100, 0,
  122 + RoundingMode.DOWN).intValue(), brokerage);
  123 + }
  124 +
  125 + @Test
  126 + public void testCalculatePrice_fixedPriceIsZero() {
  127 + // mock 数据:fixedPrice 为 0 时,仍使用固定佣金,不回退到比例计算
  128 + Integer payPrice = 1000;
  129 + Integer percent = 10;
  130 + Integer fixedPrice = 0;
  131 + // 调用
  132 + int brokerage = brokerageRecordService.calculatePrice(payPrice, percent, fixedPrice);
  133 + // 断言
  134 + assertEquals(0, brokerage);
104 135 }
105 136  
106 137 @Test
107 138 public void testCalculatePrice_equalsZero() {
108   - // mock 数据
  139 + // mock 数据:三个参数均无效时,返回 0
109 140 Integer payPrice = null;
110 141 Integer percent = null;
111 142 Integer fixedPrice = null;
112 143 // 调用
113 144 int brokerage = brokerageRecordService.calculatePrice(payPrice, percent, fixedPrice);
114 145 // 断言
115   - assertEquals(brokerage, 0);
  146 + assertEquals(0, brokerage);
  147 + }
  148 +
  149 + @Test
  150 + public void testCalculateProductBrokeragePrice_globalPercent() {
  151 + // mock:分销功能已开启,比例 10%,当前用户有分销资格
  152 + TradeConfigDO tradeConfig = new TradeConfigDO();
  153 + tradeConfig.setBrokerageEnabled(true);
  154 + tradeConfig.setBrokerageFirstPercent(10);
  155 + when(tradeConfigService.getTradeConfig()).thenReturn(tradeConfig);
  156 + when(brokerageUserService.getUserBrokerageEnabled(100L)).thenReturn(true);
  157 +
  158 + // mock:商品未开启独立分销(subCommissionType = false),SKU 售价 1000 分
  159 + ProductSpuRespDTO spu = new ProductSpuRespDTO();
  160 + spu.setSubCommissionType(false);
  161 + when(productSpuApi.getSpu(1L)).thenReturn(spu);
  162 +
  163 + ProductSkuRespDTO sku = new ProductSkuRespDTO();
  164 + sku.setPrice(1000);
  165 + when(productSkuApi.getSkuListBySpuId(ListUtil.of(1L))).thenReturn(List.of(sku));
  166 +
  167 + // 调用
  168 + AppBrokerageProductPriceRespVO result = brokerageRecordService.calculateProductBrokeragePrice(100L, 1L);
  169 +
  170 + // 断言:按 10% 比例计算,1000 * 10% = 100 分(向下取整)
  171 + assertTrue(result.getEnabled());
  172 + assertEquals(100, result.getBrokerageMinPrice());
  173 + assertEquals(100, result.getBrokerageMaxPrice());
  174 + }
  175 +
  176 + @Test
  177 + public void testCalculateProductBrokeragePrice_subCommissionZeroFixed() {
  178 + // mock:分销功能已开启,比例 10%,当前用户有分销资格
  179 + TradeConfigDO tradeConfig = new TradeConfigDO();
  180 + tradeConfig.setBrokerageEnabled(true);
  181 + tradeConfig.setBrokerageFirstPercent(10);
  182 + when(tradeConfigService.getTradeConfig()).thenReturn(tradeConfig);
  183 + when(brokerageUserService.getUserBrokerageEnabled(100L)).thenReturn(true);
  184 +
  185 + // mock:商品开启独立分销(subCommissionType = true),SKU 固定佣金为 0(商家主动设置)
  186 + ProductSpuRespDTO spu = new ProductSpuRespDTO();
  187 + spu.setSubCommissionType(true);
  188 + when(productSpuApi.getSpu(2L)).thenReturn(spu);
  189 +
  190 + ProductSkuRespDTO sku = new ProductSkuRespDTO();
  191 + sku.setPrice(1000);
  192 + sku.setFirstBrokeragePrice(0);
  193 + when(productSkuApi.getSkuListBySpuId(ListUtil.of(2L))).thenReturn(List.of(sku));
  194 +
  195 + // 调用
  196 + AppBrokerageProductPriceRespVO result = brokerageRecordService.calculateProductBrokeragePrice(100L, 2L);
  197 +
  198 + // 断言:独立分销固定佣金为 0,应返回 0,不得回退到全局比例
  199 + assertTrue(result.getEnabled());
  200 + assertEquals(0, result.getBrokerageMinPrice());
  201 + assertEquals(0, result.getBrokerageMaxPrice());
  202 + }
  203 +
  204 + @Test
  205 + public void testCalculateProductBrokeragePrice_subCommissionEmptySkuList() {
  206 + // mock:分销功能已开启,比例 10%,当前用户有分销资格
  207 + TradeConfigDO tradeConfig = new TradeConfigDO();
  208 + tradeConfig.setBrokerageEnabled(true);
  209 + tradeConfig.setBrokerageFirstPercent(10);
  210 + when(tradeConfigService.getTradeConfig()).thenReturn(tradeConfig);
  211 + when(brokerageUserService.getUserBrokerageEnabled(100L)).thenReturn(true);
  212 +
  213 + // mock:商品开启独立分销,但查询不到 SKU 固定佣金
  214 + ProductSpuRespDTO spu = new ProductSpuRespDTO();
  215 + spu.setSubCommissionType(true);
  216 + when(productSpuApi.getSpu(3L)).thenReturn(spu);
  217 + when(productSkuApi.getSkuListBySpuId(ListUtil.of(3L))).thenReturn(List.of());
  218 +
  219 + // 调用
  220 + AppBrokerageProductPriceRespVO result = brokerageRecordService.calculateProductBrokeragePrice(100L, 3L);
  221 +
  222 + // 断言:独立分销没有固定佣金时,不回退到全局比例
  223 + assertTrue(result.getEnabled());
  224 + assertEquals(0, result.getBrokerageMinPrice());
  225 + assertEquals(0, result.getBrokerageMaxPrice());
116 226 }
117 227 }
... ...
yudao-module-mall/yudao-module-trade/src/test/resources/sql/create_tables.sql
... ... @@ -173,7 +173,7 @@ CREATE TABLE IF NOT EXISTS &quot;trade_brokerage_user&quot;
173 173 ) COMMENT '分销用户';
174 174 CREATE TABLE IF NOT EXISTS "trade_brokerage_record"
175 175 (
176   - "id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  176 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
177 177 "user_id" bigint NOT NULL,
178 178 "biz_id" varchar NOT NULL,
179 179 "biz_type" varchar NOT NULL,
... ... @@ -184,6 +184,8 @@ CREATE TABLE IF NOT EXISTS &quot;trade_brokerage_record&quot;
184 184 "status" varchar NOT NULL,
185 185 "frozen_days" int NOT NULL,
186 186 "unfreeze_time" varchar,
  187 + "source_user_level" int,
  188 + "source_user_id" bigint,
187 189 "creator" varchar DEFAULT '',
188 190 "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
189 191 "updater" varchar DEFAULT '',
... ... @@ -232,4 +234,4 @@ CREATE TABLE IF NOT EXISTS &quot;trade_delivery_express&quot;
232 234 "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
233 235 "deleted" bit NOT NULL DEFAULT FALSE,
234 236 PRIMARY KEY ("id")
235   -) COMMENT '佣金提现';
236 237 \ No newline at end of file
  238 +) COMMENT '佣金提现';
... ...