trade:创建订单接口的后端实现的单元测试 50%

This commit is contained in:
YunaiV
2022-11-09 01:03:26 +08:00
parent 01d10b518c
commit bc2aa78f70
12 changed files with 247 additions and 233 deletions

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.trade.convert.order;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
@@ -33,7 +34,7 @@ public interface TradeOrderConvert {
@Mapping(source = "address.postCode", target = "receiverPostCode"),
@Mapping(source = "address.detailAddress", target = "receiverDetailAddress"),
})
TradeOrderDO convert(Long userId, String clientIp, AppTradeOrderCreateReqVO createReqVO,
TradeOrderDO convert(Long userId, String userIp, AppTradeOrderCreateReqVO createReqVO,
PriceCalculateRespDTO.Order order, AddressRespDTO address);
@Mappings({
@@ -49,10 +50,13 @@ public interface TradeOrderConvert {
TradeOrderItemDO tradeOrderItemDO = convert(orderItem, skuMap.get(orderItem.getSkuId()));
tradeOrderItemDO.setOrderId(tradeOrderDO.getId());
tradeOrderItemDO.setUserId(tradeOrderDO.getUserId());
tradeOrderItemDO.setRefundStatus(TradeOrderItemRefundStatusEnum.NONE.getStatus());
tradeOrderItemDO.setRefundStatus(TradeOrderItemRefundStatusEnum.NONE.getStatus()).setRefundTotal(0); // 退款信息
// tradeOrderItemDO.setCommented(false);
return tradeOrderItemDO;
});
}
@Mapping(source = "userId" , target = "userId")
PriceCalculateReqDTO convert(AppTradeOrderCreateReqVO createReqVO, Long userId);
}

View File

@@ -1,20 +0,0 @@
package cn.iocoder.yudao.module.trade.convert.price;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@Mapper
public interface PriceConvert {
PriceConvert INSTANCE = Mappers.getMapper(PriceConvert.class);
@Mappings(
@Mapping(source = "userId" , target = "userId")
)
PriceCalculateReqDTO convert(AppTradeOrderCreateReqVO createReqVO, Long userId);
}

View File

@@ -138,19 +138,19 @@ public class TradeOrderItemDO extends BaseDO {
* 枚举 {@link TradeOrderItemRefundStatusEnum}
*/
private Integer refundStatus; // TODO 芋艿:可以考虑去查
// // 如上字段,举个例子:
// // 假设购买三个,即 stock = 3 。
// // originPrice = 15
// // 使用限时折扣单品优惠8 折buyPrice = 12
// // 开始算总的价格
// // buyTotal = buyPrice * stock = 12 * 3 = 36
// // discountTotal ,假设有满减送(分组优惠)满 20 减 10 ,并且使用优惠劵满 1.01 减 1 ,则 discountTotal = 10 + 1 = 11
// // presentTotal = buyTotal - discountTotal = 24 - 11 = 13
// // 最终 presentPrice = presentTotal / stock = 13 / 3 = 4.33
// /**
// * 退款总金额,单位:分 TODO
// */
// private Integer refundTotal;
// 如上字段,举个例子:
// 假设购买三个,即 stock = 3 。
// originPrice = 15
// 使用限时折扣单品优惠8 折buyPrice = 12
// 开始算总的价格
// buyTotal = buyPrice * stock = 12 * 3 = 36
// discountTotal ,假设有满减送(分组优惠)满 20 减 10 ,并且使用优惠劵满 1.01 减 1 ,则 discountTotal = 10 + 1 = 11
// presentTotal = buyTotal - discountTotal = 24 - 11 = 13
// 最终 presentPrice = presentTotal / stock = 13 / 3 = 4.33
/**
* 退款总金额,单位:分 TODO
*/
private Integer refundTotal;
/**
* 商品属性

View File

@@ -14,9 +14,10 @@ public interface TradeOrderService {
* 创建交易订单
*
* @param loginUserId 登录用户
* @param clientIp 用户 IP 地址
* @param userIp 用户 IP 地址
* @param createReqVO 创建交易订单请求模型
* @return 交易订单的编号
*/
Long createTradeOrder(Long loginUserId, String clientIp, AppTradeOrderCreateReqVO createReqVO);
Long createTradeOrder(Long loginUserId, String userIp, AppTradeOrderCreateReqVO createReqVO);
}

View File

@@ -18,12 +18,10 @@ import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import cn.iocoder.yudao.module.promotion.api.price.PriceApi;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO.Item;
import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
import cn.iocoder.yudao.module.trade.convert.price.PriceConvert;
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;
@@ -82,20 +80,19 @@ public class TradeOrderServiceImpl implements TradeOrderService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTradeOrder(Long userId, String clientIp, AppTradeOrderCreateReqVO createReqVO) {
public Long createTradeOrder(Long userId, String userIp, AppTradeOrderCreateReqVO createReqVO) {
// 商品 SKU 检查:可售状态、库存
List<ProductSkuRespDTO> skus = validateSkuSaleable(createReqVO.getItems());
// 商品 SPU 检查:可售状态
List<ProductSpuRespDTO> spus = validateSpuSaleable(convertSet(skus, ProductSkuRespDTO::getSpuId));
validateSpuSaleable(convertSet(skus, ProductSkuRespDTO::getSpuId));
// 用户收件地址的校验
AddressRespDTO address = validateAddress(userId, createReqVO.getAddressId());
// 价格计算
PriceCalculateReqDTO priceCalculateReqDTO = PriceConvert.INSTANCE.convert(createReqVO, userId);
PriceCalculateRespDTO priceResp = priceApi.calculatePrice(priceCalculateReqDTO);
PriceCalculateRespDTO priceResp = priceApi.calculatePrice(TradeOrderConvert.INSTANCE.convert(createReqVO, userId));
// 插入 TradeOrderDO 订单
TradeOrderDO tradeOrderDO = createTradeOrder(userId, clientIp, createReqVO, priceResp.getOrder(), address);
TradeOrderDO tradeOrderDO = createTradeOrder(userId, userIp, createReqVO, priceResp.getOrder(), address);
// 插入 TradeOrderItemDO 订单项
createTradeOrderItems(tradeOrderDO, priceResp.getOrder().getItems(), skus);
@@ -208,7 +205,7 @@ public class TradeOrderServiceImpl implements TradeOrderService {
// 校验是否存在禁用的 SPU
ProductSpuRespDTO spu = CollectionUtils.findFirst(spus,
spuDTO -> ObjectUtil.notEqual(ProductSpuStatusEnum.ENABLE.getStatus(), spuDTO.getStatus()));
if (Objects.isNull(spu)) {
if (spu != null) {
throw exception(ErrorCodeConstants.ORDER_CREATE_SPU_NOT_SALE);
}
return spus;
@@ -238,6 +235,9 @@ public class TradeOrderServiceImpl implements TradeOrderService {
tradeOrderDO.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
tradeOrderDO.setProductCount(getSumValue(order.getItems(), PriceCalculateRespDTO.OrderItem::getCount, Integer::sum));
tradeOrderDO.setTerminal(TerminalEnum.H5.getTerminal()); // todo 数据来源?
tradeOrderDO.setAdjustPrice(0).setPayed(false); // 支付信息
tradeOrderDO.setDeliveryStatus(false); // 物流信息
tradeOrderDO.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); // 退款信息
tradeOrderMapper.insert(tradeOrderDO);
return tradeOrderDO;
}

View File

@@ -1,110 +0,0 @@
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.promotion.api.price.PriceApi;
import cn.iocoder.yudao.module.promotion.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;
// TODO @芋艿: 单测的 review; 最后搞;
/**
* @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

@@ -1,53 +0,0 @@
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

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

View File

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

View File

@@ -1,78 +0,0 @@
/**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")
);