!266 接口实现-交易订单 单元测试

Merge pull request !266 from LeeYan9/ly_uniapp
This commit is contained in:
芋道源码 2022-09-30 08:56:13 +00:00 committed by Gitee
commit 952e6aa4a8
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
22 changed files with 502 additions and 68 deletions

View File

@ -1,4 +1,5 @@
/**todo cancelType 设置默认值 0?*/ /**todo cancelType 设置默认值 0?*/
DROP TABLE IF EXISTS `trade_order`;
CREATE TABLE `trade_order` CREATE TABLE `trade_order`
( (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
@ -10,9 +11,10 @@ CREATE TABLE `trade_order`
`user_remark` varchar(200) DEFAULT NULL COMMENT '用户备注', `user_remark` varchar(200) DEFAULT NULL COMMENT '用户备注',
`status` int NOT NULL DEFAULT '0' COMMENT '订单状态[0:待付款 1:待发货 2:待收货 3:已完成 4:已关闭]', `status` int NOT NULL DEFAULT '0' COMMENT '订单状态[0:待付款 1:待发货 2:待收货 3:已完成 4:已关闭]',
`product_count` int NOT NULL COMMENT '购买的商品数量', `product_count` int NOT NULL COMMENT '购买的商品数量',
`cancel_type` int NOT NULL COMMENT '取消类型[10:超时未支付 20:退款关闭 30:买家取消 40:已通过货到付款交易]', `cancel_type` int DEFAULT NULL COMMENT '取消类型[10:超时未支付 20:退款关闭 30:买家取消 40:已通过货到付款交易]',
`remark` varchar(200) DEFAULT NULL COMMENT '商家备注', `remark` varchar(200) DEFAULT NULL COMMENT '商家备注',
`payed` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已支付[0:未支付 1:已经支付过]', `payed` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已支付[0:未支付 1:已经支付过]',
`pay_time` datetime DEFAULT NULL COMMENT '订单支付时间',
`finish_time` datetime DEFAULT NULL COMMENT '订单完成时间', `finish_time` datetime DEFAULT NULL COMMENT '订单完成时间',
`cancel_time` datetime DEFAULT NULL COMMENT '订单取消时间', `cancel_time` datetime DEFAULT NULL COMMENT '订单取消时间',
`sku_original_price` int NOT NULL DEFAULT '0' COMMENT '商品原价单位', `sku_original_price` int NOT NULL DEFAULT '0' COMMENT '商品原价单位',
@ -20,11 +22,11 @@ CREATE TABLE `trade_order`
`order_promotion_price` int NOT NULL DEFAULT '0' COMMENT '订单优惠单位', `order_promotion_price` int NOT NULL DEFAULT '0' COMMENT '订单优惠单位',
`delivery_price` int NOT NULL DEFAULT '0' COMMENT '运费金额单位', `delivery_price` int NOT NULL DEFAULT '0' COMMENT '运费金额单位',
`pay_price` int NOT NULL DEFAULT '0' COMMENT '应付金额单位', `pay_price` int NOT NULL DEFAULT '0' COMMENT '应付金额单位',
`pay_order_id` int NOT NULL COMMENT '支付订单编号', `pay_order_id` int DEFAULT NULL COMMENT '支付订单编号',
`pay_channel` int NOT NULL COMMENT '支付成功的支付渠道', `pay_channel` int DEFAULT NULL COMMENT '支付成功的支付渠道',
`delivery_type` int NOT NULL DEFAULT '1' COMMENT '配送方式:[1:快递发货 2:自提]', `delivery_type` int DEFAULT NULL DEFAULT '1' COMMENT '配送方式:[1:快递发货 2:自提]',
`actual_delivery_type` int NOT NULL DEFAULT '1' COMMENT '实际的配送方式:[1:快递发货 2:自提]', `actual_delivery_type` int DEFAULT NULL DEFAULT '1' COMMENT '实际的配送方式:[1:快递发货 2:自提]',
`delivery_templateid` int DEFAULT NULL COMMENT '配置模板的编号', `delivery_template_id` int DEFAULT NULL COMMENT '配置模板的编号',
`express_no` int DEFAULT NULL COMMENT '物流公司单号', `express_no` int DEFAULT NULL COMMENT '物流公司单号',
`delivery_status` bit(1) NOT NULL DEFAULT b'0' COMMENT '发货状态[0:未发货 1:已发货]', `delivery_status` bit(1) NOT NULL DEFAULT b'0' COMMENT '发货状态[0:未发货 1:已发货]',
`delivery_time` datetime DEFAULT NULL COMMENT '发货时间', `delivery_time` datetime DEFAULT NULL COMMENT '发货时间',

View File

@ -1,43 +1,43 @@
package cn.iocoder.yudao.module.product.api.sku; package cn.iocoder.yudao.module.product.api.sku;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO; import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO;
import cn.iocoder.yudao.module.product.convert.sku.ProductSkuConvert; import cn.iocoder.yudao.module.product.convert.sku.ProductSkuConvert;
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO; import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
import cn.iocoder.yudao.module.product.service.sku.ProductSkuService; import cn.iocoder.yudao.module.product.dal.mysql.sku.ProductSkuMapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* 商品 SKU API 实现类 * @author LeeYan9
* * @since 2022-09-06
* @author 芋道源码
* @since 2022-08-26
*/ */
@Service @Service
@Validated
public class ProductSkuApiImpl implements ProductSkuApi { public class ProductSkuApiImpl implements ProductSkuApi {
@Resource @Resource
private ProductSkuService productSkuService; private ProductSkuMapper productSkuMapper;
@Override @Override
public ProductSkuRespDTO getSku(Long id) { public List<SkuInfoRespDTO> getSkusByIds(Collection<Long> skuIds) {
ProductSkuDO skuDO = productSkuService.getSku(id); if (CollectionUtils.isAnyEmpty(skuIds)) {
return ProductSkuConvert.INSTANCE.convert02(skuDO); return Collections.emptyList();
} }
List<ProductSkuDO> productSkuDOList = productSkuMapper.selectBatchIds(skuIds);
@Override return ProductSkuConvert.INSTANCE.convertList03(productSkuDOList);
public List<ProductSkuRespDTO> getSkuList(Collection<Long> ids) {
List<ProductSkuDO> list = productSkuService.getSkuList(ids);
return ProductSkuConvert.INSTANCE.convertList02(list);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void decrementStockBatch(SkuDecrementStockBatchReqDTO batchReqDTO) { public void decrementStockBatch(SkuDecrementStockBatchReqDTO batchReqDTO) {
productSkuMapper.decrementStockBatch(batchReqDTO.getItems());
} }
} }

View File

@ -1,23 +1,35 @@
package cn.iocoder.yudao.module.product.api.spu; package cn.iocoder.yudao.module.product.api.spu;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.product.api.spu.dto.SpuInfoRespDTO; import cn.iocoder.yudao.module.product.api.spu.dto.SpuInfoRespDTO;
import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
import cn.iocoder.yudao.module.product.dal.mysql.spu.ProductSpuMapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* 商品 SPU API 实现类
*
* @author LeeYan9 * @author LeeYan9
* @since 2022-08-26 * @since 2022-09-06
*/ */
@Service @Service
@Validated
public class ProductSpuApiImpl implements ProductSpuApi { public class ProductSpuApiImpl implements ProductSpuApi {
@Resource
private ProductSpuMapper productSpuMapper;
@Override @Override
public List<SpuInfoRespDTO> getSpuList(Collection<Long> spuIds) { public List<SpuInfoRespDTO> getSpuList(Collection<Long> spuIds) {
return null; if (CollectionUtils.isAnyEmpty(spuIds)) {
return Collections.emptyList();
}
List<ProductSpuDO> productSpuDOList = productSpuMapper.selectBatchIds(spuIds);
return ProductSpuConvert.INSTANCE.convertList2(productSpuDOList);
} }
} }

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.product.convert.sku; package cn.iocoder.yudao.module.product.convert.sku;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.api.sku.dto.SkuInfoRespDTO;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO;
import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuRespVO; import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuRespVO;
@ -34,4 +36,7 @@ public interface ProductSkuConvert {
List<ProductSpuDetailRespVO.Sku> convertList03(List<ProductSkuDO> list); List<ProductSpuDetailRespVO.Sku> convertList03(List<ProductSkuDO> list);
List<SkuInfoRespDTO> convertList03(List<ProductSkuDO> list);
} }

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.product.convert.spu; package cn.iocoder.yudao.module.product.convert.spu;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.api.spu.dto.SpuInfoRespDTO;
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*; import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*;
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppSpuPageReqVO; import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppSpuPageReqVO;
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppSpuPageRespVO; import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppSpuPageRespVO;
@ -34,4 +35,7 @@ public interface ProductSpuConvert {
AppSpuPageRespVO convertAppResp(ProductSpuDO list); AppSpuPageRespVO convertAppResp(ProductSpuDO list);
List<SpuInfoRespDTO> convertList2(List<ProductSpuDO> list);
} }

View File

@ -2,7 +2,10 @@ package cn.iocoder.yudao.module.product.dal.mysql.sku;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO;
import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuPageReqVO;
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO; import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
@ -31,4 +34,16 @@ public interface ProductSkuMapper extends BaseMapperX<ProductSkuDO> {
delete(lambdaQueryWrapperX); delete(lambdaQueryWrapperX);
} }
default void decrementStockBatch(List<SkuDecrementStockBatchReqDTO.Item> items) {
for (SkuDecrementStockBatchReqDTO.Item item : items) {
// 扣减库存 cas 逻辑
LambdaUpdateWrapper<ProductSkuDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<ProductSkuDO>()
.setSql(" stock = stock-" + item.getCount())
.eq(ProductSkuDO::getSpuId, item.getProductId())
.eq(ProductSkuDO::getId, item.getSkuId())
.ge(ProductSkuDO::getStock, item.getCount());
// 执行
this.update(null, lambdaUpdateWrapper);
}
}
} }

View File

@ -57,9 +57,16 @@
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId> <artifactId>yudao-spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId> <artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency> </dependency>
<!-- DB 相关 --> <!-- DB 相关 -->

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreate
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
/** /**
@ -16,6 +17,12 @@ public interface TradeOrderConvert {
TradeOrderConvert INSTANCE = Mappers.getMapper(TradeOrderConvert.class); TradeOrderConvert INSTANCE = Mappers.getMapper(TradeOrderConvert.class);
@Mapping(source = "order.couponId", target = "couponId")
TradeOrderDO convert(AppTradeOrderCreateReqVO createReqVO, PriceCalculateRespDTO.Order order); @Mappings({
@Mapping(source = "order.couponId", target = "couponId"),
@Mapping(target = "remark", ignore = true),
@Mapping(source = "createVO.remark", target = "userRemark"),
@Mapping(source = "createVO.addressId", target = "receiverAreaId")
})
TradeOrderDO convert(AppTradeOrderCreateReqVO createVO, PriceCalculateRespDTO.Order order);
} }

View File

@ -1,11 +1,8 @@
package cn.iocoder.yudao.module.trade.convert.order; package cn.iocoder.yudao.module.trade.convert.order;
import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO; import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import java.util.List; import java.util.List;
@ -20,17 +17,8 @@ public interface TradeOrderItemConvert {
TradeOrderItemConvert INSTANCE = Mappers.getMapper(TradeOrderItemConvert.class); TradeOrderItemConvert INSTANCE = Mappers.getMapper(TradeOrderItemConvert.class);
/** /**
*
* @param tradeOrder 交易订单
* @param items sku列表价格 * @param items sku列表价格
* @return 订单项 * @return 订单项
*/ */
@Mappings({ List<TradeOrderItemDO> convertList(List<PriceCalculateRespDTO.OrderItem> items);
@Mapping(source = "tradeOrder.userId", target = "userId"),
@Mapping(source = "tradeOrder.orderId", target = "orderId")
})
default List<TradeOrderItemDO> convertList(TradeOrderDO tradeOrder, List<PriceCalculateRespDTO.OrderItem> items) {
// TODO @Com: Mapstruct 生成会报错
throw new UnsupportedOperationException("无法实现");
}
} }

View File

@ -1,17 +1,33 @@
package cn.iocoder.yudao.module.trade.convert.pay; package cn.iocoder.yudao.module.trade.convert.pay;
import cn.iocoder.yudao.module.pay.api.order.PayOrderDataCreateReqDTO; import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.pay.api.order.PayOrderInfoCreateReqDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import java.util.Date;
/** /**
* @author LeeYan9 * @author LeeYan9
* @since 2022-08-26 * @since 2022-08-26
*/ */
@Mapper
public interface PayOrderConvert { public interface PayOrderConvert {
PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class); PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class);
@Mappings({
@Mapping(source = "payPrice", target = "amount"),
@Mapping(target = "expireTime", source = "cancelTime" , qualifiedByName = "convertCreateTimeToPayExpireTime")
})
PayOrderInfoCreateReqDTO convert(TradeOrderDO tradeOrderDO);
PayOrderDataCreateReqDTO convert(TradeOrderDO tradeOrderDO); @Named("convertCreateTimeToPayExpireTime")
default Date convertCreateTimeToPayExpireTime(Date cancelTime) {
return DateUtil.offsetMinute(new Date(), 30);
}
} }

View File

@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.trade.framework.order.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author LeeYan9
* @since 2022-09-15
*/
@Configuration
@EnableConfigurationProperties(TradeOrderProperties.class)
public class TradeOrderConfig {
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.trade.framework.order.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
/**
* @author LeeYan9
* @since 2022-09-15
*/
@ConfigurationProperties(prefix = "yudao.trade.order")
@Data
@Validated
public class TradeOrderProperties {
/**
* 商户订单编号
*/
@NotNull(message = "商户订单编号不能为空")
private String merchantOrderId;
/**
* 应用编号
*/
@NotNull(message = "应用编号不能为空")
private Long appId;
}

View File

@ -1,14 +1,18 @@
package cn.iocoder.yudao.module.trade.service.order; package cn.iocoder.yudao.module.trade.service.order;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.module.market.api.price.PriceApi; import cn.iocoder.yudao.module.market.api.price.PriceApi;
import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO; import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO; import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
import cn.iocoder.yudao.module.pay.api.order.PayOrderDataCreateReqDTO; import cn.iocoder.yudao.module.pay.api.order.PayOrderInfoCreateReqDTO;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO; import cn.iocoder.yudao.module.product.api.sku.dto.SkuDecrementStockBatchReqDTO;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
@ -27,10 +31,15 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper; import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
import cn.iocoder.yudao.module.trade.dal.mysql.orderitem.TradeOrderItemMapper; import cn.iocoder.yudao.module.trade.dal.mysql.orderitem.TradeOrderItemMapper;
import cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants;
import lombok.RequiredArgsConstructor; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemRefundStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -40,20 +49,31 @@ import java.util.Objects;
* @since 2022-08-26 * @since 2022-08-26
*/ */
@Service @Service
@RequiredArgsConstructor
public class TradeOrderServiceImpl implements TradeOrderService { public class TradeOrderServiceImpl implements TradeOrderService {
private final TradeOrderMapper tradeOrderMapper; @Resource
private TradeOrderMapper tradeOrderMapper;
private final TradeOrderItemMapper tradeOrderItemMapper; @Resource
private TradeOrderItemMapper tradeOrderItemMapper;
private final PriceApi priceApi; @Resource
private PriceApi priceApi;
private final ProductSkuApi productSkuApi; @Resource
private ProductSkuApi productSkuApi;
private final ProductSpuApi productSpuApi; @Resource
private ProductSpuApi productSpuApi;
private final PayOrderApi payOrderApi; @Resource
private PayOrderApi payOrderApi;
@Resource
private TradeOrderProperties tradeOrderProperties;
private static final String BLANK_PLACEHOLDER = " ";
private static final String MULTIPLIER_PLACEHOLDER = "x";
@Override @Override
@ -61,46 +81,92 @@ public class TradeOrderServiceImpl implements TradeOrderService {
public Long createTradeOrder(Long loginUserId, String clientIp, AppTradeOrderCreateReqVO createReqVO) { public Long createTradeOrder(Long loginUserId, String clientIp, AppTradeOrderCreateReqVO createReqVO) {
List<Item> items = createReqVO.getItems(); List<Item> items = createReqVO.getItems();
// 商品SKU检查 sku可售状态,库存 // 商品SKU检查 sku可售状态,库存
List<ProductSkuRespDTO> skuInfos = productSkuApi.getSkuList(CollectionUtils.convertSet(items, Item::getSkuId)); List<ProductSkuRespDTO> skuInfos = productSkuApi.getSkuList(CollectionUtils.convertSet(items, Item::getSkuId));
Map<Long, ProductSkuRespDTO> skuInfoMap = CollectionUtils.convertMap(skuInfos, ProductSkuRespDTO::getId); Map<Long, ProductSkuRespDTO> skuInfoMap = CollectionUtils.convertMap(skuInfos, ProductSkuRespDTO::getId);
checkSaleableAndStockFromSpu(skuInfoMap, items); checkSaleableAndStockFromSpu(skuInfoMap, items);
// 商品SPU检查 sku可售状态,库存 // 商品SPU检查 sku可售状态,库存
List<SpuInfoRespDTO> spuInfos = productSpuApi.getSpuList(CollectionUtils.convertSet(skuInfos, ProductSkuRespDTO::getSpuId)); List<SpuInfoRespDTO> spuInfos = productSpuApi.getSpuList(CollectionUtils.convertSet(skuInfos, ProductSkuRespDTO::getSpuId));
checkSaleableFromSpu(spuInfos); checkSaleableFromSpu(spuInfos);
// 价格计算 // 价格计算
PriceCalculateReqDTO priceCalculateReqDTO = PriceConvert.INSTANCE.convert(createReqVO, loginUserId); PriceCalculateReqDTO priceCalculateReqDTO = PriceConvert.INSTANCE.convert(createReqVO, loginUserId);
PriceCalculateRespDTO priceResp = priceApi.calculatePrice(priceCalculateReqDTO); PriceCalculateRespDTO priceResp = priceApi.calculatePrice(priceCalculateReqDTO);
// 订单信息记录 // 订单信息记录
TradeOrderDO tradeOrderDO = TradeOrderConvert.INSTANCE.convert(createReqVO, priceResp.getOrder()); TradeOrderDO tradeOrderDO = TradeOrderConvert.INSTANCE.convert(createReqVO, priceResp.getOrder());
fillTradeOrderInfoFromReqInfo(tradeOrderDO,createReqVO,loginUserId, clientIp);
tradeOrderMapper.insert(tradeOrderDO); tradeOrderMapper.insert(tradeOrderDO);
// 订单项信息记录 // 订单项信息记录
List<TradeOrderItemDO> tradeOrderItems = TradeOrderItemConvert.INSTANCE.convertList(tradeOrderDO, priceResp.getOrder().getItems()); List<TradeOrderItemDO> tradeOrderItems = TradeOrderItemConvert.INSTANCE.convertList(priceResp.getOrder().getItems());
//-填充订单项-SKU信息 //-填充订单项-SKU信息
fillItemsInfoFromSku(tradeOrderItems, skuInfoMap); fillItemsInfoFromSkuAndOrder(tradeOrderDO, tradeOrderItems, skuInfoMap);
tradeOrderItemMapper.insertBatch(tradeOrderItems); tradeOrderItemMapper.insertBatch(tradeOrderItems);
// 库存扣减 // 库存扣减
List<SkuDecrementStockBatchReqDTO.Item> skuDecrementStockItems = ProductSkuConvert.INSTANCE.convert(tradeOrderItems); List<SkuDecrementStockBatchReqDTO.Item> skuDecrementStockItems = ProductSkuConvert.INSTANCE.convert(tradeOrderItems);
productSkuApi.decrementStockBatch(SkuDecrementStockBatchReqDTO.of(skuDecrementStockItems)); productSkuApi.decrementStockBatch(SkuDecrementStockBatchReqDTO.of(skuDecrementStockItems));
// 生成预支付
PayOrderDataCreateReqDTO payOrderCreateReqDTO = PayOrderConvert.INSTANCE.convert(tradeOrderDO); // 构建预支付请求参数
PayOrderInfoCreateReqDTO payOrderCreateReqDTO = PayOrderConvert.INSTANCE.convert(tradeOrderDO);
fillPayOrderInfoFromItems(payOrderCreateReqDTO, tradeOrderItems);
// 生成预支付
return payOrderApi.createPayOrder(payOrderCreateReqDTO); return payOrderApi.createPayOrder(payOrderCreateReqDTO);
} }
private void fillItemsInfoFromSku(List<TradeOrderItemDO> tradeOrderItems, private void fillTradeOrderInfoFromReqInfo(TradeOrderDO tradeOrderDO, AppTradeOrderCreateReqVO createReqVO,
Map<Long, ProductSkuRespDTO> spuInfos) { Long loginUserId, String clientIp) {
tradeOrderDO.setUserId(loginUserId);
tradeOrderDO.setUserIp(clientIp);
tradeOrderDO.setSn(IdUtil.getSnowflakeNextId() + "");
tradeOrderDO.setStatus(TradeOrderStatusEnum.WAITING_PAYMENT.getStatus());
tradeOrderDO.setType(TradeOrderTypeEnum.NORMAL.getType());
tradeOrderDO.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
tradeOrderDO.setProductCount(CollectionUtils.getSumValue(createReqVO.getItems(), Item::getCount,Integer::sum));
// todo 地址&用户信息解析
// todo 数据来源?
tradeOrderDO.setTerminal(TerminalEnum.H5.getTerminal());
}
private void fillPayOrderInfoFromItems(PayOrderInfoCreateReqDTO payOrderInfoCreateReqDTO,
List<TradeOrderItemDO> tradeOrderItems) {
// 填写 商品&应用信息
payOrderInfoCreateReqDTO.setMerchantOrderId(tradeOrderProperties.getMerchantOrderId());
payOrderInfoCreateReqDTO.setAppId(tradeOrderProperties.getAppId());
// 填写商品信息
StrBuilder subject = new StrBuilder();
StrBuilder body = new StrBuilder();
for (TradeOrderItemDO tradeOrderItem : tradeOrderItems) { for (TradeOrderItemDO tradeOrderItem : tradeOrderItems) {
// append subject
subject.append(BLANK_PLACEHOLDER);
subject.append(tradeOrderItem.getName());
// append body
body.append(BLANK_PLACEHOLDER);
body.append(tradeOrderItem.getName());
body.append(MULTIPLIER_PLACEHOLDER);
body.append(tradeOrderItem.getCount());
}
// 设置 subject & body
payOrderInfoCreateReqDTO.setSubject(StrUtils.maxLength(subject.subString(1), 32));
payOrderInfoCreateReqDTO.setBody(StrUtils.maxLength(body.subString(1), 128));
}
private void fillItemsInfoFromSkuAndOrder(TradeOrderDO tradeOrderDO, List<TradeOrderItemDO> tradeOrderItems,
Map<Long, ProductSkuRespDTO> spuInfos) {
for (TradeOrderItemDO tradeOrderItem : tradeOrderItems) {
// 填充订单信息
tradeOrderItem.setOrderId(tradeOrderDO.getId());
tradeOrderItem.setUserId(tradeOrderDO.getUserId());
// 填充SKU信息 // 填充SKU信息
ProductSkuRespDTO skuInfoRespDTO = spuInfos.get(tradeOrderItem.getSkuId()); ProductSkuRespDTO skuInfoRespDTO = spuInfos.get(tradeOrderItem.getSkuId());
tradeOrderItem.setSpuId(skuInfoRespDTO.getSpuId()); tradeOrderItem.setSpuId(skuInfoRespDTO.getSpuId());
tradeOrderItem.setPicUrl(skuInfoRespDTO.getPicUrl()); tradeOrderItem.setPicUrl(skuInfoRespDTO.getPicUrl());
tradeOrderItem.setName(skuInfoRespDTO.getName()); tradeOrderItem.setName(skuInfoRespDTO.getName());
tradeOrderItem.setRefundStatus(TradeOrderItemRefundStatusEnum.NONE.getStatus());
// todo // todo
List<TradeOrderItemDO.Property> property = List<TradeOrderItemDO.Property> property =
BeanUtil.copyToList(skuInfoRespDTO.getProperties(), TradeOrderItemDO.Property.class); BeanUtil.copyToList(skuInfoRespDTO.getProperties(), TradeOrderItemDO.Property.class);

View File

@ -0,0 +1,109 @@
package cn.iocoder.yudao.module.trade.service.order;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.market.api.price.PriceApi;
import cn.iocoder.yudao.module.market.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.SpuInfoRespDTO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
import cn.iocoder.yudao.module.trade.dal.mysql.orderitem.TradeOrderItemMapper;
import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderConfig;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.util.Collections;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomInteger;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
* @author LeeYan9
* @since 2022-09-07
*/
@Import({TradeOrderServiceImpl.class, TradeOrderConfig.class})
class TradeOrderServiceTest extends BaseDbUnitTest {
@Resource
TradeOrderService tradeOrderService;
@Resource
TradeOrderMapper tradeOrderMapper;
@Resource
TradeOrderItemMapper tradeOrderItemMapper;
@MockBean
ProductSpuApi productSpuApi;
@MockBean
ProductSkuApi productSkuApi;
@MockBean
PriceApi priceApi;
@MockBean
private PayOrderApi payOrderApi;
@Test
void testCreateTradeOrder_success() {
// mock 商品SPU数据
SpuInfoRespDTO spuInfoRespDTO = randomPojo(SpuInfoRespDTO.class, spuInfo -> {
spuInfo.setId(1L);
spuInfo.setStatus(CommonStatusEnum.ENABLE.getStatus());
});
when(productSpuApi.getSpuList(Collections.singleton(1L))).thenReturn(Lists.newArrayList(spuInfoRespDTO));
// mock 商品SkU数据
ProductSkuRespDTO skuInfoRespDTO = randomPojo(ProductSkuRespDTO.class, skuInfo -> {
skuInfo.setId(1L);
skuInfo.setStatus(CommonStatusEnum.ENABLE.getStatus());
skuInfo.setStock(randomInteger());
skuInfo.setSpuId(1L);
});
when(productSkuApi.getSkuList(Collections.singleton(1L))).thenReturn(Lists.newArrayList(skuInfoRespDTO));
// mock 价格信息
PriceCalculateRespDTO calculateRespDTO = randomPojo(PriceCalculateRespDTO.class, priceCalculateRespDTO -> {
PriceCalculateRespDTO.OrderItem item = priceCalculateRespDTO.getOrder().getItems().get(0);
item.setSkuId(1L);
item.setCount(2);
priceCalculateRespDTO.getOrder().setItems(Collections.singletonList(item));
});
when(priceApi.calculatePrice(any())).thenReturn(calculateRespDTO);
//mock 支付订单信息
when(payOrderApi.createPayOrder(any())).thenReturn(1L);
// 准备请求数据
AppTradeOrderCreateReqVO tradeOrderCreateReqVO = randomPojo(AppTradeOrderCreateReqVO.class, reqVO -> {
AppTradeOrderCreateReqVO.Item item = randomPojo(AppTradeOrderCreateReqVO.Item.class, o -> {
o.setSkuId(1L);
o.setCount(2);
});
reqVO.setItems(Collections.singletonList(item));
});
// 创建交易订单,支付订单记录
Long payOrderId = tradeOrderService.createTradeOrder(1L, "127.0.0.1", tradeOrderCreateReqVO);
//断言交易订单
TradeOrderDO tradeOrderDO = tradeOrderMapper.selectOne(TradeOrderDO::getUserId, 1L);
assertNotNull(tradeOrderDO);
//价格&用户
assertEquals(calculateRespDTO.getOrder().getPayPrice(), tradeOrderDO.getPayPrice());
assertEquals(1L, tradeOrderDO.getUserId());
//断言交易订单项
TradeOrderItemDO tradeOrderItemDO = tradeOrderItemMapper.selectOne(TradeOrderItemDO::getOrderId, tradeOrderDO.getId());
assertNotNull(tradeOrderDO);
//商品&用户
assertEquals(skuInfoRespDTO.getId(), tradeOrderItemDO.getSkuId());
assertEquals(1L, tradeOrderItemDO.getUserId());
//价格
assertEquals(calculateRespDTO.getOrder().getItems().get(0).getPresentPrice(), tradeOrderItemDO.getPresentPrice());
}
}

View File

@ -0,0 +1,53 @@
spring:
main:
lazy-initialization: true # 开启懒加载,加快速度
banner-mode: off # 单元测试,禁用 Banner
--- #################### 数据库相关配置 ####################
spring:
# 数据源配置项
datasource:
name: ruoyi-vue-pro
url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式DATABASE_TO_UPPER 配置表和字段使用小写
driver-class-name: org.h2.Driver
username: sa
password:
druid:
async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
initial-size: 1 # 单元测试,配置为 1提升启动速度
sql:
init:
schema-locations: classpath:/sql/create_tables.sql
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
redis:
host: 127.0.0.1 # 地址
port: 16379 # 端口(单元测试,使用 16379 端口)
database: 0 # 数据库索引
mybatis:
lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
--- #################### 定时任务相关配置 ####################
--- #################### 配置中心相关配置 ####################
--- #################### 服务保障相关配置 ####################
# Lock4j 配置项(单元测试,禁用 Lock4j
# Resilience4j 配置项
--- #################### 监控相关配置 ####################
--- #################### 芋道相关配置 ####################
# 芋道配置项,设置当前项目所有自定义的配置
yudao:
info:
base-package: cn.iocoder.yudao.module
trade:
order:
app-id: 1
merchant-order-id: 1

View File

@ -0,0 +1,4 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
</configuration>

View File

@ -0,0 +1,2 @@
DELETE FROM trade_order;
DELETE FROM trade_order_item;

View File

@ -0,0 +1,78 @@
/**todo cancelType 设置默认值 0?*/
CREATE TABLE IF NOT EXISTS `trade_order`
(
`id` number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
`sn` varchar(32) NOT NULL,
`type` int NOT NULL,
`terminal` int NOT NULL,
`user_id` bigint unsigned NOT NULL,
`user_ip` varchar(30) NOT NULL,
`user_remark` varchar(200),
`status` int NOT NULL,
`product_count` int NOT NULL,
`cancel_type` int DEFAULT NULL,
`remark` varchar(200),
`payed` bit(1) NOT NULL DEFAULT FALSE,
`pay_time` datetime DEFAULT NULL,
`finish_time` datetime DEFAULT NULL,
`cancel_time` datetime DEFAULT NULL,
`sku_original_price` int NOT NULL DEFAULT '0',
`sku_promotion_price` int NOT NULL DEFAULT '0',
`order_promotion_price` int NOT NULL DEFAULT '0',
`delivery_price` int NOT NULL DEFAULT '0',
`pay_price` int DEFAULT '0',
`pay_order_id` int DEFAULT NULL,
`pay_channel` int DEFAULT NULL,
`delivery_type` int NOT NULL DEFAULT '1',
`actual_delivery_type` int NOT NULL DEFAULT '1',
`delivery_template_id` int DEFAULT NULL,
`express_no` int DEFAULT NULL,
`delivery_status` bit(1) NOT NULL DEFAULT FALSE,
`delivery_time` datetime DEFAULT NULL,
`receive_time` datetime DEFAULT NULL,
`receiver_name` varchar(20) DEFAULT NULL,
`receiver_mobile` varchar(20) DEFAULT NULL,
`receiver_area_id` int DEFAULT NULL,
`receiver_post_code` int DEFAULT NULL,
`receiver_detail_address` varchar(255) DEFAULT NULL,
`refund_status` int NOT NULL DEFAULT '0',
`refund_price` int NOT NULL DEFAULT '0',
`coupon_id` bigint unsigned DEFAULT NULL,
`creator` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS `trade_order_item`
(
`id` number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
`user_id` bigint unsigned NOT NULL,
`order_id` bigint unsigned NOT NULL,
`spu_id` bigint unsigned NOT NULL,
`sku_id` bigint unsigned NOT NULL,
`properties` json DEFAULT NULL,
`name` varchar(128) DEFAULT NULL,
`pic_url` varchar(200) DEFAULT NULL,
`count` int NOT NULL,
`commented` bit(1) DEFAULT NULL,
`original_price` int NOT NULL DEFAULT '0',
`total_original_price` int NOT NULL DEFAULT '0',
`total_promotion_price` int NOT NULL DEFAULT '0',
`present_price` int NOT NULL DEFAULT '0',
`total_present_price` int NOT NULL DEFAULT '0',
`total_pay_price` int NOT NULL DEFAULT '0',
`refund_status` int NOT NULL DEFAULT '0',
`refund_total` int NOT NULL DEFAULT '0',
`creator` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) DEFAULT FALSE,
PRIMARY KEY ("id")
);

View File

@ -15,6 +15,6 @@ public interface PayOrderApi {
* @param reqDTO 创建请求 * @param reqDTO 创建请求
* @return 支付单编号 * @return 支付单编号
*/ */
Long createPayOrder(@Valid PayOrderDataCreateReqDTO reqDTO); Long createPayOrder(@Valid PayOrderInfoCreateReqDTO reqDTO);
} }

View File

@ -14,7 +14,7 @@ import java.util.Date;
* @author LeeYan9 * @author LeeYan9
*/ */
@Data @Data
public class PayOrderDataCreateReqDTO implements Serializable { public class PayOrderInfoCreateReqDTO implements Serializable {
/** /**
* 应用编号 * 应用编号

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.pay.api.order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author LeeYan9
* @since 2022-09-06
*/
@Service
public class PayOrderApiImpl implements PayOrderApi {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createPayOrder(PayOrderInfoCreateReqDTO reqDTO) {
return null;
}
}

View File

@ -95,6 +95,10 @@ yudao:
base-package: ${yudao.info.base-package} base-package: ${yudao.info.base-package}
captcha: captcha:
enable: true # 验证码的开关,默认为 true注意优先读取数据库 infra_config 的 yudao.captcha.enable所以请从数据库修改可能需要重启项目 enable: true # 验证码的开关,默认为 true注意优先读取数据库 infra_config 的 yudao.captcha.enable所以请从数据库修改可能需要重启项目
trade:
order:
app-id: 1
merchant-order-id: 1
codegen: codegen:
base-package: ${yudao.info.base-package} base-package: ${yudao.info.base-package}
db-schemas: ${spring.datasource.dynamic.datasource.master.name} db-schemas: ${spring.datasource.dynamic.datasource.master.name}