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