From d3904420a94d37060c5c64be544aa85892cae566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 30 Jul 2025 01:32:10 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E4=BF=9D=E5=AD=98=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=95=86=E5=93=81=EF=BC=8C2=E3=80=81=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=98=AF=E5=90=A6=E5=90=88=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ORDER_GOODS_FEATURE_GUIDE.md | 215 ++++++++++++++++ docs/ORDER_VALIDATION_GUIDE.md | 192 ++++++++++++++ .../shop/config/OrderConfigProperties.java | 26 +- .../shop/dto/OrderCreateRequest.java | 37 ++- .../com/gxwebsoft/shop/entity/ShopOrder.java | 4 +- .../shop/service/OrderBusinessService.java | 167 +++++++++++- src/main/resources/application.yml | 1 + .../shop/OrderBusinessServiceTest.java | 170 ++++++++++++ .../gxwebsoft/shop/OrderValidationTest.java | 243 ++++++++++++++++++ 下单流程图.svg | 1 + 10 files changed, 1041 insertions(+), 15 deletions(-) create mode 100644 docs/ORDER_GOODS_FEATURE_GUIDE.md create mode 100644 docs/ORDER_VALIDATION_GUIDE.md create mode 100644 src/test/java/com/gxwebsoft/shop/OrderBusinessServiceTest.java create mode 100644 src/test/java/com/gxwebsoft/shop/OrderValidationTest.java create mode 100644 下单流程图.svg diff --git a/docs/ORDER_GOODS_FEATURE_GUIDE.md b/docs/ORDER_GOODS_FEATURE_GUIDE.md new file mode 100644 index 0000000..7801d68 --- /dev/null +++ b/docs/ORDER_GOODS_FEATURE_GUIDE.md @@ -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 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 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. 支持优惠券和折扣计算 diff --git a/docs/ORDER_VALIDATION_GUIDE.md b/docs/ORDER_VALIDATION_GUIDE.md new file mode 100644 index 0000000..340a287 --- /dev/null +++ b/docs/ORDER_VALIDATION_GUIDE.md @@ -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. **可维护性**:清晰的代码结构和完整的测试覆盖 + +这种方案比前端传递商品信息更安全可靠,是电商系统的最佳实践。 diff --git a/src/main/java/com/gxwebsoft/shop/config/OrderConfigProperties.java b/src/main/java/com/gxwebsoft/shop/config/OrderConfigProperties.java index b21f776..2d0ba07 100644 --- a/src/main/java/com/gxwebsoft/shop/config/OrderConfigProperties.java +++ b/src/main/java/com/gxwebsoft/shop/config/OrderConfigProperties.java @@ -39,12 +39,12 @@ public class OrderConfigProperties { * 测试手机号列表 */ private List phoneNumbers; - + /** * 测试支付金额 */ private BigDecimal testPayAmount = new BigDecimal("0.01"); - + /** * 是否启用测试模式 */ @@ -57,22 +57,22 @@ public class OrderConfigProperties { * 租户ID */ private Integer tenantId; - + /** * 租户名称 */ private String tenantName; - + /** * 最小金额限制 */ private BigDecimal minAmount; - + /** * 金额限制提示信息 */ private String minAmountMessage; - + /** * 是否启用 */ @@ -81,16 +81,22 @@ public class OrderConfigProperties { @Data public static class DefaultConfig { + + /** + * 默认标题 + */ + private String defaultTitle = "订单标题"; + /** * 默认备注 */ private String defaultComments = "暂无"; - + /** * 最小订单金额 */ private BigDecimal minOrderAmount = BigDecimal.ZERO; - + /** * 订单超时时间(分钟) */ @@ -101,8 +107,8 @@ public class OrderConfigProperties { * 检查是否为测试账号 */ public boolean isTestAccount(String phone) { - return testAccount.isEnabled() && - testAccount.getPhoneNumbers() != null && + return testAccount.isEnabled() && + testAccount.getPhoneNumbers() != null && testAccount.getPhoneNumbers().contains(phone); } diff --git a/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java b/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java index d516f00..0ed0290 100644 --- a/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java +++ b/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java @@ -1,11 +1,12 @@ package com.gxwebsoft.shop.dto; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import javax.validation.Valid; import javax.validation.constraints.*; import java.math.BigDecimal; +import java.util.List; /** * 订单创建请求DTO @@ -23,6 +24,10 @@ public class OrderCreateRequest { @Max(value = 2, message = "订单类型值无效") private Integer type; + @Size(max = 60, message = "备注长度不能超过60个字符") + @Schema(description = "订单标题") + private String title; + @Schema(description = "快递/自提") private Integer deliveryType; @@ -44,9 +49,15 @@ public class OrderCreateRequest { @Schema(description = "使用的会员卡id") private String cardId; + @Schema(description = "关联收货地址") + private Integer addressId; + @Schema(description = "收货地址") private String address; + @Schema(description = "收货人姓名") + private String realName; + @Schema(description = "地址纬度") private String addressLat; @@ -129,4 +140,28 @@ public class OrderCreateRequest { @Schema(description = "租户id") @NotNull(message = "租户ID不能为空") private Integer tenantId; + + @Schema(description = "订单商品列表") + @Valid + @NotEmpty(message = "订单商品列表不能为空") + private List 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; + } } diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java b/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java index 541b1dd..af57caa 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java +++ b/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java @@ -40,6 +40,9 @@ public class ShopOrder implements Serializable { @Schema(description = "订单类型,0商城订单 1预定订单/外卖 2会员卡") private Integer type; + @Schema(description = "订单标题") + private String title; + @Schema(description = "快递/自提") private Integer deliveryType; @@ -218,7 +221,6 @@ public class ShopOrder implements Serializable { private String nickname; @Schema(description = "真实姓名") - @TableField(exist = false) private String realName; @Schema(description = "手机号码") diff --git a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java index 3d5f648..34f8690 100644 --- a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java +++ b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java @@ -5,14 +5,19 @@ 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.entity.ShopOrder; +import com.gxwebsoft.shop.entity.ShopOrderGoods; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** @@ -29,6 +34,12 @@ public class OrderBusinessService { @Resource private ShopOrderService shopOrderService; + @Resource + private ShopOrderGoodsService shopOrderGoodsService; + + @Resource + private ShopGoodsService shopGoodsService; + @Resource private OrderConfigProperties orderConfig; @@ -41,6 +52,7 @@ public class OrderBusinessService { */ @Transactional(rollbackFor = Exception.class) public Map createOrder(OrderCreateRequest request, User loginUser) { + // 1. 参数校验 validateOrderRequest(request, loginUser); @@ -56,7 +68,10 @@ public class OrderBusinessService { throw new BusinessException("订单保存失败"); } - // 5. 创建微信支付订单 + // 5. 保存订单商品 + saveOrderGoods(request, shopOrder); + + // 6. 创建微信支付订单 try { return shopOrderService.createWxOrder(shopOrder); } catch (Exception e) { @@ -73,19 +88,92 @@ public class OrderBusinessService { 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"); } + // 检查前端传入的总金额是否正确(允许小的误差,比如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()); if (tenantRule != null && tenantRule.getMinAmount() != null) { - if (request.getTotalPrice().compareTo(tenantRule.getMinAmount()) < 0) { + if (calculatedTotal.compareTo(tenantRule.getMinAmount()) < 0) { 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 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()); + } + /** * 检查是否为测试账号 */ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4d94f34..ec4af5a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -135,6 +135,7 @@ shop: # 默认配置 default-config: + default-title: "订单名称" default-comments: "暂无" min-order-amount: 0 order-timeout-minutes: 30 diff --git a/src/test/java/com/gxwebsoft/shop/OrderBusinessServiceTest.java b/src/test/java/com/gxwebsoft/shop/OrderBusinessServiceTest.java new file mode 100644 index 0000000..462282c --- /dev/null +++ b/src/test/java/com/gxwebsoft/shop/OrderBusinessServiceTest.java @@ -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 wxOrderInfo = new HashMap<>(); + wxOrderInfo.put("prepay_id", "test_prepay_id"); + when(shopOrderService.createWxOrder(any(ShopOrder.class))).thenReturn(wxOrderInfo); + + // 执行测试 + Map 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) list).size() == 2 + )); + } +} diff --git a/src/test/java/com/gxwebsoft/shop/OrderValidationTest.java b/src/test/java/com/gxwebsoft/shop/OrderValidationTest.java new file mode 100644 index 0000000..aa1f473 --- /dev/null +++ b/src/test/java/com/gxwebsoft/shop/OrderValidationTest.java @@ -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("订单金额计算错误")); + } +} diff --git a/下单流程图.svg b/下单流程图.svg new file mode 100644 index 0000000..6dfab6c --- /dev/null +++ b/下单流程图.svg @@ -0,0 +1 @@ +

前端提交订单请求

validateOrderRequest

用户是否登录?

抛出异常: 用户未登录

validateAndCalculateTotal

遍历商品列表

根据商品ID查询商品信息

商品是否存在?

抛出异常: 商品不存在

商品是否上架?

抛出异常: 商品已下架

库存是否充足?

抛出异常: 库存不足

是否超过购买限制?

抛出异常: 超过购买限制

计算商品小计

累加到总金额

还有其他商品?

返回计算的总金额

前端金额与后台计算是否一致?

抛出异常: 金额计算错误

使用后台计算的金额

检查租户规则

验证通过

构建订单对象

保存订单

saveOrderGoods

重新验证商品状态

使用后台数据保存订单商品

订单创建成功

\ No newline at end of file