10 changed files with 1041 additions and 15 deletions
@ -0,0 +1,215 @@ |
|||
# 订单商品保存功能使用指南 |
|||
|
|||
## 功能概述 |
|||
|
|||
本功能为下单接口添加了保存订单商品的能力,支持在创建订单时同时保存多个商品项到订单商品表中。 |
|||
|
|||
## 前端数据结构 |
|||
|
|||
根据您提供的前端数据结构,系统现在支持以下格式的订单创建请求: |
|||
|
|||
```json |
|||
{ |
|||
"goodsItems": [ |
|||
{ |
|||
"goodsId": 10018, |
|||
"quantity": 1, |
|||
"payType": 1 |
|||
} |
|||
], |
|||
"addressId": 10832, |
|||
"comments": "科技小王子大米年卡套餐2.5kg", |
|||
"deliveryType": 0, |
|||
"type": 0, |
|||
"totalPrice": 99.00, |
|||
"payPrice": 99.00, |
|||
"totalNum": 1, |
|||
"payType": 1, |
|||
"tenantId": 1 |
|||
} |
|||
``` |
|||
|
|||
## 代码修改说明 |
|||
|
|||
### 1. OrderCreateRequest DTO 修改 |
|||
|
|||
在 `OrderCreateRequest` 类中添加了 `goodsItems` 字段: |
|||
|
|||
```java |
|||
@Schema(description = "订单商品列表") |
|||
@Valid |
|||
@NotEmpty(message = "订单商品列表不能为空") |
|||
private List<OrderGoodsItem> goodsItems; |
|||
|
|||
/** |
|||
* 订单商品项 |
|||
*/ |
|||
@Data |
|||
@Schema(name = "OrderGoodsItem", description = "订单商品项") |
|||
public static class OrderGoodsItem { |
|||
@Schema(description = "商品ID", required = true) |
|||
@NotNull(message = "商品ID不能为空") |
|||
private Integer goodsId; |
|||
|
|||
@Schema(description = "商品数量", required = true) |
|||
@NotNull(message = "商品数量不能为空") |
|||
@Min(value = 1, message = "商品数量必须大于0") |
|||
private Integer quantity; |
|||
|
|||
@Schema(description = "支付类型") |
|||
private Integer payType; |
|||
} |
|||
``` |
|||
|
|||
### 2. OrderBusinessService 修改 |
|||
|
|||
在 `OrderBusinessService` 中添加了保存订单商品的逻辑: |
|||
|
|||
```java |
|||
// 5. 保存订单商品 |
|||
saveOrderGoods(request, shopOrder); |
|||
|
|||
/** |
|||
* 保存订单商品 |
|||
*/ |
|||
private void saveOrderGoods(OrderCreateRequest request, ShopOrder shopOrder) { |
|||
if (CollectionUtils.isEmpty(request.getGoodsItems())) { |
|||
log.warn("订单商品列表为空,订单号:{}", shopOrder.getOrderNo()); |
|||
return; |
|||
} |
|||
|
|||
List<ShopOrderGoods> orderGoodsList = new ArrayList<>(); |
|||
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) { |
|||
// 获取商品信息 |
|||
ShopGoods goods = shopGoodsService.getById(item.getGoodsId()); |
|||
if (goods == null) { |
|||
throw new BusinessException("商品不存在,商品ID:" + item.getGoodsId()); |
|||
} |
|||
|
|||
ShopOrderGoods orderGoods = new ShopOrderGoods(); |
|||
|
|||
// 设置订单关联信息 |
|||
orderGoods.setOrderId(shopOrder.getOrderId()); |
|||
orderGoods.setOrderCode(shopOrder.getOrderNo()); |
|||
|
|||
// 设置商户信息 |
|||
orderGoods.setMerchantId(shopOrder.getMerchantId()); |
|||
orderGoods.setMerchantName(shopOrder.getMerchantName()); |
|||
|
|||
// 设置商品信息 |
|||
orderGoods.setGoodsId(item.getGoodsId()); |
|||
orderGoods.setGoodsName(goods.getName()); |
|||
orderGoods.setImage(goods.getImage()); |
|||
orderGoods.setPrice(goods.getPrice()); |
|||
orderGoods.setTotalNum(item.getQuantity()); |
|||
|
|||
// 设置支付相关信息 |
|||
orderGoods.setPayStatus(0); // 0 未付款 |
|||
orderGoods.setOrderStatus(0); // 0 未使用 |
|||
orderGoods.setIsFree(false); // 默认收费 |
|||
orderGoods.setVersion(0); // 当前版本 |
|||
|
|||
// 设置其他信息 |
|||
orderGoods.setComments(request.getComments()); |
|||
orderGoods.setUserId(shopOrder.getUserId()); |
|||
orderGoods.setTenantId(shopOrder.getTenantId()); |
|||
|
|||
orderGoodsList.add(orderGoods); |
|||
} |
|||
|
|||
// 批量保存订单商品 |
|||
boolean saved = shopOrderGoodsService.saveBatch(orderGoodsList); |
|||
if (!saved) { |
|||
throw new BusinessException("保存订单商品失败"); |
|||
} |
|||
|
|||
log.info("成功保存订单商品,订单号:{},商品数量:{}", shopOrder.getOrderNo(), orderGoodsList.size()); |
|||
} |
|||
``` |
|||
|
|||
## 功能特性 |
|||
|
|||
### 1. 数据验证 |
|||
- 商品ID不能为空 |
|||
- 商品数量必须大于0 |
|||
- 订单商品列表不能为空 |
|||
|
|||
### 2. 商品信息自动填充 |
|||
- 自动从商品表获取商品名称、图片、价格等信息 |
|||
- 验证商品是否存在 |
|||
|
|||
### 3. 状态管理 |
|||
- 默认设置为未付款状态(payStatus = 0) |
|||
- 默认设置为未使用状态(orderStatus = 0) |
|||
- 默认设置为收费商品(isFree = false) |
|||
|
|||
### 4. 事务支持 |
|||
- 整个订单创建过程在同一个事务中 |
|||
- 如果保存订单商品失败,整个订单创建会回滚 |
|||
|
|||
## 使用示例 |
|||
|
|||
### 单商品订单 |
|||
```json |
|||
{ |
|||
"goodsItems": [ |
|||
{ |
|||
"goodsId": 10018, |
|||
"quantity": 1, |
|||
"payType": 1 |
|||
} |
|||
], |
|||
"type": 0, |
|||
"totalPrice": 99.00, |
|||
"payPrice": 99.00, |
|||
"totalNum": 1, |
|||
"payType": 1, |
|||
"tenantId": 1, |
|||
"comments": "科技小王子大米年卡套餐2.5kg" |
|||
} |
|||
``` |
|||
|
|||
### 多商品订单 |
|||
```json |
|||
{ |
|||
"goodsItems": [ |
|||
{ |
|||
"goodsId": 10018, |
|||
"quantity": 1, |
|||
"payType": 1 |
|||
}, |
|||
{ |
|||
"goodsId": 10019, |
|||
"quantity": 2, |
|||
"payType": 1 |
|||
} |
|||
], |
|||
"type": 0, |
|||
"totalPrice": 297.00, |
|||
"payPrice": 297.00, |
|||
"totalNum": 3, |
|||
"payType": 1, |
|||
"tenantId": 1, |
|||
"comments": "多商品订单" |
|||
} |
|||
``` |
|||
|
|||
## 测试建议 |
|||
|
|||
1. **单元测试**: 已创建 `OrderBusinessServiceTest` 测试类,包含单商品和多商品订单的测试用例 |
|||
2. **集成测试**: 建议使用 Postman 或类似工具测试完整的订单创建流程 |
|||
3. **数据验证**: 测试各种边界情况,如商品不存在、数量为0等 |
|||
|
|||
## 注意事项 |
|||
|
|||
1. 确保商品表中存在对应的商品记录 |
|||
2. 前端需要正确计算订单总金额和总数量 |
|||
3. 支付类型字段在订单商品表中暂未使用,但保留了接口兼容性 |
|||
4. 建议在生产环境部署前进行充分测试 |
|||
|
|||
## 后续优化建议 |
|||
|
|||
1. 添加库存检查和扣减逻辑 |
|||
2. 支持商品规格(SKU)选择 |
|||
3. 添加商品价格变动检查 |
|||
4. 支持优惠券和折扣计算 |
@ -0,0 +1,192 @@ |
|||
# 订单商品验证功能指南 |
|||
|
|||
## 概述 |
|||
|
|||
本文档介绍了订单创建时商品信息后台验证的实现方案。该方案确保了订单数据的安全性和一致性,防止前端数据被篡改。 |
|||
|
|||
## 主要特性 |
|||
|
|||
### 1. 商品信息后台验证 |
|||
- ✅ 商品存在性验证 |
|||
- ✅ 商品状态验证(上架/下架) |
|||
- ✅ 商品价格验证 |
|||
- ✅ 库存数量验证 |
|||
- ✅ 购买数量限制验证 |
|||
- ✅ 订单总金额重新计算 |
|||
|
|||
### 2. 数据安全保障 |
|||
- ✅ 防止前端价格篡改 |
|||
- ✅ 使用后台查询的真实商品信息 |
|||
- ✅ 订单金额一致性检查 |
|||
- ✅ 详细的错误提示信息 |
|||
|
|||
## 实现细节 |
|||
|
|||
### 核心方法 |
|||
|
|||
#### 1. validateOrderRequest() |
|||
```java |
|||
private void validateOrderRequest(OrderCreateRequest request, User loginUser) { |
|||
// 1. 用户登录验证 |
|||
if (loginUser == null) { |
|||
throw new BusinessException("用户未登录"); |
|||
} |
|||
|
|||
// 2. 验证商品信息并计算总金额 |
|||
BigDecimal calculatedTotal = validateAndCalculateTotal(request); |
|||
|
|||
// 3. 检查金额一致性 |
|||
if (request.getTotalPrice() != null && |
|||
request.getTotalPrice().subtract(calculatedTotal).abs().compareTo(new BigDecimal("0.01")) > 0) { |
|||
throw new BusinessException("订单金额计算错误,请刷新重试"); |
|||
} |
|||
|
|||
// 4. 使用后台计算的金额 |
|||
request.setTotalPrice(calculatedTotal); |
|||
|
|||
// 5. 检查租户特殊规则 |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
#### 2. validateAndCalculateTotal() |
|||
```java |
|||
private BigDecimal validateAndCalculateTotal(OrderCreateRequest request) { |
|||
BigDecimal total = BigDecimal.ZERO; |
|||
|
|||
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) { |
|||
// 获取商品信息 |
|||
ShopGoods goods = shopGoodsService.getById(item.getGoodsId()); |
|||
|
|||
// 验证商品存在性 |
|||
if (goods == null) { |
|||
throw new BusinessException("商品不存在,商品ID:" + item.getGoodsId()); |
|||
} |
|||
|
|||
// 验证商品状态 |
|||
if (goods.getStatus() != 0) { |
|||
throw new BusinessException("商品已下架:" + goods.getName()); |
|||
} |
|||
|
|||
// 验证库存 |
|||
if (goods.getStock() != null && goods.getStock() < item.getQuantity()) { |
|||
throw new BusinessException("商品库存不足:" + goods.getName()); |
|||
} |
|||
|
|||
// 验证购买数量限制 |
|||
if (goods.getCanBuyNumber() != null && item.getQuantity() > goods.getCanBuyNumber()) { |
|||
throw new BusinessException("商品购买数量超过限制:" + goods.getName()); |
|||
} |
|||
|
|||
// 计算小计 |
|||
BigDecimal itemTotal = goods.getPrice().multiply(new BigDecimal(item.getQuantity())); |
|||
total = total.add(itemTotal); |
|||
} |
|||
|
|||
return total; |
|||
} |
|||
``` |
|||
|
|||
### 订单商品保存优化 |
|||
|
|||
```java |
|||
private void saveOrderGoods(OrderCreateRequest request, ShopOrder shopOrder) { |
|||
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) { |
|||
// 重新获取商品信息(确保数据一致性) |
|||
ShopGoods goods = shopGoodsService.getById(item.getGoodsId()); |
|||
|
|||
// 再次验证商品状态(防止并发问题) |
|||
if (goods.getStatus() != 0) { |
|||
throw new BusinessException("商品已下架:" + goods.getName()); |
|||
} |
|||
|
|||
ShopOrderGoods orderGoods = new ShopOrderGoods(); |
|||
|
|||
// 使用后台查询的真实数据 |
|||
orderGoods.setGoodsId(item.getGoodsId()); |
|||
orderGoods.setGoodsName(goods.getName()); |
|||
orderGoods.setPrice(goods.getPrice()); // 使用后台价格 |
|||
orderGoods.setTotalNum(item.getQuantity()); |
|||
|
|||
// 设置其他信息... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 验证流程 |
|||
|
|||
```mermaid |
|||
graph TD |
|||
A[前端提交订单] --> B[validateOrderRequest] |
|||
B --> C[validateAndCalculateTotal] |
|||
C --> D[验证商品存在性] |
|||
D --> E[验证商品状态] |
|||
E --> F[验证库存数量] |
|||
F --> G[验证购买限制] |
|||
G --> H[计算总金额] |
|||
H --> I[检查金额一致性] |
|||
I --> J[保存订单] |
|||
J --> K[saveOrderGoods] |
|||
K --> L[再次验证商品状态] |
|||
L --> M[使用后台数据保存] |
|||
``` |
|||
|
|||
## 错误处理 |
|||
|
|||
### 常见错误信息 |
|||
|
|||
| 错误码 | 错误信息 | 说明 | |
|||
|--------|----------|------| |
|||
| 1 | 用户未登录 | 用户身份验证失败 | |
|||
| 1 | 商品不存在,商品ID:xxx | 商品ID无效或已删除 | |
|||
| 1 | 商品已下架:xxx | 商品状态不是上架状态 | |
|||
| 1 | 商品价格异常:xxx | 商品价格为空或小于等于0 | |
|||
| 1 | 商品库存不足:xxx,当前库存:xxx | 购买数量超过可用库存 | |
|||
| 1 | 商品购买数量超过限制:xxx,最大购买数量:xxx | 超过单次购买限制 | |
|||
| 1 | 订单金额计算错误,请刷新重试 | 前端金额与后台计算不一致 | |
|||
| 1 | 商品金额不能为0 | 计算后的总金额为0或负数 | |
|||
|
|||
## 测试用例 |
|||
|
|||
项目包含完整的单元测试,覆盖以下场景: |
|||
|
|||
1. ✅ 正常订单创建 |
|||
2. ✅ 商品不存在 |
|||
3. ✅ 商品已下架 |
|||
4. ✅ 库存不足 |
|||
5. ✅ 超过购买限制 |
|||
6. ✅ 多商品金额计算 |
|||
7. ✅ 金额不一致检测 |
|||
|
|||
运行测试: |
|||
```bash |
|||
mvn test -Dtest=OrderValidationTest |
|||
``` |
|||
|
|||
## 最佳实践 |
|||
|
|||
### 1. 安全性 |
|||
- 始终使用后台查询的商品信息 |
|||
- 不信任前端传入的价格数据 |
|||
- 在保存前再次验证商品状态 |
|||
|
|||
### 2. 性能优化 |
|||
- 批量查询商品信息(如果需要) |
|||
- 缓存商品基础信息(可选) |
|||
- 合理的错误提示,避免过多数据库查询 |
|||
|
|||
### 3. 用户体验 |
|||
- 提供清晰的错误提示信息 |
|||
- 支持金额小误差容忍(0.01元) |
|||
- 及时更新前端商品状态 |
|||
|
|||
## 总结 |
|||
|
|||
通过后台验证商品信息的方案,我们实现了: |
|||
|
|||
1. **数据安全性**:防止价格篡改,确保订单金额准确 |
|||
2. **业务完整性**:完整的商品状态、库存、限制验证 |
|||
3. **系统稳定性**:详细的错误处理和日志记录 |
|||
4. **可维护性**:清晰的代码结构和完整的测试覆盖 |
|||
|
|||
这种方案比前端传递商品信息更安全可靠,是电商系统的最佳实践。 |
@ -0,0 +1,170 @@ |
|||
package com.gxwebsoft.shop; |
|||
|
|||
import com.gxwebsoft.common.system.entity.User; |
|||
import com.gxwebsoft.shop.dto.OrderCreateRequest; |
|||
import com.gxwebsoft.shop.entity.ShopGoods; |
|||
import com.gxwebsoft.shop.entity.ShopOrder; |
|||
import com.gxwebsoft.shop.entity.ShopOrderGoods; |
|||
import com.gxwebsoft.shop.service.OrderBusinessService; |
|||
import com.gxwebsoft.shop.service.ShopGoodsService; |
|||
import com.gxwebsoft.shop.service.ShopOrderGoodsService; |
|||
import com.gxwebsoft.shop.service.ShopOrderService; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
|
|||
import java.math.BigDecimal; |
|||
import java.util.Arrays; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
import static org.mockito.ArgumentMatchers.*; |
|||
import static org.mockito.Mockito.*; |
|||
|
|||
/** |
|||
* 订单业务服务测试类 |
|||
* |
|||
* @author 科技小王子 |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@ExtendWith(MockitoExtension.class) |
|||
public class OrderBusinessServiceTest { |
|||
|
|||
@Mock |
|||
private ShopOrderService shopOrderService; |
|||
|
|||
@Mock |
|||
private ShopOrderGoodsService shopOrderGoodsService; |
|||
|
|||
@Mock |
|||
private ShopGoodsService shopGoodsService; |
|||
|
|||
@InjectMocks |
|||
private OrderBusinessService orderBusinessService; |
|||
|
|||
private User testUser; |
|||
private OrderCreateRequest testRequest; |
|||
private ShopGoods testGoods; |
|||
|
|||
@BeforeEach |
|||
void setUp() { |
|||
// 准备测试用户
|
|||
testUser = new User(); |
|||
testUser.setUserId(1); |
|||
testUser.setOpenid("test_openid"); |
|||
testUser.setPhone("13800138000"); |
|||
|
|||
// 准备测试商品
|
|||
testGoods = new ShopGoods(); |
|||
testGoods.setGoodsId(10018); |
|||
testGoods.setName("科技小王子大米年卡套餐2.5kg"); |
|||
testGoods.setPrice(new BigDecimal("99.00")); |
|||
testGoods.setImage("test_image.jpg"); |
|||
|
|||
// 准备测试订单请求
|
|||
testRequest = new OrderCreateRequest(); |
|||
testRequest.setType(0); |
|||
testRequest.setTotalPrice(new BigDecimal("99.00")); |
|||
testRequest.setPayPrice(new BigDecimal("99.00")); |
|||
testRequest.setTotalNum(1); |
|||
testRequest.setPayType(1); |
|||
testRequest.setTenantId(1); |
|||
testRequest.setAddressId(10832); |
|||
testRequest.setComments("科技小王子大米年卡套餐2.5kg"); |
|||
testRequest.setDeliveryType(0); |
|||
|
|||
// 准备商品项列表
|
|||
OrderCreateRequest.OrderGoodsItem goodsItem = new OrderCreateRequest.OrderGoodsItem(); |
|||
goodsItem.setGoodsId(10018); |
|||
goodsItem.setQuantity(1); |
|||
goodsItem.setPayType(1); |
|||
|
|||
testRequest.setGoodsItems(Arrays.asList(goodsItem)); |
|||
} |
|||
|
|||
@Test |
|||
void testCreateOrderWithGoods() { |
|||
// Mock 商品查询
|
|||
when(shopGoodsService.getById(10018)).thenReturn(testGoods); |
|||
|
|||
// Mock 订单保存
|
|||
when(shopOrderService.save(any(ShopOrder.class))).thenAnswer(invocation -> { |
|||
ShopOrder order = invocation.getArgument(0); |
|||
order.setOrderId(1); // 模拟数据库生成的ID
|
|||
return true; |
|||
}); |
|||
|
|||
// Mock 订单商品批量保存
|
|||
when(shopOrderGoodsService.saveBatch(anyList())).thenReturn(true); |
|||
|
|||
// Mock 微信支付订单创建
|
|||
HashMap<String, String> wxOrderInfo = new HashMap<>(); |
|||
wxOrderInfo.put("prepay_id", "test_prepay_id"); |
|||
when(shopOrderService.createWxOrder(any(ShopOrder.class))).thenReturn(wxOrderInfo); |
|||
|
|||
// 执行测试
|
|||
Map<String, String> result = orderBusinessService.createOrder(testRequest, testUser); |
|||
|
|||
// 验证结果
|
|||
assert result != null; |
|||
assert result.containsKey("prepay_id"); |
|||
|
|||
// 验证方法调用
|
|||
verify(shopGoodsService, times(1)).getById(10018); |
|||
verify(shopOrderService, times(1)).save(any(ShopOrder.class)); |
|||
verify(shopOrderGoodsService, times(1)).saveBatch(anyList()); |
|||
verify(shopOrderService, times(1)).createWxOrder(any(ShopOrder.class)); |
|||
} |
|||
|
|||
@Test |
|||
void testCreateOrderWithMultipleGoods() { |
|||
// 准备多个商品项
|
|||
OrderCreateRequest.OrderGoodsItem goodsItem1 = new OrderCreateRequest.OrderGoodsItem(); |
|||
goodsItem1.setGoodsId(10018); |
|||
goodsItem1.setQuantity(1); |
|||
goodsItem1.setPayType(1); |
|||
|
|||
OrderCreateRequest.OrderGoodsItem goodsItem2 = new OrderCreateRequest.OrderGoodsItem(); |
|||
goodsItem2.setGoodsId(10019); |
|||
goodsItem2.setQuantity(2); |
|||
goodsItem2.setPayType(1); |
|||
|
|||
testRequest.setGoodsItems(Arrays.asList(goodsItem1, goodsItem2)); |
|||
testRequest.setTotalPrice(new BigDecimal("297.00")); // 99 + 99*2
|
|||
|
|||
// Mock 商品查询
|
|||
when(shopGoodsService.getById(10018)).thenReturn(testGoods); |
|||
|
|||
ShopGoods testGoods2 = new ShopGoods(); |
|||
testGoods2.setGoodsId(10019); |
|||
testGoods2.setName("测试商品2"); |
|||
testGoods2.setPrice(new BigDecimal("99.00")); |
|||
testGoods2.setImage("test_image2.jpg"); |
|||
when(shopGoodsService.getById(10019)).thenReturn(testGoods2); |
|||
|
|||
// Mock 其他服务
|
|||
when(shopOrderService.save(any(ShopOrder.class))).thenAnswer(invocation -> { |
|||
ShopOrder order = invocation.getArgument(0); |
|||
order.setOrderId(1); |
|||
return true; |
|||
}); |
|||
when(shopOrderGoodsService.saveBatch(anyList())).thenReturn(true); |
|||
when(shopOrderService.createWxOrder(any(ShopOrder.class))).thenReturn(new HashMap<>()); |
|||
|
|||
// 执行测试
|
|||
orderBusinessService.createOrder(testRequest, testUser); |
|||
|
|||
// 验证商品查询次数
|
|||
verify(shopGoodsService, times(1)).getById(10018); |
|||
verify(shopGoodsService, times(1)).getById(10019); |
|||
|
|||
// 验证保存的商品项数量
|
|||
verify(shopOrderGoodsService, times(1)).saveBatch(argThat(list -> |
|||
((List<ShopOrderGoods>) list).size() == 2 |
|||
)); |
|||
} |
|||
} |
@ -0,0 +1,243 @@ |
|||
package com.gxwebsoft.shop; |
|||
|
|||
import com.gxwebsoft.common.core.exception.BusinessException; |
|||
import com.gxwebsoft.common.system.entity.User; |
|||
import com.gxwebsoft.shop.config.OrderConfigProperties; |
|||
import com.gxwebsoft.shop.dto.OrderCreateRequest; |
|||
import com.gxwebsoft.shop.entity.ShopGoods; |
|||
import com.gxwebsoft.shop.service.OrderBusinessService; |
|||
import com.gxwebsoft.shop.service.ShopGoodsService; |
|||
import com.gxwebsoft.shop.service.ShopOrderGoodsService; |
|||
import com.gxwebsoft.shop.service.ShopOrderService; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
|
|||
import java.math.BigDecimal; |
|||
import java.util.Arrays; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
import static org.mockito.Mockito.*; |
|||
|
|||
/** |
|||
* 订单验证测试类 |
|||
* 测试商品信息后台验证逻辑 |
|||
*/ |
|||
@ExtendWith(MockitoExtension.class) |
|||
class OrderValidationTest { |
|||
|
|||
@Mock |
|||
private ShopOrderService shopOrderService; |
|||
|
|||
@Mock |
|||
private ShopOrderGoodsService shopOrderGoodsService; |
|||
|
|||
@Mock |
|||
private ShopGoodsService shopGoodsService; |
|||
|
|||
@Mock |
|||
private OrderConfigProperties orderConfig; |
|||
|
|||
@InjectMocks |
|||
private OrderBusinessService orderBusinessService; |
|||
|
|||
private User testUser; |
|||
private OrderCreateRequest testRequest; |
|||
private ShopGoods testGoods; |
|||
|
|||
@BeforeEach |
|||
void setUp() { |
|||
// 准备测试用户
|
|||
testUser = new User(); |
|||
testUser.setUserId(1); |
|||
testUser.setNickname("测试用户"); |
|||
testUser.setPhone("13800138000"); |
|||
|
|||
// 准备测试商品
|
|||
testGoods = new ShopGoods(); |
|||
testGoods.setGoodsId(10018); |
|||
testGoods.setName("测试商品"); |
|||
testGoods.setPrice(new BigDecimal("99.00")); |
|||
testGoods.setStatus(0); // 上架状态
|
|||
testGoods.setStock(100); // 库存100
|
|||
testGoods.setCanBuyNumber(10); // 最大购买数量10
|
|||
testGoods.setCode("TEST001"); |
|||
|
|||
// 准备测试订单请求
|
|||
testRequest = new OrderCreateRequest(); |
|||
testRequest.setType(0); |
|||
testRequest.setTitle("测试订单"); |
|||
testRequest.setTotalPrice(new BigDecimal("99.00")); |
|||
testRequest.setTenantId(1); |
|||
|
|||
// 准备商品项
|
|||
OrderCreateRequest.OrderGoodsItem goodsItem = new OrderCreateRequest.OrderGoodsItem(); |
|||
goodsItem.setGoodsId(10018); |
|||
goodsItem.setQuantity(1); |
|||
testRequest.setGoodsItems(Arrays.asList(goodsItem)); |
|||
} |
|||
|
|||
@Test |
|||
void testValidateOrderRequest_Success() { |
|||
// Mock 商品查询
|
|||
when(shopGoodsService.getById(10018)).thenReturn(testGoods); |
|||
when(orderConfig.getTenantRule(1)).thenReturn(null); |
|||
|
|||
// 执行验证 - 应该成功
|
|||
assertDoesNotThrow(() -> { |
|||
// 使用反射调用私有方法进行测试
|
|||
java.lang.reflect.Method method = OrderBusinessService.class |
|||
.getDeclaredMethod("validateOrderRequest", OrderCreateRequest.class, User.class); |
|||
method.setAccessible(true); |
|||
method.invoke(orderBusinessService, testRequest, testUser); |
|||
}); |
|||
|
|||
// 验证总金额被正确设置
|
|||
assertEquals(new BigDecimal("99.00"), testRequest.getTotalPrice()); |
|||
} |
|||
|
|||
@Test |
|||
void testValidateOrderRequest_GoodsNotFound() { |
|||
// Mock 商品不存在
|
|||
when(shopGoodsService.getById(10018)).thenReturn(null); |
|||
|
|||
// 执行验证 - 应该抛出异常
|
|||
Exception exception = assertThrows(Exception.class, () -> { |
|||
java.lang.reflect.Method method = OrderBusinessService.class |
|||
.getDeclaredMethod("validateOrderRequest", OrderCreateRequest.class, User.class); |
|||
method.setAccessible(true); |
|||
method.invoke(orderBusinessService, testRequest, testUser); |
|||
}); |
|||
|
|||
// 检查是否是 InvocationTargetException 包装的 BusinessException
|
|||
assertTrue(exception instanceof java.lang.reflect.InvocationTargetException); |
|||
Throwable cause = exception.getCause(); |
|||
assertTrue(cause instanceof BusinessException); |
|||
assertTrue(cause.getMessage().contains("商品不存在")); |
|||
} |
|||
|
|||
@Test |
|||
void testValidateOrderRequest_GoodsOffShelf() { |
|||
// 设置商品为下架状态
|
|||
testGoods.setStatus(1); |
|||
when(shopGoodsService.getById(10018)).thenReturn(testGoods); |
|||
|
|||
// 执行验证 - 应该抛出异常
|
|||
Exception exception = assertThrows(Exception.class, () -> { |
|||
java.lang.reflect.Method method = OrderBusinessService.class |
|||
.getDeclaredMethod("validateOrderRequest", OrderCreateRequest.class, User.class); |
|||
method.setAccessible(true); |
|||
method.invoke(orderBusinessService, testRequest, testUser); |
|||
}); |
|||
|
|||
// 检查是否是 InvocationTargetException 包装的 BusinessException
|
|||
assertTrue(exception instanceof java.lang.reflect.InvocationTargetException); |
|||
Throwable cause = exception.getCause(); |
|||
assertTrue(cause instanceof BusinessException); |
|||
assertTrue(cause.getMessage().contains("商品已下架")); |
|||
} |
|||
|
|||
@Test |
|||
void testValidateOrderRequest_InsufficientStock() { |
|||
// 设置库存不足
|
|||
testGoods.setStock(0); |
|||
when(shopGoodsService.getById(10018)).thenReturn(testGoods); |
|||
|
|||
// 执行验证 - 应该抛出异常
|
|||
Exception exception = assertThrows(Exception.class, () -> { |
|||
java.lang.reflect.Method method = OrderBusinessService.class |
|||
.getDeclaredMethod("validateOrderRequest", OrderCreateRequest.class, User.class); |
|||
method.setAccessible(true); |
|||
method.invoke(orderBusinessService, testRequest, testUser); |
|||
}); |
|||
|
|||
// 检查是否是 InvocationTargetException 包装的 BusinessException
|
|||
assertTrue(exception instanceof java.lang.reflect.InvocationTargetException); |
|||
Throwable cause = exception.getCause(); |
|||
assertTrue(cause instanceof BusinessException); |
|||
assertTrue(cause.getMessage().contains("商品库存不足")); |
|||
} |
|||
|
|||
@Test |
|||
void testValidateOrderRequest_ExceedBuyLimit() { |
|||
// 设置购买数量超过限制
|
|||
testRequest.getGoodsItems().get(0).setQuantity(15); // 超过最大购买数量10
|
|||
when(shopGoodsService.getById(10018)).thenReturn(testGoods); |
|||
|
|||
// 执行验证 - 应该抛出异常
|
|||
Exception exception = assertThrows(Exception.class, () -> { |
|||
java.lang.reflect.Method method = OrderBusinessService.class |
|||
.getDeclaredMethod("validateOrderRequest", OrderCreateRequest.class, User.class); |
|||
method.setAccessible(true); |
|||
method.invoke(orderBusinessService, testRequest, testUser); |
|||
}); |
|||
|
|||
// 检查是否是 InvocationTargetException 包装的 BusinessException
|
|||
assertTrue(exception instanceof java.lang.reflect.InvocationTargetException); |
|||
Throwable cause = exception.getCause(); |
|||
assertTrue(cause instanceof BusinessException); |
|||
assertTrue(cause.getMessage().contains("购买数量超过限制")); |
|||
} |
|||
|
|||
@Test |
|||
void testValidateOrderRequest_PriceCalculation() { |
|||
// 设置多个商品项
|
|||
OrderCreateRequest.OrderGoodsItem goodsItem1 = new OrderCreateRequest.OrderGoodsItem(); |
|||
goodsItem1.setGoodsId(10018); |
|||
goodsItem1.setQuantity(2); |
|||
|
|||
OrderCreateRequest.OrderGoodsItem goodsItem2 = new OrderCreateRequest.OrderGoodsItem(); |
|||
goodsItem2.setGoodsId(10019); |
|||
goodsItem2.setQuantity(1); |
|||
|
|||
testRequest.setGoodsItems(Arrays.asList(goodsItem1, goodsItem2)); |
|||
testRequest.setTotalPrice(new BigDecimal("297.00")); // 99*2 + 99*1
|
|||
|
|||
// 准备第二个商品
|
|||
ShopGoods testGoods2 = new ShopGoods(); |
|||
testGoods2.setGoodsId(10019); |
|||
testGoods2.setName("测试商品2"); |
|||
testGoods2.setPrice(new BigDecimal("99.00")); |
|||
testGoods2.setStatus(0); |
|||
testGoods2.setStock(100); |
|||
|
|||
when(shopGoodsService.getById(10018)).thenReturn(testGoods); |
|||
when(shopGoodsService.getById(10019)).thenReturn(testGoods2); |
|||
when(orderConfig.getTenantRule(1)).thenReturn(null); |
|||
|
|||
// 执行验证 - 应该成功
|
|||
assertDoesNotThrow(() -> { |
|||
java.lang.reflect.Method method = OrderBusinessService.class |
|||
.getDeclaredMethod("validateOrderRequest", OrderCreateRequest.class, User.class); |
|||
method.setAccessible(true); |
|||
method.invoke(orderBusinessService, testRequest, testUser); |
|||
}); |
|||
|
|||
// 验证总金额计算正确
|
|||
assertEquals(new BigDecimal("297.00"), testRequest.getTotalPrice()); |
|||
} |
|||
|
|||
@Test |
|||
void testValidateOrderRequest_PriceDiscrepancy() { |
|||
// 设置前端传入的金额与后台计算不一致
|
|||
testRequest.setTotalPrice(new BigDecimal("88.00")); // 错误的金额
|
|||
when(shopGoodsService.getById(10018)).thenReturn(testGoods); |
|||
|
|||
// 执行验证 - 应该抛出异常
|
|||
Exception exception = assertThrows(Exception.class, () -> { |
|||
java.lang.reflect.Method method = OrderBusinessService.class |
|||
.getDeclaredMethod("validateOrderRequest", OrderCreateRequest.class, User.class); |
|||
method.setAccessible(true); |
|||
method.invoke(orderBusinessService, testRequest, testUser); |
|||
}); |
|||
|
|||
// 检查是否是 InvocationTargetException 包装的 BusinessException
|
|||
assertTrue(exception instanceof java.lang.reflect.InvocationTargetException); |
|||
Throwable cause = exception.getCause(); |
|||
assertTrue(cause instanceof BusinessException); |
|||
assertTrue(cause.getMessage().contains("订单金额计算错误")); |
|||
} |
|||
} |
After Width: | Height: | Size: 46 KiB |
Loading…
Reference in new issue