1、保存订单商品,2、验证参数是否合法
This commit is contained in:
215
docs/ORDER_GOODS_FEATURE_GUIDE.md
Normal file
215
docs/ORDER_GOODS_FEATURE_GUIDE.md
Normal file
@@ -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. 支持优惠券和折扣计算
|
||||||
192
docs/ORDER_VALIDATION_GUIDE.md
Normal file
192
docs/ORDER_VALIDATION_GUIDE.md
Normal file
@@ -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. **可维护性**:清晰的代码结构和完整的测试覆盖
|
||||||
|
|
||||||
|
这种方案比前端传递商品信息更安全可靠,是电商系统的最佳实践。
|
||||||
@@ -39,12 +39,12 @@ public class OrderConfigProperties {
|
|||||||
* 测试手机号列表
|
* 测试手机号列表
|
||||||
*/
|
*/
|
||||||
private List<String> phoneNumbers;
|
private List<String> phoneNumbers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试支付金额
|
* 测试支付金额
|
||||||
*/
|
*/
|
||||||
private BigDecimal testPayAmount = new BigDecimal("0.01");
|
private BigDecimal testPayAmount = new BigDecimal("0.01");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否启用测试模式
|
* 是否启用测试模式
|
||||||
*/
|
*/
|
||||||
@@ -57,22 +57,22 @@ public class OrderConfigProperties {
|
|||||||
* 租户ID
|
* 租户ID
|
||||||
*/
|
*/
|
||||||
private Integer tenantId;
|
private Integer tenantId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户名称
|
* 租户名称
|
||||||
*/
|
*/
|
||||||
private String tenantName;
|
private String tenantName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 最小金额限制
|
* 最小金额限制
|
||||||
*/
|
*/
|
||||||
private BigDecimal minAmount;
|
private BigDecimal minAmount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 金额限制提示信息
|
* 金额限制提示信息
|
||||||
*/
|
*/
|
||||||
private String minAmountMessage;
|
private String minAmountMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否启用
|
* 是否启用
|
||||||
*/
|
*/
|
||||||
@@ -81,16 +81,22 @@ public class OrderConfigProperties {
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class DefaultConfig {
|
public static class DefaultConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认标题
|
||||||
|
*/
|
||||||
|
private String defaultTitle = "订单标题";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认备注
|
* 默认备注
|
||||||
*/
|
*/
|
||||||
private String defaultComments = "暂无";
|
private String defaultComments = "暂无";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 最小订单金额
|
* 最小订单金额
|
||||||
*/
|
*/
|
||||||
private BigDecimal minOrderAmount = BigDecimal.ZERO;
|
private BigDecimal minOrderAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单超时时间(分钟)
|
* 订单超时时间(分钟)
|
||||||
*/
|
*/
|
||||||
@@ -101,8 +107,8 @@ public class OrderConfigProperties {
|
|||||||
* 检查是否为测试账号
|
* 检查是否为测试账号
|
||||||
*/
|
*/
|
||||||
public boolean isTestAccount(String phone) {
|
public boolean isTestAccount(String phone) {
|
||||||
return testAccount.isEnabled() &&
|
return testAccount.isEnabled() &&
|
||||||
testAccount.getPhoneNumbers() != null &&
|
testAccount.getPhoneNumbers() != null &&
|
||||||
testAccount.getPhoneNumbers().contains(phone);
|
testAccount.getPhoneNumbers().contains(phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package com.gxwebsoft.shop.dto;
|
package com.gxwebsoft.shop.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.*;
|
import javax.validation.constraints.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单创建请求DTO
|
* 订单创建请求DTO
|
||||||
@@ -23,6 +24,10 @@ public class OrderCreateRequest {
|
|||||||
@Max(value = 2, message = "订单类型值无效")
|
@Max(value = 2, message = "订单类型值无效")
|
||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
|
@Size(max = 60, message = "备注长度不能超过60个字符")
|
||||||
|
@Schema(description = "订单标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
@Schema(description = "快递/自提")
|
@Schema(description = "快递/自提")
|
||||||
private Integer deliveryType;
|
private Integer deliveryType;
|
||||||
|
|
||||||
@@ -44,9 +49,15 @@ public class OrderCreateRequest {
|
|||||||
@Schema(description = "使用的会员卡id")
|
@Schema(description = "使用的会员卡id")
|
||||||
private String cardId;
|
private String cardId;
|
||||||
|
|
||||||
|
@Schema(description = "关联收货地址")
|
||||||
|
private Integer addressId;
|
||||||
|
|
||||||
@Schema(description = "收货地址")
|
@Schema(description = "收货地址")
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "收货人姓名")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
@Schema(description = "地址纬度")
|
@Schema(description = "地址纬度")
|
||||||
private String addressLat;
|
private String addressLat;
|
||||||
|
|
||||||
@@ -129,4 +140,28 @@ public class OrderCreateRequest {
|
|||||||
@Schema(description = "租户id")
|
@Schema(description = "租户id")
|
||||||
@NotNull(message = "租户ID不能为空")
|
@NotNull(message = "租户ID不能为空")
|
||||||
private Integer tenantId;
|
private Integer tenantId;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ public class ShopOrder implements Serializable {
|
|||||||
@Schema(description = "订单类型,0商城订单 1预定订单/外卖 2会员卡")
|
@Schema(description = "订单类型,0商城订单 1预定订单/外卖 2会员卡")
|
||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "订单标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
@Schema(description = "快递/自提")
|
@Schema(description = "快递/自提")
|
||||||
private Integer deliveryType;
|
private Integer deliveryType;
|
||||||
|
|
||||||
@@ -218,7 +221,6 @@ public class ShopOrder implements Serializable {
|
|||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
@Schema(description = "真实姓名")
|
@Schema(description = "真实姓名")
|
||||||
@TableField(exist = false)
|
|
||||||
private String realName;
|
private String realName;
|
||||||
|
|
||||||
@Schema(description = "手机号码")
|
@Schema(description = "手机号码")
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ import com.gxwebsoft.common.core.exception.BusinessException;
|
|||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
import com.gxwebsoft.shop.config.OrderConfigProperties;
|
import com.gxwebsoft.shop.config.OrderConfigProperties;
|
||||||
import com.gxwebsoft.shop.dto.OrderCreateRequest;
|
import com.gxwebsoft.shop.dto.OrderCreateRequest;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopGoods;
|
||||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopOrderGoods;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +34,12 @@ public class OrderBusinessService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ShopOrderService shopOrderService;
|
private ShopOrderService shopOrderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopOrderGoodsService shopOrderGoodsService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopGoodsService shopGoodsService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private OrderConfigProperties orderConfig;
|
private OrderConfigProperties orderConfig;
|
||||||
|
|
||||||
@@ -41,6 +52,7 @@ public class OrderBusinessService {
|
|||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Map<String, String> createOrder(OrderCreateRequest request, User loginUser) {
|
public Map<String, String> createOrder(OrderCreateRequest request, User loginUser) {
|
||||||
|
|
||||||
// 1. 参数校验
|
// 1. 参数校验
|
||||||
validateOrderRequest(request, loginUser);
|
validateOrderRequest(request, loginUser);
|
||||||
|
|
||||||
@@ -56,7 +68,10 @@ public class OrderBusinessService {
|
|||||||
throw new BusinessException("订单保存失败");
|
throw new BusinessException("订单保存失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 创建微信支付订单
|
// 5. 保存订单商品
|
||||||
|
saveOrderGoods(request, shopOrder);
|
||||||
|
|
||||||
|
// 6. 创建微信支付订单
|
||||||
try {
|
try {
|
||||||
return shopOrderService.createWxOrder(shopOrder);
|
return shopOrderService.createWxOrder(shopOrder);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -73,19 +88,92 @@ public class OrderBusinessService {
|
|||||||
throw new BusinessException("用户未登录");
|
throw new BusinessException("用户未登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.getTotalPrice() == null || request.getTotalPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
// 验证商品信息并计算总金额
|
||||||
|
BigDecimal calculatedTotal = validateAndCalculateTotal(request);
|
||||||
|
|
||||||
|
if (calculatedTotal.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
throw new BusinessException("商品金额不能为0");
|
throw new BusinessException("商品金额不能为0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查前端传入的总金额是否正确(允许小的误差,比如0.01)
|
||||||
|
if (request.getTotalPrice() != null &&
|
||||||
|
request.getTotalPrice().subtract(calculatedTotal).abs().compareTo(new BigDecimal("0.01")) > 0) {
|
||||||
|
log.warn("订单金额计算不一致,前端传入:{},后台计算:{}", request.getTotalPrice(), calculatedTotal);
|
||||||
|
throw new BusinessException("订单金额计算错误,请刷新重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用后台计算的金额
|
||||||
|
request.setTotalPrice(calculatedTotal);
|
||||||
|
|
||||||
// 检查租户特殊规则
|
// 检查租户特殊规则
|
||||||
OrderConfigProperties.TenantRule tenantRule = orderConfig.getTenantRule(request.getTenantId());
|
OrderConfigProperties.TenantRule tenantRule = orderConfig.getTenantRule(request.getTenantId());
|
||||||
if (tenantRule != null && tenantRule.getMinAmount() != null) {
|
if (tenantRule != null && tenantRule.getMinAmount() != null) {
|
||||||
if (request.getTotalPrice().compareTo(tenantRule.getMinAmount()) < 0) {
|
if (calculatedTotal.compareTo(tenantRule.getMinAmount()) < 0) {
|
||||||
throw new BusinessException(tenantRule.getMinAmountMessage());
|
throw new BusinessException(tenantRule.getMinAmountMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证商品信息并计算总金额
|
||||||
|
*/
|
||||||
|
private BigDecimal validateAndCalculateTotal(OrderCreateRequest request) {
|
||||||
|
if (CollectionUtils.isEmpty(request.getGoodsItems())) {
|
||||||
|
throw new BusinessException("订单商品列表不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) {
|
||||||
|
// 验证商品ID
|
||||||
|
if (item.getGoodsId() == null) {
|
||||||
|
throw new BusinessException("商品ID不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证购买数量
|
||||||
|
if (item.getQuantity() == null || item.getQuantity() <= 0) {
|
||||||
|
throw new BusinessException("商品购买数量必须大于0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取商品信息
|
||||||
|
ShopGoods goods = shopGoodsService.getById(item.getGoodsId());
|
||||||
|
if (goods == null) {
|
||||||
|
throw new BusinessException("商品不存在,商品ID:" + item.getGoodsId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证商品状态
|
||||||
|
if (goods.getStatus() == null || goods.getStatus() != 0) {
|
||||||
|
throw new BusinessException("商品已下架:" + goods.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证商品价格
|
||||||
|
if (goods.getPrice() == null || goods.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
throw new BusinessException("商品价格异常:" + goods.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证库存(如果商品有库存管理)
|
||||||
|
if (goods.getStock() != null && goods.getStock() < item.getQuantity()) {
|
||||||
|
throw new BusinessException("商品库存不足:" + goods.getName() + ",当前库存:" + goods.getStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证购买数量限制
|
||||||
|
if (goods.getCanBuyNumber() != null && goods.getCanBuyNumber() > 0 &&
|
||||||
|
item.getQuantity() > goods.getCanBuyNumber()) {
|
||||||
|
throw new BusinessException("商品购买数量超过限制:" + goods.getName() + ",最大购买数量:" + goods.getCanBuyNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算商品小计
|
||||||
|
BigDecimal itemTotal = goods.getPrice().multiply(new BigDecimal(item.getQuantity()));
|
||||||
|
total = total.add(itemTotal);
|
||||||
|
|
||||||
|
log.debug("商品验证通过 - ID:{},名称:{},单价:{},数量:{},小计:{}",
|
||||||
|
goods.getGoodsId(), goods.getName(), goods.getPrice(), item.getQuantity(), itemTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("订单商品验证完成,总金额:{}", total);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建订单对象
|
* 构建订单对象
|
||||||
*/
|
*/
|
||||||
@@ -148,6 +236,79 @@ public class OrderBusinessService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存订单商品
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次验证商品状态(防止并发问题)
|
||||||
|
if (goods.getStatus() == null || goods.getStatus() != 0) {
|
||||||
|
throw new BusinessException("商品已下架:" + goods.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
// 计算商品小计(用于日志记录)
|
||||||
|
BigDecimal itemTotal = goods.getPrice().multiply(new BigDecimal(item.getQuantity()));
|
||||||
|
|
||||||
|
// 设置商品规格信息(如果有的话)
|
||||||
|
if (goods.getCode() != null) {
|
||||||
|
orderGoods.setSpec(goods.getCode()); // 使用商品编码作为规格
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置支付相关信息
|
||||||
|
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);
|
||||||
|
|
||||||
|
log.debug("准备保存订单商品 - 商品ID:{},名称:{},单价:{},数量:{},小计:{}",
|
||||||
|
goods.getGoodsId(), goods.getName(), goods.getPrice(), item.getQuantity(), itemTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存订单商品
|
||||||
|
boolean saved = shopOrderGoodsService.saveBatch(orderGoodsList);
|
||||||
|
if (!saved) {
|
||||||
|
throw new BusinessException("保存订单商品失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("成功保存订单商品,订单号:{},商品数量:{}", shopOrder.getOrderNo(), orderGoodsList.size());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否为测试账号
|
* 检查是否为测试账号
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ shop:
|
|||||||
|
|
||||||
# 默认配置
|
# 默认配置
|
||||||
default-config:
|
default-config:
|
||||||
|
default-title: "订单名称"
|
||||||
default-comments: "暂无"
|
default-comments: "暂无"
|
||||||
min-order-amount: 0
|
min-order-amount: 0
|
||||||
order-timeout-minutes: 30
|
order-timeout-minutes: 30
|
||||||
|
|||||||
170
src/test/java/com/gxwebsoft/shop/OrderBusinessServiceTest.java
Normal file
170
src/test/java/com/gxwebsoft/shop/OrderBusinessServiceTest.java
Normal file
@@ -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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
243
src/test/java/com/gxwebsoft/shop/OrderValidationTest.java
Normal file
243
src/test/java/com/gxwebsoft/shop/OrderValidationTest.java
Normal file
@@ -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("订单金额计算错误"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user