Commit 5934d6b0299b0091e1b652aeaf1bad7e968c6a1f

Authored by YunaiV
1 parent 84f6ec10

trade:完成下单时,创建支付单逻辑

yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
1 1 package cn.iocoder.yudao.module.trade.convert.order;
2 2  
  3 +import cn.hutool.core.date.DateUtil;
3 4 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  5 +import cn.iocoder.yudao.framework.common.util.date.DateUtils;
4 6 import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
  7 +import cn.iocoder.yudao.module.pay.api.order.PayOrderInfoCreateReqDTO;
5 8 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
6 9 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
  10 +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
7 11 import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
8 12 import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
9 13 import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO;
10 14 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
11 15 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
12 16 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemRefundStatusEnum;
  17 +import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
13 18 import org.mapstruct.Mapper;
14 19 import org.mapstruct.Mapping;
15 20 import org.mapstruct.Mappings;
16 21 import org.mapstruct.factory.Mappers;
17 22  
  23 +import java.time.Duration;
18 24 import java.util.List;
19 25 import java.util.Map;
20 26  
21 27 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  28 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
22 29  
23 30 @Mapper
24 31 public interface TradeOrderConvert {
... ... @@ -67,4 +74,20 @@ public interface TradeOrderConvert {
67 74 ProductSkuUpdateStockReqDTO.Item convert(TradeOrderItemDO bean);
68 75 List<ProductSkuUpdateStockReqDTO.Item> convertList(List<TradeOrderItemDO> list);
69 76  
  77 + default PayOrderInfoCreateReqDTO convert(TradeOrderDO tradeOrderDO, List<TradeOrderItemDO> tradeOrderItemDOs,
  78 + List<ProductSpuRespDTO> spus, TradeOrderProperties tradeOrderProperties) {
  79 + PayOrderInfoCreateReqDTO createReqDTO = new PayOrderInfoCreateReqDTO()
  80 + .setAppId(tradeOrderProperties.getAppId()).setUserIp(tradeOrderDO.getUserIp());
  81 + // 商户相关字段
  82 + createReqDTO.setMerchantOrderId(String.valueOf(tradeOrderDO.getId()));
  83 + String subject = spus.get(0).getName();
  84 + if (spus.size() > 1) {
  85 + subject += " 等多件";
  86 + }
  87 + createReqDTO.setSubject(subject);
  88 + // 订单相关字段
  89 + createReqDTO.setAmount(tradeOrderDO.getPayPrice()).setExpireTime(addTime(tradeOrderProperties.getExpireTime()));
  90 + return createReqDTO;
  91 + }
  92 +
70 93 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/pay/PayOrderConvert.java deleted
1   -package cn.iocoder.yudao.module.trade.convert.pay;
2   -
3   -import cn.hutool.core.date.DateUtil;
4   -import cn.iocoder.yudao.module.pay.api.order.PayOrderInfoCreateReqDTO;
5   -import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
6   -import org.mapstruct.Mapper;
7   -import org.mapstruct.Mapping;
8   -import org.mapstruct.Mappings;
9   -import org.mapstruct.Named;
10   -import org.mapstruct.factory.Mappers;
11   -
12   -import java.util.Date;
13   -
14   -@Mapper
15   -public interface PayOrderConvert {
16   -
17   - PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class);
18   -
19   - @Mappings({
20   - @Mapping(source = "payPrice", target = "amount"),
21   - @Mapping(target = "expireTime", source = "cancelTime" , qualifiedByName = "convertCreateTimeToPayExpireTime")
22   - })
23   - PayOrderInfoCreateReqDTO convert(TradeOrderDO tradeOrderDO);
24   -
25   - @Named("convertCreateTimeToPayExpireTime")
26   - default Date convertCreateTimeToPayExpireTime(Date cancelTime) {
27   - return DateUtil.offsetMinute(new Date(), 30);
28   - }
29   -
30   -}
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java
... ... @@ -5,8 +5,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
5 5 import org.springframework.validation.annotation.Validated;
6 6  
7 7 import javax.validation.constraints.NotNull;
  8 +import java.time.Duration;
8 9  
9 10 /**
  11 + * 交易订单的配置项
  12 + *
10 13 * @author LeeYan9
11 14 * @since 2022-09-15
12 15 */
... ... @@ -16,15 +19,15 @@ import javax.validation.constraints.NotNull;
16 19 public class TradeOrderProperties {
17 20  
18 21 /**
19   - * 商户订单编号
20   - */
21   - @NotNull(message = "商户订单编号不能为空")
22   - private String merchantOrderId;
23   -
24   - /**
25 22 * 应用编号
26 23 */
27 24 @NotNull(message = "应用编号不能为空")
28 25 private Long appId;
29 26  
  27 + /**
  28 + * 支付超时时间
  29 + */
  30 + @NotNull(message = "支付超时时间不能为空")
  31 + private Duration expireTime;
  32 +
30 33 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderServiceImpl.java
1 1 package cn.iocoder.yudao.module.trade.service.order;
2 2  
3   -import cn.hutool.core.bean.BeanUtil;
4   -import cn.hutool.core.text.StrBuilder;
5 3 import cn.hutool.core.util.IdUtil;
6 4 import cn.hutool.core.util.ObjectUtil;
7 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
8 6 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
9 7 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
10   -import cn.iocoder.yudao.framework.common.util.string.StrUtils;
11 8 import cn.iocoder.yudao.module.member.api.address.AddressApi;
12 9 import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
13 10 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
... ... @@ -30,7 +27,6 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
30 27 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
31 28 import cn.iocoder.yudao.module.trade.dal.mysql.orderitem.TradeOrderItemMapper;
32 29 import cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants;
33   -import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemRefundStatusEnum;
34 30 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
35 31 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
36 32 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
... ... @@ -58,10 +54,6 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_CREAT
58 54 @Service
59 55 public class TradeOrderServiceImpl implements TradeOrderService {
60 56  
61   - // TODO LeeYan9: 静态变量, 需要在最前面哈; 另外, 静态变量的注释最好写下;
62   - private static final String BLANK_PLACEHOLDER = " ";
63   - private static final String MULTIPLIER_PLACEHOLDER = "x";
64   -
65 57 @Resource
66 58 private TradeOrderMapper tradeOrderMapper;
67 59 @Resource
... ... @@ -89,7 +81,7 @@ public class TradeOrderServiceImpl implements TradeOrderService {
89 81 // 商品 SKU 检查:可售状态、库存
90 82 List<ProductSkuRespDTO> skus = validateSkuSaleable(createReqVO.getItems());
91 83 // 商品 SPU 检查:可售状态
92   - validateSpuSaleable(convertSet(skus, ProductSkuRespDTO::getSpuId));
  84 + List<ProductSpuRespDTO> spus = validateSpuSaleable(convertSet(skus, ProductSkuRespDTO::getSpuId));
93 85 // 用户收件地址的校验
94 86 AddressRespDTO address = validateAddress(userId, createReqVO.getAddressId());
95 87  
... ... @@ -102,55 +94,11 @@ public class TradeOrderServiceImpl implements TradeOrderService {
102 94 List<TradeOrderItemDO> tradeOrderItems = createTradeOrderItems(tradeOrderDO, priceResp.getOrder().getItems(), skus);
103 95  
104 96 // 订单创建完后的逻辑
105   - afterCreateTradeOrder(userId, createReqVO, tradeOrderDO, tradeOrderItems);
  97 + afterCreateTradeOrder(userId, createReqVO, tradeOrderDO, tradeOrderItems, spus);
106 98 // TODO @LeeYan9: 是可以思考下, 订单的营销优惠记录, 应该记录在哪里, 微信讨论起来!
107 99 return tradeOrderDO.getId();
108 100 }
109 101  
110   - private void fillPayOrderInfoFromItems(PayOrderInfoCreateReqDTO payOrderInfoCreateReqDTO,
111   - List<TradeOrderItemDO> tradeOrderItems) {
112   - // 填写 商品&应用信息
113   - payOrderInfoCreateReqDTO.setMerchantOrderId(tradeOrderProperties.getMerchantOrderId());
114   - payOrderInfoCreateReqDTO.setAppId(tradeOrderProperties.getAppId());
115   -
116   - // 填写商品信息
117   - StrBuilder subject = new StrBuilder();
118   - StrBuilder body = new StrBuilder();
119   - for (TradeOrderItemDO tradeOrderItem : tradeOrderItems) {
120   - // append subject
121   - subject.append(BLANK_PLACEHOLDER);
122   - subject.append(tradeOrderItem.getName());
123   - // append body
124   - body.append(BLANK_PLACEHOLDER);
125   - body.append(tradeOrderItem.getName());
126   - body.append(MULTIPLIER_PLACEHOLDER);
127   - body.append(tradeOrderItem.getCount());
128   - }
129   - // 设置 subject & body
130   - // TODO @LeeYan9: 可以抽象一个 StrUtils 方法; 或者看看 hutool 有没自带的哈
131   - payOrderInfoCreateReqDTO.setSubject(StrUtils.maxLength(subject.subString(1), 32));
132   - payOrderInfoCreateReqDTO.setBody(StrUtils.maxLength(body.subString(1), 128));
133   - }
134   -
135   - private void xfillItemsInfoFromSkuAndOrder(TradeOrderDO tradeOrderDO, List<TradeOrderItemDO> tradeOrderItems,
136   - Map<Long, ProductSkuRespDTO> spuInfos) {
137   - for (TradeOrderItemDO tradeOrderItem : tradeOrderItems) {
138   - // 填充订单信息
139   - tradeOrderItem.setOrderId(tradeOrderDO.getId());
140   - tradeOrderItem.setUserId(tradeOrderDO.getUserId());
141   - // 填充SKU信息
142   - ProductSkuRespDTO skuInfoRespDTO = spuInfos.get(tradeOrderItem.getSkuId());
143   - tradeOrderItem.setSpuId(skuInfoRespDTO.getSpuId());
144   - tradeOrderItem.setPicUrl(skuInfoRespDTO.getPicUrl());
145   - tradeOrderItem.setName(skuInfoRespDTO.getName());
146   - tradeOrderItem.setRefundStatus(TradeOrderItemRefundStatusEnum.NONE.getStatus());
147   - // todo
148   - List<TradeOrderItemDO.Property> property =
149   - BeanUtil.copyToList(skuInfoRespDTO.getProperties(), TradeOrderItemDO.Property.class);
150   - tradeOrderItem.setProperties(property);
151   - }
152   - }
153   -
154 102 /**
155 103 * 校验商品 SKU 是否可出售
156 104 *
... ... @@ -248,7 +196,8 @@ public class TradeOrderServiceImpl implements TradeOrderService {
248 196 * @param tradeOrderDO 交易订单
249 197 */
250 198 private void afterCreateTradeOrder(Long userId, AppTradeOrderCreateReqVO createReqVO,
251   - TradeOrderDO tradeOrderDO, List<TradeOrderItemDO> tradeOrderItemDOs) {
  199 + TradeOrderDO tradeOrderDO, List<TradeOrderItemDO> tradeOrderItemDOs,
  200 + List<ProductSpuRespDTO> spus) {
252 201 // 下单时扣减商品库存
253 202 productSkuApi.updateSkuStock(new ProductSkuUpdateStockReqDTO(TradeOrderConvert.INSTANCE.convertList(tradeOrderItemDOs)));
254 203  
... ... @@ -262,13 +211,21 @@ public class TradeOrderServiceImpl implements TradeOrderService {
262 211 .setOrderId(tradeOrderDO.getId()));
263 212 }
264 213  
265   - // 构建预支付请求参数
266   - // TODO @LeeYan9: 需要更新到订单上
267   -// PayOrderInfoCreateReqDTO payOrderCreateReqDTO = PayOrderConvert.INSTANCE.convert(tradeOrderDO);
268   -// fillPayOrderInfoFromItems(payOrderCreateReqDTO, tradeOrderItems);
269 214 // 生成预支付
  215 + createPayOrder(tradeOrderDO, tradeOrderItemDOs, spus);
270 216  
271 217 // 增加订单日志 TODO 芋艿:待实现
272 218 }
273 219  
  220 + private void createPayOrder(TradeOrderDO tradeOrderDO, List<TradeOrderItemDO> tradeOrderItemDOs,
  221 + List<ProductSpuRespDTO> spus) {
  222 + // 创建支付单,用于后续的支付
  223 + PayOrderInfoCreateReqDTO payOrderCreateReqDTO = TradeOrderConvert.INSTANCE.convert(
  224 + tradeOrderDO, tradeOrderItemDOs, spus, tradeOrderProperties);
  225 + Long payOrderId = payOrderApi.createPayOrder(payOrderCreateReqDTO);
  226 +
  227 + // 更新到交易单上
  228 + tradeOrderMapper.updateById(new TradeOrderDO().setId(tradeOrderDO.getId()).setPayOrderId(payOrderId));
  229 + }
  230 +
274 231 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderServiceTest.java
... ... @@ -25,12 +25,15 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
25 25 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
26 26 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
27 27 import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderConfig;
  28 +import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
  29 +import org.junit.jupiter.api.BeforeEach;
28 30 import org.junit.jupiter.api.Test;
29 31 import org.mockito.ArgumentMatcher;
30 32 import org.springframework.boot.test.mock.mockito.MockBean;
31 33 import org.springframework.context.annotation.Import;
32 34  
33 35 import javax.annotation.Resource;
  36 +import java.time.Duration;
34 37 import java.util.Arrays;
35 38 import java.util.List;
36 39  
... ... @@ -73,6 +76,15 @@ class TradeOrderServiceTest extends BaseDbUnitTest {
73 76 @MockBean
74 77 private CouponApi couponApi;
75 78  
  79 + @MockBean
  80 + private TradeOrderProperties tradeOrderProperties;
  81 +
  82 + @BeforeEach
  83 + public void setUp() {
  84 + when(tradeOrderProperties.getAppId()).thenReturn(888L);
  85 + when(tradeOrderProperties.getExpireTime()).thenReturn(Duration.ofDays(1));
  86 + }
  87 +
76 88 @Test
77 89 public void testCreateTradeOrder_success() {
78 90 // 准备参数
... ... @@ -92,7 +104,7 @@ class TradeOrderServiceTest extends BaseDbUnitTest {
92 104 when(productSkuApi.getSkuList(eq(asSet(1L, 2L)))).thenReturn(Arrays.asList(sku01, sku02));
93 105 // mock 方法(商品 SPU 检查)
94 106 ProductSpuRespDTO spu01 = randomPojo(ProductSpuRespDTO.class, o -> o.setId(11L)
95   - .setStatus(ProductSpuStatusEnum.ENABLE.getStatus()));
  107 + .setStatus(ProductSpuStatusEnum.ENABLE.getStatus()).setName("商品 1"));
96 108 ProductSpuRespDTO spu02 = randomPojo(ProductSpuRespDTO.class, o -> o.setId(21L)
97 109 .setStatus(ProductSpuStatusEnum.ENABLE.getStatus()));
98 110 when(productSpuApi.getSpuList(eq(asSet(11L, 21L)))).thenReturn(Arrays.asList(spu01, spu02));
... ... @@ -120,6 +132,17 @@ class TradeOrderServiceTest extends BaseDbUnitTest {
120 132 assertEquals(priceCalculateReqDTO.getItems().get(1).getCount(), 4);
121 133 return true;
122 134 }))).thenReturn(new PriceCalculateRespDTO().setOrder(priceOrder));
  135 + // mock 方法(创建支付单)
  136 + when(payOrderApi.createPayOrder(argThat(createReqDTO -> {
  137 + assertEquals(createReqDTO.getAppId(), 888L);
  138 + assertEquals(createReqDTO.getUserIp(), userIp);
  139 + assertNotNull(createReqDTO.getMerchantOrderId()); // 由于 tradeOrderId 后生成,只能校验非空
  140 + assertEquals(createReqDTO.getSubject(), "商品 1 等多件");
  141 + assertNull(createReqDTO.getBody());
  142 + assertEquals(createReqDTO.getAmount(), 80);
  143 + assertNotNull(createReqDTO.getExpireTime());
  144 + return true;
  145 + }))).thenReturn(1000L);
123 146  
124 147 // 调用方法
125 148 Long tradeOrderId = tradeOrderService.createTradeOrder(userId, userIp, reqVO);
... ... @@ -147,7 +170,7 @@ class TradeOrderServiceTest extends BaseDbUnitTest {
147 170 assertEquals(tradeOrderDO.getDiscountPrice(), 0);
148 171 assertEquals(tradeOrderDO.getAdjustPrice(), 0);
149 172 assertEquals(tradeOrderDO.getPayPrice(), 80);
150   - assertNull(tradeOrderDO.getPayOrderId());
  173 + assertEquals(tradeOrderDO.getPayOrderId(), 1000L);
151 174 assertNull(tradeOrderDO.getPayChannel());
152 175 assertNull(tradeOrderDO.getDeliveryTemplateId());
153 176 assertNull(tradeOrderDO.getExpressNo());
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderInfoCreateReqDTO.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.pay.api.order;
3 3 import lombok.Data;
4 4 import org.hibernate.validator.constraints.Length;
5 5  
6   -import javax.validation.constraints.DecimalMin;
  6 +import javax.validation.constraints.Min;
7 7 import javax.validation.constraints.NotEmpty;
8 8 import javax.validation.constraints.NotNull;
9 9 import java.io.Serializable;
... ... @@ -45,7 +45,7 @@ public class PayOrderInfoCreateReqDTO implements Serializable {
45 45 /**
46 46 * 商品描述
47 47 */
48   - @NotEmpty(message = "商品描述信息不能为空")
  48 +// @NotEmpty(message = "商品描述信息不能为空") // 允许空
49 49 @Length(max = 128, message = "商品描述信息长度不能超过128")
50 50 private String body;
51 51  
... ... @@ -55,8 +55,7 @@ public class PayOrderInfoCreateReqDTO implements Serializable {
55 55 * 支付金额,单位:分
56 56 */
57 57 @NotNull(message = "支付金额不能为空")
58   - // TODO @LeeYan9: 是不是 @Min 注解呀, 是 Integer 哈
59   - @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
  58 + @Min(value = 1, message = "支付金额必须大于零")
60 59 private Integer amount;
61 60  
62 61 /**
... ... @@ -65,4 +64,4 @@ public class PayOrderInfoCreateReqDTO implements Serializable {
65 64 @NotNull(message = "支付过期时间不能为空")
66 65 private Date expireTime;
67 66  
68   -}
69 67 \ No newline at end of file
  68 +}
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -97,8 +97,8 @@ yudao:
97 97 enable: true # 验证码的开关,默认为 true;注意,优先读取数据库 infra_config 的 yudao.captcha.enable,所以请从数据库修改,可能需要重启项目
98 98 trade:
99 99 order:
100   - app-id: 1
101   - merchant-order-id: 1
  100 + app-id: 1 # 商户编号
  101 + expire-time: 2h # 支付的过期时间
102 102 codegen:
103 103 base-package: ${yudao.info.base-package}
104 104 db-schemas: ${spring.datasource.dynamic.datasource.master.name}
... ...