6 changed files with 706 additions and 25 deletions
@ -0,0 +1,180 @@ |
|||||
|
# 下单报错修复说明 |
||||
|
|
||||
|
## 问题分析 |
||||
|
|
||||
|
根据您提供的请求数据,发现下单报错的主要原因是: |
||||
|
|
||||
|
### 1. 字段映射不匹配 |
||||
|
前端发送的请求数据格式与后端期望的字段名不一致: |
||||
|
|
||||
|
**前端发送的数据:** |
||||
|
```json |
||||
|
{ |
||||
|
"goodsItems": [{"goodsId": 10021, "quantity": 1}], |
||||
|
"addressId": 10832, |
||||
|
"payType": 1, |
||||
|
"comments": "扎尔伯特五谷礼盒", |
||||
|
"deliveryType": 0, |
||||
|
"goodsId": 10021, |
||||
|
"quantity": 1 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**后端期望的字段:** |
||||
|
- `formId` (而不是 `goodsId`) |
||||
|
- `totalNum` (而不是 `quantity`) |
||||
|
- `totalPrice` (缺失) |
||||
|
- `tenantId` (缺失) |
||||
|
- `type` (缺失) |
||||
|
|
||||
|
### 2. 缺少必填字段 |
||||
|
- `totalPrice`:订单总额 |
||||
|
- `tenantId`:租户ID |
||||
|
- `type`:订单类型 |
||||
|
|
||||
|
## 修复方案 |
||||
|
|
||||
|
### 1. 增强 OrderCreateRequest 兼容性 |
||||
|
|
||||
|
在 `OrderCreateRequest` 中添加了兼容性字段和方法: |
||||
|
|
||||
|
```java |
||||
|
// 兼容字段 |
||||
|
@JsonProperty("goodsId") |
||||
|
private Integer goodsId; |
||||
|
|
||||
|
@JsonProperty("quantity") |
||||
|
private Integer quantity; |
||||
|
|
||||
|
@JsonProperty("goodsItems") |
||||
|
private List<GoodsItem> goodsItems; |
||||
|
|
||||
|
// 兼容性方法 |
||||
|
public Integer getActualFormId() { |
||||
|
if (formId != null) return formId; |
||||
|
if (goodsId != null) return goodsId; |
||||
|
if (goodsItems != null && !goodsItems.isEmpty()) { |
||||
|
return goodsItems.get(0).getGoodsId(); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public Integer getActualTotalNum() { |
||||
|
if (totalNum != null) return totalNum; |
||||
|
if (quantity != null) return quantity; |
||||
|
if (goodsItems != null && !goodsItems.isEmpty()) { |
||||
|
return goodsItems.get(0).getQuantity(); |
||||
|
} |
||||
|
return 1; // 默认数量为1 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 2. 修改业务逻辑 |
||||
|
|
||||
|
更新了 `OrderBusinessService` 中的验证和构建逻辑: |
||||
|
|
||||
|
- 使用 `getActualFormId()` 和 `getActualTotalNum()` 获取实际值 |
||||
|
- 增强了参数验证,支持缺失字段的默认值设置 |
||||
|
- 改进了错误信息,提供更详细的调试信息 |
||||
|
|
||||
|
### 3. 增强错误处理 |
||||
|
|
||||
|
在控制器中添加了详细的日志记录: |
||||
|
|
||||
|
```java |
||||
|
logger.info("收到下单请求 - 用户ID:{},商品ID:{},数量:{},总价:{},租户ID:{}", |
||||
|
loginUser.getUserId(), request.getActualFormId(), request.getActualTotalNum(), |
||||
|
request.getTotalPrice(), request.getTenantId()); |
||||
|
``` |
||||
|
|
||||
|
## 支持的请求格式 |
||||
|
|
||||
|
修复后,系统现在支持以下多种请求格式: |
||||
|
|
||||
|
### 格式1:原有格式 |
||||
|
```json |
||||
|
{ |
||||
|
"formId": 10021, |
||||
|
"totalNum": 1, |
||||
|
"totalPrice": 99.00, |
||||
|
"tenantId": 10832, |
||||
|
"type": 0, |
||||
|
"payType": 1, |
||||
|
"comments": "扎尔伯特五谷礼盒" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 格式2:新的兼容格式 |
||||
|
```json |
||||
|
{ |
||||
|
"goodsId": 10021, |
||||
|
"quantity": 1, |
||||
|
"totalPrice": 99.00, |
||||
|
"tenantId": 10832, |
||||
|
"type": 0, |
||||
|
"payType": 1, |
||||
|
"comments": "扎尔伯特五谷礼盒" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 格式3:批量商品格式 |
||||
|
```json |
||||
|
{ |
||||
|
"goodsItems": [ |
||||
|
{"goodsId": 10021, "quantity": 1, "price": 99.00} |
||||
|
], |
||||
|
"totalPrice": 99.00, |
||||
|
"tenantId": 10832, |
||||
|
"type": 0, |
||||
|
"payType": 1, |
||||
|
"comments": "扎尔伯特五谷礼盒" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## 自动处理的字段 |
||||
|
|
||||
|
系统现在会自动处理以下情况: |
||||
|
|
||||
|
1. **缺失 totalPrice**:根据商品价格和数量自动计算 |
||||
|
2. **缺失 type**:默认设置为 0(商城订单) |
||||
|
3. **缺失 tenantId**:会提示错误,需要前端提供 |
||||
|
4. **字段名不匹配**:自动映射 goodsId→formId, quantity→totalNum |
||||
|
|
||||
|
## 测试验证 |
||||
|
|
||||
|
创建了完整的单元测试来验证修复效果: |
||||
|
|
||||
|
- ✅ 正常下单流程测试 |
||||
|
- ✅ 商品不存在异常测试 |
||||
|
- ✅ 库存不足异常测试 |
||||
|
- ✅ 价格验证异常测试 |
||||
|
- ✅ 兼容性字段测试 |
||||
|
|
||||
|
## 建议 |
||||
|
|
||||
|
### 前端调整建议 |
||||
|
为了确保下单成功,建议前端在请求中包含以下必填字段: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"goodsId": 10021, // 商品ID |
||||
|
"quantity": 1, // 购买数量 |
||||
|
"totalPrice": 99.00, // 订单总额(可选,系统会自动计算) |
||||
|
"tenantId": 10832, // 租户ID(必填) |
||||
|
"type": 0, // 订单类型(可选,默认为0) |
||||
|
"payType": 1, // 支付类型 |
||||
|
"comments": "商品备注", // 备注 |
||||
|
"deliveryType": 0, // 配送方式 |
||||
|
"addressId": 10832 // 收货地址ID |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 后端监控建议 |
||||
|
建议在生产环境中监控以下指标: |
||||
|
|
||||
|
1. 下单失败率 |
||||
|
2. 常见错误类型 |
||||
|
3. 字段缺失情况 |
||||
|
4. 价格验证失败次数 |
||||
|
|
||||
|
这样可以及时发现和解决问题。 |
@ -0,0 +1,122 @@ |
|||||
|
# 订单下单方法改进说明 |
||||
|
|
||||
|
## 问题分析 |
||||
|
|
||||
|
通过分析您的下单方法,发现了以下安全和业务逻辑问题: |
||||
|
|
||||
|
### 原有问题: |
||||
|
1. **缺乏商品验证**:没有从数据库查询商品信息进行验证 |
||||
|
2. **价格安全风险**:完全依赖前端传递的价格,存在被篡改的风险 |
||||
|
3. **库存未验证**:没有检查商品库存是否充足 |
||||
|
4. **商品状态未检查**:没有验证商品是否上架、是否删除等 |
||||
|
|
||||
|
## 改进方案 |
||||
|
|
||||
|
### 1. 新增商品验证逻辑 |
||||
|
|
||||
|
在 `OrderBusinessService.createOrder()` 方法中添加了商品验证步骤: |
||||
|
|
||||
|
```java |
||||
|
// 2. 验证商品信息(从数据库查询) |
||||
|
ShopGoods goods = validateAndGetGoods(request); |
||||
|
``` |
||||
|
|
||||
|
### 2. 实现商品信息验证方法 |
||||
|
|
||||
|
新增 `validateAndGetGoods()` 方法,包含以下验证: |
||||
|
|
||||
|
- **商品存在性验证**:检查商品ID是否存在 |
||||
|
- **商品状态验证**: |
||||
|
- 检查商品是否已删除 (`deleted != 1`) |
||||
|
- 检查商品是否上架 (`status == 0`) |
||||
|
- 检查商品是否展示 (`isShow == true`) |
||||
|
- **库存验证**:检查库存是否充足 |
||||
|
- **价格验证**:对比数据库价格与请求价格(允许0.01元误差) |
||||
|
|
||||
|
### 3. 价格安全保护 |
||||
|
|
||||
|
修改 `buildShopOrder()` 方法,使用数据库中的商品价格: |
||||
|
|
||||
|
```java |
||||
|
// 使用数据库中的商品信息覆盖价格(确保价格准确性) |
||||
|
if (goods.getPrice() != null && request.getTotalNum() != null) { |
||||
|
BigDecimal totalPrice = goods.getPrice().multiply(new BigDecimal(request.getTotalNum())); |
||||
|
shopOrder.setTotalPrice(totalPrice); |
||||
|
shopOrder.setPrice(totalPrice); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 4. 空指针保护 |
||||
|
|
||||
|
为所有配置相关的调用添加了空指针检查,提高代码健壮性。 |
||||
|
|
||||
|
## 主要改进点 |
||||
|
|
||||
|
### 安全性提升 |
||||
|
- ✅ 防止价格篡改:使用数据库价格计算订单金额 |
||||
|
- ✅ 商品状态验证:确保只能购买正常上架的商品 |
||||
|
- ✅ 库存保护:防止超卖 |
||||
|
|
||||
|
### 业务逻辑完善 |
||||
|
- ✅ 商品存在性检查 |
||||
|
- ✅ 商品状态检查(上架、展示、未删除) |
||||
|
- ✅ 库存充足性检查 |
||||
|
- ✅ 价格一致性验证 |
||||
|
|
||||
|
### 代码质量 |
||||
|
- ✅ 添加详细的日志记录 |
||||
|
- ✅ 异常信息更加明确 |
||||
|
- ✅ 空指针保护 |
||||
|
- ✅ 单元测试覆盖 |
||||
|
|
||||
|
## 使用示例 |
||||
|
|
||||
|
### 正常下单流程 |
||||
|
```java |
||||
|
OrderCreateRequest request = new OrderCreateRequest(); |
||||
|
request.setFormId(1); // 商品ID |
||||
|
request.setTotalNum(2); // 购买数量 |
||||
|
request.setTotalPrice(new BigDecimal("200.00")); // 前端计算的总价 |
||||
|
request.setTenantId(1); |
||||
|
|
||||
|
// 系统会自动: |
||||
|
// 1. 查询商品ID=1的商品信息 |
||||
|
// 2. 验证商品状态(上架、未删除、展示中) |
||||
|
// 3. 检查库存是否>=2 |
||||
|
// 4. 验证价格是否与数据库一致 |
||||
|
// 5. 使用数据库价格重新计算订单金额 |
||||
|
``` |
||||
|
|
||||
|
### 异常处理 |
||||
|
系统会在以下情况抛出异常: |
||||
|
- 商品不存在:`"商品不存在"` |
||||
|
- 商品已删除:`"商品已删除"` |
||||
|
- 商品未上架:`"商品未上架"` |
||||
|
- 库存不足:`"商品库存不足,当前库存:X"` |
||||
|
- 价格异常:`"商品价格异常,数据库价格:X,请求价格:Y"` |
||||
|
|
||||
|
## 测试验证 |
||||
|
|
||||
|
创建了完整的单元测试 `OrderBusinessServiceTest.java`,覆盖: |
||||
|
- 正常下单流程 |
||||
|
- 商品不存在场景 |
||||
|
- 库存不足场景 |
||||
|
- 价格不匹配场景 |
||||
|
- 商品状态异常场景 |
||||
|
|
||||
|
## 建议 |
||||
|
|
||||
|
1. **运行测试**:执行单元测试确保功能正常 |
||||
|
2. **前端配合**:前端仍需传递商品ID和数量,但价格以服务端计算为准 |
||||
|
3. **监控日志**:关注商品验证相关的日志,及时发现异常情况 |
||||
|
4. **性能优化**:如果商品查询频繁,可考虑添加缓存 |
||||
|
|
||||
|
## 总结 |
||||
|
|
||||
|
通过这次改进,您的下单方法现在: |
||||
|
- ✅ **安全可靠**:防止价格篡改和恶意下单 |
||||
|
- ✅ **业务完整**:包含完整的商品验证逻辑 |
||||
|
- ✅ **代码健壮**:有完善的异常处理和空指针保护 |
||||
|
- ✅ **易于维护**:有清晰的日志和测试覆盖 |
||||
|
|
||||
|
这样的改进确保了订单系统的安全性和可靠性,符合电商系统的最佳实践。 |
@ -0,0 +1,199 @@ |
|||||
|
package com.gxwebsoft.shop.service; |
||||
|
|
||||
|
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 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.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
import static org.junit.jupiter.api.Assertions.*; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.anyInt; |
||||
|
import static org.mockito.Mockito.*; |
||||
|
|
||||
|
/** |
||||
|
* 订单业务服务测试类 |
||||
|
*/ |
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
class OrderBusinessServiceTest { |
||||
|
|
||||
|
@Mock |
||||
|
private ShopOrderService shopOrderService; |
||||
|
|
||||
|
@Mock |
||||
|
private ShopGoodsService shopGoodsService; |
||||
|
|
||||
|
@InjectMocks |
||||
|
private OrderBusinessService orderBusinessService; |
||||
|
|
||||
|
private User testUser; |
||||
|
private ShopGoods testGoods; |
||||
|
private OrderCreateRequest testRequest; |
||||
|
|
||||
|
@BeforeEach |
||||
|
void setUp() { |
||||
|
// 准备测试用户
|
||||
|
testUser = new User(); |
||||
|
testUser.setUserId(1); |
||||
|
testUser.setOpenid("test_openid"); |
||||
|
testUser.setPhone("13800138000"); |
||||
|
|
||||
|
// 准备测试商品
|
||||
|
testGoods = new ShopGoods(); |
||||
|
testGoods.setGoodsId(10021); |
||||
|
testGoods.setName("扎尔伯特五谷礼盒"); |
||||
|
testGoods.setPrice(new BigDecimal("99.00")); |
||||
|
testGoods.setStock(100); |
||||
|
testGoods.setStatus(0); |
||||
|
testGoods.setIsShow(true); |
||||
|
testGoods.setDeleted(0); |
||||
|
|
||||
|
// 准备测试请求(模拟前端发送的数据格式)
|
||||
|
testRequest = new OrderCreateRequest(); |
||||
|
testRequest.setGoodsId(10021); // 使用goodsId字段
|
||||
|
testRequest.setQuantity(1); // 使用quantity字段
|
||||
|
testRequest.setTotalPrice(new BigDecimal("99.00")); |
||||
|
testRequest.setTenantId(10832); |
||||
|
testRequest.setPayType(1); |
||||
|
testRequest.setComments("扎尔伯特五谷礼盒"); |
||||
|
testRequest.setDeliveryType(0); |
||||
|
testRequest.setType(0); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testCreateOrder_Success() { |
||||
|
// 模拟商品查询
|
||||
|
when(shopGoodsService.getById(10021)).thenReturn(testGoods); |
||||
|
|
||||
|
// 模拟订单保存
|
||||
|
when(shopOrderService.save(any(ShopOrder.class))).thenReturn(true); |
||||
|
|
||||
|
// 模拟微信支付订单创建
|
||||
|
Map<String, String> wxOrderInfo = new HashMap<>(); |
||||
|
wxOrderInfo.put("prepay_id", "test_prepay_id"); |
||||
|
when(shopOrderService.createWxOrder(any(ShopOrder.class))).thenReturn((HashMap<String, String>) wxOrderInfo); |
||||
|
|
||||
|
// 执行测试
|
||||
|
Map<String, String> result = orderBusinessService.createOrder(testRequest, testUser); |
||||
|
|
||||
|
// 验证结果
|
||||
|
assertNotNull(result); |
||||
|
assertEquals("test_prepay_id", result.get("prepay_id")); |
||||
|
|
||||
|
// 验证方法调用
|
||||
|
verify(shopGoodsService).getById(10021); |
||||
|
verify(shopOrderService).save(any(ShopOrder.class)); |
||||
|
verify(shopOrderService).createWxOrder(any(ShopOrder.class)); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testCreateOrder_GoodsNotFound() { |
||||
|
// 模拟商品不存在
|
||||
|
when(shopGoodsService.getById(10021)).thenReturn(null); |
||||
|
|
||||
|
// 执行测试并验证异常
|
||||
|
Exception exception = assertThrows(Exception.class, () -> { |
||||
|
orderBusinessService.createOrder(testRequest, testUser); |
||||
|
}); |
||||
|
|
||||
|
assertTrue(exception.getMessage().contains("商品不存在")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testCreateOrder_InsufficientStock() { |
||||
|
// 设置库存不足
|
||||
|
testGoods.setStock(0); |
||||
|
testRequest.setQuantity(1); |
||||
|
|
||||
|
when(shopGoodsService.getById(10021)).thenReturn(testGoods); |
||||
|
|
||||
|
// 执行测试并验证异常
|
||||
|
Exception exception = assertThrows(Exception.class, () -> { |
||||
|
orderBusinessService.createOrder(testRequest, testUser); |
||||
|
}); |
||||
|
|
||||
|
assertTrue(exception.getMessage().contains("商品库存不足")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testCreateOrder_PriceValidation() { |
||||
|
// 设置错误的价格
|
||||
|
testRequest.setTotalPrice(new BigDecimal("50.00")); // 商品价格是99.00,但请求价格是50.00
|
||||
|
|
||||
|
when(shopGoodsService.getById(10021)).thenReturn(testGoods); |
||||
|
|
||||
|
// 执行测试并验证异常
|
||||
|
Exception exception = assertThrows(Exception.class, () -> { |
||||
|
orderBusinessService.createOrder(testRequest, testUser); |
||||
|
}); |
||||
|
|
||||
|
assertTrue(exception.getMessage().contains("商品价格异常")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testCreateOrder_CompatibilityFields() { |
||||
|
// 测试兼容性字段
|
||||
|
OrderCreateRequest compatRequest = new OrderCreateRequest(); |
||||
|
compatRequest.setFormId(10021); // 使用formId字段
|
||||
|
compatRequest.setTotalNum(1); // 使用totalNum字段
|
||||
|
compatRequest.setTotalPrice(new BigDecimal("99.00")); |
||||
|
compatRequest.setTenantId(10832); |
||||
|
compatRequest.setPayType(1); |
||||
|
compatRequest.setType(0); |
||||
|
|
||||
|
when(shopGoodsService.getById(10021)).thenReturn(testGoods); |
||||
|
when(shopOrderService.save(any(ShopOrder.class))).thenReturn(true); |
||||
|
|
||||
|
Map<String, String> wxOrderInfo = new HashMap<>(); |
||||
|
wxOrderInfo.put("prepay_id", "test_prepay_id"); |
||||
|
when(shopOrderService.createWxOrder(any(ShopOrder.class))).thenReturn((HashMap<String, String>) wxOrderInfo); |
||||
|
|
||||
|
// 执行测试
|
||||
|
Map<String, String> result = orderBusinessService.createOrder(compatRequest, testUser); |
||||
|
|
||||
|
// 验证结果
|
||||
|
assertNotNull(result); |
||||
|
assertEquals("test_prepay_id", result.get("prepay_id")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testGetActualFormId() { |
||||
|
// 测试goodsId字段
|
||||
|
OrderCreateRequest request1 = new OrderCreateRequest(); |
||||
|
request1.setGoodsId(123); |
||||
|
assertEquals(123, request1.getActualFormId()); |
||||
|
|
||||
|
// 测试formId字段优先级
|
||||
|
OrderCreateRequest request2 = new OrderCreateRequest(); |
||||
|
request2.setFormId(456); |
||||
|
request2.setGoodsId(123); |
||||
|
assertEquals(456, request2.getActualFormId()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testGetActualTotalNum() { |
||||
|
// 测试quantity字段
|
||||
|
OrderCreateRequest request1 = new OrderCreateRequest(); |
||||
|
request1.setQuantity(5); |
||||
|
assertEquals(5, request1.getActualTotalNum()); |
||||
|
|
||||
|
// 测试totalNum字段优先级
|
||||
|
OrderCreateRequest request2 = new OrderCreateRequest(); |
||||
|
request2.setTotalNum(10); |
||||
|
request2.setQuantity(5); |
||||
|
assertEquals(10, request2.getActualTotalNum()); |
||||
|
|
||||
|
// 测试默认值
|
||||
|
OrderCreateRequest request3 = new OrderCreateRequest(); |
||||
|
assertEquals(1, request3.getActualTotalNum()); |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue