Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java
#	src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java
This commit is contained in:
2025-08-09 14:46:00 +08:00
149 changed files with 6425 additions and 5835 deletions

View File

@@ -0,0 +1,187 @@
# 百色中学订单总金额统计功能实现文档
## 功能概述
参考ShopOrderController的total方法完善了BszxOrderController中的订单总金额统计功能提供REST API接口用于统计百色中学所有捐款记录的总金额。
## API接口
### 统计订单总金额
**接口地址**: `GET /api/bszx/bszx-order/total`
**接口描述**: 统计百色中学所有捐款记录的总金额
**请求参数**: 无
**响应格式**:
```json
{
"code": 200,
"message": "操作成功",
"data": 12345.67
}
```
**响应说明**:
- `data`: BigDecimal类型表示捐款总金额
- 统计所有捐款记录bszx_pay表中的price字段
- 使用COALESCE函数处理空值确保返回值不为null
## 实现细节
### 1. 接口层 (Controller)
**文件**: `BszxOrderController.java`
```java
@Operation(summary = "统计订单总金额")
@GetMapping("/total")
public ApiResult<BigDecimal> total() {
try {
BigDecimal totalAmount = bszxPayService.total();
return success(totalAmount);
} catch (Exception e) {
// 异常时返回0保持接口稳定性
return success(BigDecimal.ZERO);
}
}
```
### 2. 服务层 (Service)
**接口定义** (`BszxPayService.java`):
```java
/**
* 统计捐款总金额
*
* @return 捐款总金额
*/
BigDecimal total();
```
**实现类** (`BszxPayServiceImpl.java`):
```java
@Override
public BigDecimal total() {
try {
// 使用数据库聚合查询统计捐款总金额,性能更高
LambdaQueryWrapper<BszxPay> wrapper = new LambdaQueryWrapper<>();
BigDecimal total = baseMapper.selectSumMoney(wrapper);
if (total == null) {
total = BigDecimal.ZERO;
}
return total;
} catch (Exception e) {
// 异常时返回0确保接口稳定性
return BigDecimal.ZERO;
}
}
```
### 3. 数据访问层 (Mapper)
**Mapper接口** (`BszxPayMapper.java`):
```java
BigDecimal selectSumMoney(@Param("ew") Wrapper<?> wrapper);
```
**XML映射** (`BszxPayMapper.xml`):
```xml
<!-- 统计金额总和 -->
<select id="selectSumMoney" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(price), 0) as total_money
FROM bszx_pay
<if test="ew != null">
${ew.customSqlSegment}
</if>
</select>
```
## 与ShopOrderController的对比
| 特性 | ShopOrderController | BszxOrderController |
|------|-------------------|-------------------|
| 统计字段 | pay_price | price |
| 过滤条件 | pay_status = 1 AND deleted = 0 | 无特殊过滤 |
| 数据表 | shop_order | bszx_pay |
| 业务场景 | 商城已支付订单 | 百色中学捐款记录 |
| 异常处理 | ✓ | ✓ |
| 空值处理 | ✓ | ✓ |
## 统计规则
1. **全量统计**: 统计bszx_pay表中所有记录的price字段总和
2. **空值处理**: 使用COALESCE函数当没有记录时返回0
3. **异常处理**: 包含完整的异常处理机制,确保接口稳定性
4. **性能优化**: 使用数据库聚合查询,在数据库层面进行计算
## 性能优化
1. **数据库聚合**: 使用SQL的SUM函数在数据库层面进行聚合计算
2. **复用现有方法**: 复用了已有的selectSumMoney方法避免重复开发
3. **异常处理**: 包含完整的异常处理机制,确保接口稳定性
4. **索引建议**: 如果数据量大建议在price字段上创建索引
## 测试用例
创建了测试类 `BszxOrderTotalTest.java` 用于验证功能:
```java
@Test
void testBszxOrderTotal() {
BigDecimal total = bszxPayService.total();
assertNotNull(total, "百色中学订单总金额不应该为null");
assertTrue(total.compareTo(BigDecimal.ZERO) >= 0, "百色中学订单总金额应该大于等于0");
}
@Test
void testBszxOrderTotalPerformance() {
long startTime = System.currentTimeMillis();
BigDecimal total = bszxPayService.total();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
assertTrue(duration < 5000, "查询时间应该在5秒以内");
}
```
## 使用示例
### 前端调用示例
```javascript
// 获取百色中学订单总金额
fetch('/api/bszx/bszx-order/total')
.then(response => response.json())
.then(data => {
if (data.code === 200) {
console.log('百色中学订单总金额:', data.data);
}
});
```
### cURL调用示例
```bash
curl -X GET "http://localhost:8080/api/bszx/bszx-order/total" \
-H "Content-Type: application/json"
```
## 注意事项
1. **数据精度**: 使用BigDecimal确保金额计算的精度
2. **并发安全**: 查询操作是只读的,天然支持并发访问
3. **业务逻辑**: 与商城订单不同,百色中学捐款记录不需要过滤支付状态
4. **扩展性**: 可以通过传入不同的查询条件实现更复杂的统计需求
## 扩展功能建议
1. **按时间范围统计**: 支持指定时间范围的捐款金额统计
2. **按项目统计**: 支持按form_id进行分组统计
3. **按用户统计**: 支持统计不同用户的捐款总额
4. **缓存机制**: 对于大数据量场景可以添加Redis缓存
5. **权限控制**: 根据业务需要可以添加相应的权限控制注解

View File

@@ -0,0 +1,191 @@
# 优惠券功能使用指南
## 功能概述
本系统实现了完整的优惠券功能,包括优惠券模板管理、用户优惠券管理、优惠券使用、统计分析等功能。
## 核心功能
### 1. 优惠券模板管理
- 创建优惠券模板(满减券、折扣券、免费券)
- 设置发放数量限制和个人领取限制
- 配置适用范围(全部商品、指定商品、指定分类)
- 设置有效期(领取后生效或固定时间)
### 2. 用户优惠券管理
- 用户主动领取优惠券
- 系统自动发放优惠券
- 优惠券使用和退还
- 优惠券状态管理(未使用、已使用、已过期)
### 3. 订单优惠券功能
- 获取订单可用优惠券
- 计算优惠券优惠金额
- 验证优惠券适用性
- 推荐最优优惠券组合
### 4. 业务场景支持
- 新用户注册赠送
- 生日优惠券发放
- 消费返券
- 活动批量发放
## 数据库表结构
### 优惠券模板表 (shop_coupon)
```sql
- id: 主键
- name: 优惠券名称
- description: 优惠券描述
- type: 优惠券类型(10满减券 20折扣券 30免费券)
- reduce_price: 满减金额
- discount: 折扣率(0-100)
- min_price: 最低消费金额
- total_count: 发放总数量(-1无限制)
- issued_count: 已发放数量
- limit_per_user: 每人限领数量(-1无限制)
- expire_type: 到期类型(10领取后生效 20固定时间)
- expire_day: 有效天数
- start_time: 有效期开始时间
- end_time: 有效期结束时间
- apply_range: 适用范围(10全部商品 20指定商品 30指定分类)
- apply_range_config: 适用范围配置(JSON格式)
- enabled: 是否启用
- status: 状态
```
### 用户优惠券表 (shop_user_coupon)
```sql
- id: 主键
- coupon_id: 优惠券模板ID
- user_id: 用户ID
- name: 优惠券名称
- type: 优惠券类型
- reduce_price: 满减金额
- discount: 折扣率
- min_price: 最低消费金额
- start_time: 有效期开始时间
- end_time: 有效期结束时间
- status: 使用状态(0未使用 1已使用 2已过期)
- use_time: 使用时间
- order_id: 使用订单ID
- order_no: 使用订单号
- obtain_type: 获取方式(10主动领取 20系统发放 30活动赠送)
- obtain_source: 获取来源描述
```
## API接口说明
### 优惠券模板管理
- `GET /api/shop/shop-coupon/page` - 分页查询优惠券模板
- `POST /api/shop/shop-coupon` - 创建优惠券模板
- `PUT /api/shop/shop-coupon` - 更新优惠券模板
- `DELETE /api/shop/shop-coupon/{id}` - 删除优惠券模板
### 用户优惠券管理
- `GET /api/shop/user-coupon/my` - 获取当前用户优惠券
- `GET /api/shop/user-coupon/my/available` - 获取可用优惠券
- `POST /api/shop/user-coupon/receive/{couponId}` - 领取优惠券
- `PUT /api/shop/user-coupon/use` - 使用优惠券
- `PUT /api/shop/user-coupon/return/{orderId}` - 退还优惠券
### 优惠券业务功能
- `POST /api/shop/coupon-business/available-for-order` - 获取订单可用优惠券
- `POST /api/shop/coupon-business/calculate-order-amount` - 计算使用优惠券后的订单金额
- `POST /api/shop/coupon-business/recommend-best-combination` - 推荐最优优惠券组合
- `POST /api/shop/coupon-business/batch-issue-activity` - 批量发放活动优惠券
## 使用示例
### 1. 创建优惠券模板
```json
{
"name": "新用户专享券",
"description": "新用户注册即可领取",
"type": 10,
"reducePrice": 20.00,
"minPrice": 100.00,
"totalCount": 1000,
"limitPerUser": 1,
"expireType": 10,
"expireDay": 30,
"applyRange": 10,
"enabled": 1
}
```
### 2. 用户领取优惠券
```javascript
// 前端调用
POST /api/shop/user-coupon/receive/1
```
### 3. 订单使用优惠券
```json
{
"goodsItems": [
{
"goodsId": 1,
"categoryId": 1,
"price": 150.00,
"quantity": 1
}
],
"totalAmount": 150.00
}
```
### 4. 计算优惠金额
```javascript
// 获取可用优惠券
POST /api/shop/coupon-business/available-for-order
// 计算优惠金额
POST /api/shop/coupon-business/calculate-order-amount?userCouponId=1
```
## 定时任务
系统包含以下定时任务:
1. **过期优惠券处理** - 每天凌晨2点执行
- 自动更新过期优惠券状态
2. **优惠券到期提醒** - 每天上午10点执行
- 提醒用户即将过期的优惠券
3. **生日优惠券发放** - 每天凌晨1点执行
- 为当天生日的用户发放生日优惠券
## 配置说明
### 优惠券类型配置
- `TYPE_REDUCE = 10` - 满减券
- `TYPE_DISCOUNT = 20` - 折扣券
- `TYPE_FREE = 30` - 免费券
### 适用范围配置
- `APPLY_ALL = 10` - 全部商品
- `APPLY_GOODS = 20` - 指定商品
- `APPLY_CATEGORY = 30` - 指定分类
### 获取方式配置
- `OBTAIN_RECEIVE = 10` - 主动领取
- `OBTAIN_SYSTEM = 20` - 系统发放
- `OBTAIN_ACTIVITY = 30` - 活动赠送
## 注意事项
1. **数据一致性**:优惠券使用和退还操作需要保证数据一致性
2. **并发控制**:优惠券领取需要考虑并发情况,避免超发
3. **性能优化**:大量用户时需要考虑查询性能优化
4. **业务规则**:根据实际业务需求调整优惠券规则和限制
## 扩展功能
可以根据业务需要扩展以下功能:
- 优惠券分享功能
- 优惠券兑换码功能
- 优惠券组合使用
- 优惠券使用统计分析
- 优惠券营销活动管理

View File

@@ -0,0 +1,149 @@
# 订单数据库字段缺失默认值修复指南
## 问题描述
在提交订单时遇到以下数据库错误:
```
{"code":1,"message":"\n### Error updating database. Cause: java.sql.SQLException: Field 'pay_price' doesn't have a default value\n### The error may exist in com/gxwebsoft/shop/mapper/ShopOrderMapper.java (best guess)\n### The error may involve com.gxwebsoft.shop.mapper.ShopOrderMapper.insert-Inline\n### The error occurred while setting parameters\n### SQL: INSERT INTO shop_order (order_no, delivery_type, address_id, total_price, price, pay_user_id, pay_type, pay_status, user_id, comments, tenant_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 10550)\n### Cause: java.sql.SQLException: Field 'pay_price' doesn't have a default value\n; Field 'pay_price' doesn't have a default value; nested exception is java.sql.SQLException: Field 'pay_price' doesn't have a default value"}
```
## 根本原因
数据库表 `shop_order` 中的 `pay_price` 字段没有设置默认值,而在插入订单记录时没有为该字段提供值,导致 SQL 插入失败。
## 解决方案
### 1. 修改 `buildShopOrder` 方法
`OrderBusinessService.buildShopOrder()` 方法中添加了所有必需字段的默认值设置:
```java
// 设置价格相关字段(解决数据库字段没有默认值的问题)
if (shopOrder.getPayPrice() == null) {
shopOrder.setPayPrice(shopOrder.getTotalPrice()); // 实际付款默认等于订单总额
}
if (shopOrder.getPrice() == null) {
shopOrder.setPrice(shopOrder.getTotalPrice()); // 用于统计的价格默认等于订单总额
}
if (shopOrder.getReducePrice() == null) {
shopOrder.setReducePrice(BigDecimal.ZERO); // 减少金额默认为0
}
if (shopOrder.getMoney() == null) {
shopOrder.setMoney(shopOrder.getTotalPrice()); // 用于积分赠送的价格默认等于订单总额
}
// 设置默认状态
shopOrder.setPayStatus(false); // 未付款
shopOrder.setOrderStatus(0); // 未使用
shopOrder.setDeliveryStatus(10); // 未发货
shopOrder.setIsInvoice(0); // 未开发票
shopOrder.setIsSettled(0); // 未结算
shopOrder.setCheckBill(0); // 未对账
shopOrder.setVersion(0); // 当前版本
// 设置默认支付类型(如果没有指定)
if (shopOrder.getPayType() == null) {
shopOrder.setPayType(1); // 默认微信支付
}
```
### 2. 修改 `applyBusinessRules` 方法
确保测试账号逻辑也正确设置所有相关字段:
```java
// 测试账号处理
if (orderConfig.isTestAccount(loginUser.getPhone())) {
BigDecimal testAmount = orderConfig.getTestAccount().getTestPayAmount();
shopOrder.setPrice(testAmount);
shopOrder.setTotalPrice(testAmount);
shopOrder.setPayPrice(testAmount); // 确保实际付款也设置为测试金额
shopOrder.setMoney(testAmount); // 确保积分计算金额也设置为测试金额
log.info("应用测试账号规则,用户:{},测试金额:{}", loginUser.getPhone(), testAmount);
}
```
## 修复的字段列表
| 字段名 | 默认值 | 说明 |
|--------|--------|------|
| `payPrice` | `totalPrice` | 实际付款金额,默认等于订单总额 |
| `price` | `totalPrice` | 用于统计的价格,默认等于订单总额 |
| `reducePrice` | `BigDecimal.ZERO` | 减少的金额(优惠券、折扣等) |
| `money` | `totalPrice` | 用于积分赠送的价格 |
| `payStatus` | `false` | 支付状态,默认未付款 |
| `orderStatus` | `0` | 订单状态,默认未使用 |
| `deliveryStatus` | `10` | 发货状态,默认未发货 |
| `isInvoice` | `0` | 发票状态,默认未开发票 |
| `isSettled` | `0` | 结算状态,默认未结算 |
| `checkBill` | `0` | 对账状态,默认未对账 |
| `version` | `0` | 系统版本,默认当前版本 |
| `payType` | `1` | 支付类型,默认微信支付 |
## 测试验证
创建了专门的测试用例 `testBuildShopOrder_RequiredFields` 来验证所有必需字段都正确设置:
```java
@Test
void testBuildShopOrder_RequiredFields() throws Exception {
// 验证必需字段都已设置
assertNotNull(result.getPayPrice(), "payPrice 不能为空");
assertNotNull(result.getPrice(), "price 不能为空");
assertNotNull(result.getReducePrice(), "reducePrice 不能为空");
assertNotNull(result.getMoney(), "money 不能为空");
// ... 其他字段验证
// 验证默认值
assertEquals(testRequest.getTotalPrice(), result.getPayPrice());
assertEquals(testRequest.getTotalPrice(), result.getPrice());
assertEquals(BigDecimal.ZERO, result.getReducePrice());
// ... 其他默认值验证
}
```
## 业务逻辑说明
### 价格字段关系
1. **totalPrice**: 订单总额,由商品价格计算得出
2. **payPrice**: 实际付款金额,通常等于 totalPrice但可能因优惠而不同
3. **price**: 用于统计的价格,通常等于 totalPrice
4. **money**: 用于积分赠送计算的价格
5. **reducePrice**: 优惠减免的金额优惠券、VIP折扣等
### 计算公式
```
payPrice = totalPrice - reducePrice
```
在没有优惠的情况下:
```
payPrice = totalPrice
reducePrice = 0
```
## 影响范围
这个修复解决了以下问题:
1.**数据库插入错误**: 解决了 `pay_price` 等字段缺少默认值的问题
2.**订单状态完整性**: 确保所有状态字段都有正确的初始值
3.**价格计算一致性**: 保证各个价格字段的逻辑关系正确
4.**测试账号兼容性**: 确保测试账号逻辑正常工作
## 注意事项
1. **向后兼容**: 修改保持了向后兼容性,不会影响现有功能
2. **数据完整性**: 所有必需字段都有合理的默认值
3. **业务逻辑**: 默认值符合业务逻辑,不会产生异常数据
4. **测试覆盖**: 有完整的测试用例覆盖修改的功能
## 总结
通过在 `buildShopOrder` 方法中添加完整的字段默认值设置,成功解决了订单提交时的数据库字段缺失问题。这个修复不仅解决了当前的错误,还提高了系统的健壮性,确保订单数据的完整性和一致性。

View 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. 支持优惠券和折扣计算

View File

@@ -0,0 +1,163 @@
# 订单总金额统计功能实现文档
## 功能概述
实现了订单总金额统计功能提供REST API接口用于统计所有已支付订单的总金额。
## API接口
### 统计订单总金额
**接口地址**: `GET /api/shop/shop-order/total`
**接口描述**: 统计所有已支付订单的总金额
**请求参数**: 无
**响应格式**:
```json
{
"code": 200,
"message": "操作成功",
"data": 12345.67
}
```
**响应说明**:
- `data`: BigDecimal类型表示订单总金额
- 只统计已支付的订单pay_status = 1
- 排除已删除的订单deleted = 0
- 使用实际付款金额pay_price字段进行统计
## 实现细节
### 1. 接口层 (Controller)
```java
@Operation(summary = "统计订单总金额")
@GetMapping("/total")
public ApiResult<BigDecimal> total() {
return success(shopOrderService.total());
}
```
### 2. 服务层 (Service)
**接口定义** (`ShopOrderService.java`):
```java
/**
* 统计订单总金额
*
* @return 订单总金额
*/
BigDecimal total();
```
**实现类** (`ShopOrderServiceImpl.java`):
```java
@Override
public BigDecimal total() {
try {
// 使用数据库聚合查询统计订单总金额,性能更高
BigDecimal total = baseMapper.selectTotalAmount();
if (total == null) {
total = BigDecimal.ZERO;
}
log.info("统计订单总金额完成,总金额:{}", total);
return total;
} catch (Exception e) {
log.error("统计订单总金额失败", e);
return BigDecimal.ZERO;
}
}
```
### 3. 数据访问层 (Mapper)
**Mapper接口** (`ShopOrderMapper.java`):
```java
/**
* 统计订单总金额
* 只统计已支付的订单pay_status = 1且未删除的订单deleted = 0
*
* @return 订单总金额
*/
@Select("SELECT COALESCE(SUM(pay_price), 0) FROM shop_order WHERE pay_status = 1 AND deleted = 0 AND pay_price IS NOT NULL")
BigDecimal selectTotalAmount();
```
## 统计规则
1. **已支付订单**: 只统计 `pay_status = 1` 的订单
2. **未删除订单**: 排除 `deleted = 1` 的订单
3. **有效金额**: 排除 `pay_price IS NULL` 的记录
4. **使用实际付款**: 统计 `pay_price` 字段而不是 `total_price`
5. **空值处理**: 使用 `COALESCE` 函数,当没有符合条件的记录时返回 0
## 性能优化
1. **数据库聚合**: 使用SQL的SUM函数在数据库层面进行聚合计算
2. **索引优化**: 建议在 `pay_status``deleted` 字段上创建索引
3. **异常处理**: 包含完整的异常处理机制,确保接口稳定性
## 测试用例
创建了测试类 `OrderTotalTest.java` 用于验证功能:
```java
@Test
void testOrderTotal() {
BigDecimal total = shopOrderService.total();
assertNotNull(total, "订单总金额不应该为null");
assertTrue(total.compareTo(BigDecimal.ZERO) >= 0, "订单总金额应该大于等于0");
}
@Test
void testOrderTotalPerformance() {
long startTime = System.currentTimeMillis();
BigDecimal total = shopOrderService.total();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
assertTrue(duration < 5000, "查询时间应该在5秒以内");
}
```
## 使用示例
### 前端调用示例
```javascript
// 获取订单总金额
fetch('/api/shop/shop-order/total')
.then(response => response.json())
.then(data => {
if (data.code === 200) {
console.log('订单总金额:', data.data);
}
});
```
### cURL调用示例
```bash
curl -X GET "http://localhost:8080/api/shop/shop-order/total" \
-H "Content-Type: application/json"
```
## 注意事项
1. **数据精度**: 使用BigDecimal确保金额计算的精度
2. **并发安全**: 查询操作是只读的,天然支持并发访问
3. **缓存考虑**: 如果数据量很大且实时性要求不高,可以考虑添加缓存
4. **权限控制**: 根据业务需要可以添加相应的权限控制注解
## 扩展功能建议
1. **按时间范围统计**: 支持指定时间范围的订单金额统计
2. **按商户统计**: 支持按商户ID进行分组统计
3. **按订单状态统计**: 支持统计不同状态订单的金额
4. **缓存机制**: 对于大数据量场景可以添加Redis缓存

View 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 | 商品不存在商品IDxxx | 商品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. **可维护性**:清晰的代码结构和完整的测试覆盖
这种方案比前端传递商品信息更安全可靠,是电商系统的最佳实践。

View File

@@ -1,7 +1,7 @@
# 服务器URL配置重构总结
## 概述
将项目中硬编码的服务器地址 `https://server.gxwebsoft.com/api` 改为从配置文件读取,提高了代码的可维护性和灵活性。
将项目中硬编码的服务器地址 `https://server.websoft.top/api` 改为从配置文件读取,提高了代码的可维护性和灵活性。
## 修改的文件
@@ -31,7 +31,7 @@
**文件路径**: `src/main/java/com/gxwebsoft/common/core/security/JwtAuthenticationFilter.java`
**修改内容**:
- 将硬编码的URL `"https://server.gxwebsoft.com/api/auth/user"`
- 将硬编码的URL `"https://server.websoft.top/api/auth/user"`
- 改为 `configProperties.getServerUrl() + "/auth/user"`
### 3. OaAppController.java
@@ -39,21 +39,21 @@
**修改内容**:
- 添加了 `ConfigProperties` 依赖注入
- 将硬编码的URL `"https://server.gxwebsoft.com/api/file/page"`
- 将硬编码的URL `"https://server.websoft.top/api/file/page"`
- 改为 `configProperties.getServerUrl() + "/file/page"`
### 4. SwaggerConfig.java
**文件路径**: `src/main/java/com/gxwebsoft/common/core/config/SwaggerConfig.java`
**修改内容**:
- 将硬编码的URL `"https://server.gxwebsoft.com/api/system"`
- 将硬编码的URL `"https://server.websoft.top/api/system"`
- 改为 `config.getServerUrl() + "/system"`
### 5. WxOfficialUtil.java
**文件路径**: `src/main/java/com/gxwebsoft/common/core/utils/WxOfficialUtil.java`
**修改内容**:
- 将硬编码的URL `"https://server.gxwebsoft.com/api/open/wx-official/accessToken"`
- 将硬编码的URL `"https://server.websoft.top/api/open/wx-official/accessToken"`
- 改为 `pathConfig.getServerUrl() + "/open/wx-official/accessToken"`
### 6. ShopOrderServiceImpl.java
@@ -61,7 +61,7 @@
**修改内容**:
- 将微信支付回调地址中的硬编码URL
-`"https://server.gxwebsoft.com/api/system/wx-pay/notify/"`
-`"https://server.websoft.top/api/system/wx-pay/notify/"`
- 改为 `config.getServerUrl() + "/system/wx-pay/notify/"`
## 配置文件设置
@@ -75,13 +75,13 @@ config:
### 生产环境 (application-prod.yml)
```yaml
config:
server-url: https://server.gxwebsoft.com/api
server-url: https://server.websoft.top/api
```
### 默认配置 (application.yml)
```yaml
config:
server-url: https://server.gxwebsoft.com/api
server-url: https://server.websoft.top/api
```
## 优势

View File

@@ -0,0 +1,110 @@
# 商城订单状态筛选功能修复报告
## 问题描述
在调用商城订单分页查询API时`statusFilter`查询条件没有生效,导致无法按订单状态进行筛选。
**问题API**: `GET /api/shop/shop-order/page?statusFilter=3&page=1&limit=10`
## 问题分析
通过代码分析发现:
1. **参数定义正确**: 在`ShopOrderParam.java`中已正确定义了`statusFilter`参数
```java
@Schema(description = "订单状态筛选:-1全部0待支付1待发货2待核销3待收货4待评价5已完成6已退款7已删除")
private Integer statusFilter;
```
2. **SQL映射缺失**: 在`ShopOrderMapper.xml`的SQL查询中缺少对`statusFilter`参数的处理逻辑
## 解决方案
在`src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml`文件中添加了`statusFilter`的SQL处理逻辑
```xml
<!-- 订单状态筛选:-1全部0待支付1待发货2待核销3待收货4待评价5已完成6已退款7已删除 -->
<if test="param.statusFilter != null and param.statusFilter != -1">
<if test="param.statusFilter == 0">
<!-- 0待支付未付款 -->
AND a.pay_status = 0
</if>
<if test="param.statusFilter == 1">
<!-- 1待发货已付款但未发货 -->
AND a.pay_status = 1 AND a.delivery_status = 10
</if>
<if test="param.statusFilter == 2">
<!-- 2待核销已付款但订单状态为未使用 -->
AND a.pay_status = 1 AND a.order_status = 0
</if>
<if test="param.statusFilter == 3">
<!-- 3待收货已发货但订单状态不是已完成 -->
AND a.delivery_status = 20 AND a.order_status != 1
</if>
<if test="param.statusFilter == 4">
<!-- 4待评价订单已完成但可能需要评价 -->
AND a.order_status = 1
</if>
<if test="param.statusFilter == 5">
<!-- 5已完成订单状态为已完成 -->
AND a.order_status = 1
</if>
<if test="param.statusFilter == 6">
<!-- 6已退款订单状态为退款成功 -->
AND a.order_status = 6
</if>
<if test="param.statusFilter == 7">
<!-- 7已删除订单被删除 -->
AND a.deleted = 1
</if>
</if>
```
## 状态映射说明
根据数据库字段定义,状态筛选的映射关系如下:
| statusFilter | 含义 | SQL条件 |
|-------------|------|---------|
| -1 | 全部 | 无额外条件 |
| 0 | 待支付 | `pay_status = 0` |
| 1 | 待发货 | `pay_status = 1 AND delivery_status = 10` |
| 2 | 待核销 | `pay_status = 1 AND order_status = 0` |
| 3 | 待收货 | `delivery_status = 20 AND order_status != 1` |
| 4 | 待评价 | `order_status = 1` |
| 5 | 已完成 | `order_status = 1` |
| 6 | 已退款 | `order_status = 6` |
| 7 | 已删除 | `deleted = 1` |
## 测试验证
修复后进行了以下测试:
1. **statusFilter=3**: 查询待收货订单 ✅
2. **statusFilter=0**: 查询待支付订单 ✅
3. **statusFilter=-1**: 查询全部订单 ✅
4. **不传statusFilter**: 正常查询 ✅
所有测试均返回正确的JSON响应格式
```json
{
"code": 0,
"message": "操作成功",
"data": {
"list": [],
"count": 0
}
}
```
## 修复文件
- `src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml`
## 影响范围
此修复仅影响商城订单的状态筛选功能,不会对其他功能造成影响。
## 部署说明
修复已应用到运行时环境,无需重启应用即可生效。

163
docs/TENANT_ID_FIX.md Normal file
View File

@@ -0,0 +1,163 @@
# 租户ID传递问题修复指南
## 问题描述
在订单创建过程中出现微信支付证书路径错误:
```
message: "创建支付订单失败创建支付订单失败构建微信支付服务失败证书加载失败dev/wechat/null/apiclient_key.pem"
```
## 问题分析
### 根本原因
证书路径中出现了 `null`,说明 `tenantId` 在传递过程中丢失了。微信支付服务构建证书路径的逻辑是:
```java
String tenantCertPath = "dev/wechat/" + order.getTenantId();
String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile();
```
`order.getTenantId()` 返回 `null` 时,路径就变成了 `dev/wechat/null/apiclient_key.pem`
### 影响范围
- ❌ 微信支付证书加载失败
- ❌ 订单支付功能无法正常工作
- ❌ 所有依赖租户ID的功能可能受影响
## 解决方案
### 1. 修改 `buildShopOrder` 方法
`OrderBusinessService.buildShopOrder()` 方法中添加了租户ID的验证和保护逻辑
```java
private ShopOrder buildShopOrder(OrderCreateRequest request, User loginUser) {
ShopOrder shopOrder = new ShopOrder();
// 复制请求参数到订单对象
BeanUtils.copyProperties(request, shopOrder);
// 确保租户ID正确设置关键字段影响微信支付证书路径
if (shopOrder.getTenantId() == null && request.getTenantId() != null) {
shopOrder.setTenantId(request.getTenantId());
log.warn("租户ID未正确复制手动设置为{}", request.getTenantId());
}
// 验证关键字段
if (shopOrder.getTenantId() == null) {
throw new BusinessException("租户ID不能为空这会导致微信支付证书路径错误");
}
// 设置用户相关信息
shopOrder.setUserId(loginUser.getUserId());
shopOrder.setOpenid(loginUser.getOpenid());
shopOrder.setPayUserId(loginUser.getUserId());
log.debug("构建订单对象 - 租户ID{}用户ID{}", shopOrder.getTenantId(), shopOrder.getUserId());
// ... 其他设置
}
```
### 2. 添加防护机制
#### 2.1 早期验证
在订单构建阶段就验证租户ID避免在支付阶段才发现问题。
#### 2.2 明确的错误提示
当租户ID为空时抛出明确的业务异常说明问题的影响。
#### 2.3 日志记录
添加调试日志,便于排查问题。
### 3. 测试验证
添加了专门的测试用例来验证租户ID的处理
```java
@Test
void testBuildShopOrder_TenantIdValidation() throws Exception {
// 创建租户ID为空的请求
OrderCreateRequest requestWithoutTenant = new OrderCreateRequest();
requestWithoutTenant.setTenantId(null);
// 执行验证 - 应该抛出异常
Exception exception = assertThrows(Exception.class, () -> {
buildMethod.invoke(orderBusinessService, requestWithoutTenant, testUser);
});
// 验证异常类型和消息
assertTrue(cause instanceof BusinessException);
assertTrue(cause.getMessage().contains("租户ID不能为空"));
}
```
## 可能的原因分析
### 1. BeanUtils.copyProperties 问题
`BeanUtils.copyProperties` 在某些情况下可能不会正确复制字段:
- 字段类型不匹配
- 字段名称不一致
- 源对象字段为 null
### 2. 前端传递问题
前端可能没有正确传递 `tenantId` 字段:
- 请求参数缺失
- JSON 序列化问题
- 字段映射错误
### 3. 数据验证问题
虽然 `OrderCreateRequest` 中有 `@NotNull` 验证,但可能:
- 验证没有生效
- 验证在错误的时机执行
- 验证被绕过
## 修复效果
### ✅ 问题解决
1. **租户ID保护**: 确保租户ID不会丢失
2. **早期发现**: 在订单构建阶段就发现问题
3. **明确错误**: 提供清晰的错误信息
4. **日志追踪**: 便于问题排查
### ✅ 证书路径修复
修复后的证书路径将是正确的格式:
```
dev/wechat/{实际租户ID}/apiclient_key.pem
```
而不是:
```
dev/wechat/null/apiclient_key.pem
```
## 预防措施
### 1. 代码层面
- 在关键方法中验证必需字段
- 使用明确的字段设置而不完全依赖 BeanUtils
- 添加详细的日志记录
### 2. 测试层面
- 添加边界条件测试
- 验证字段传递的完整性
- 测试异常情况的处理
### 3. 监控层面
- 监控租户ID为空的情况
- 记录证书路径构建的详细信息
- 设置告警机制
## 总结
通过在 `buildShopOrder` 方法中添加租户ID的验证和保护逻辑我们解决了微信支付证书路径中出现 `null` 的问题。这个修复不仅解决了当前的支付问题,还提高了系统的健壮性,确保了关键字段的正确传递。
### 关键改进
1.**租户ID验证**: 确保不为空
2.**手动设置**: 当 BeanUtils 复制失败时的备用方案
3.**明确异常**: 提供有意义的错误信息
4.**日志记录**: 便于问题排查
5.**测试覆盖**: 验证修复的有效性
现在订单创建时应该不会再出现 `dev/wechat/null/apiclient_key.pem` 的错误了!

131
docs/price-sort-fix.md Normal file
View File

@@ -0,0 +1,131 @@
# 房源价格排序Bug修复文档
## 问题描述
API接口 `https://cms-api.websoft.top/api/house/house-info/page?status=0&page=1&sortScene=%E4%BB%B7%E6%A0%BC(%E4%BD%8E-%E9%AB%98)` 中的价格从低到高排序功能失效。
URL参数 `%E4%BB%B7%E6%A0%BC(%E4%BD%8E-%E9%AB%98)` 解码后为 `价格(低-高)`,但排序功能不生效。
## 问题分析
1. **URL编码问题**: 前端传递的中文参数经过URL编码后端可能没有正确解码
2. **字符串匹配问题**: MyBatis XML中的字符串比较可能存在编码或空格问题
3. **数据类型问题**: `monthly_rent` 字段可能存在NULL值或数据类型转换问题
## 解决方案
### 1. 创建排序场景工具类
创建了 `SortSceneUtil` 工具类来标准化排序参数:
```java
public class SortSceneUtil {
public static String normalizeSortScene(String sortScene) {
// URL解码 + 字符串标准化
// 支持多种格式的价格排序参数
}
}
```
**功能特点:**
- 自动URL解码中文参数
- 标准化排序场景字符串
- 支持多种格式的排序参数(如"低-高"、"低到高"、"升序"等)
- 提供便捷的判断方法
### 2. 修改Controller层
`HouseInfoController.page()` 方法中添加参数标准化:
```java
@GetMapping("/page")
public ApiResult<PageResult<HouseInfo>> page(HouseInfoParam param) {
// 标准化排序参数解决URL编码问题
if (param.getSortScene() != null) {
String normalizedSortScene = SortSceneUtil.normalizeSortScene(param.getSortScene());
param.setSortScene(normalizedSortScene);
}
return success(houseInfoService.pageRel(param));
}
```
### 3. 优化MyBatis XML映射
`HouseInfoMapper.xml` 中优化排序逻辑:
```xml
<if test="param.sortScene == '价格(低-高)'">
CASE WHEN a.monthly_rent IS NULL THEN 1 ELSE 0 END,
CAST(COALESCE(a.monthly_rent, 0) AS DECIMAL(10,2)) asc,
</if>
<if test="param.sortScene == '价格(高-低)'">
CASE WHEN a.monthly_rent IS NULL THEN 1 ELSE 0 END,
CAST(COALESCE(a.monthly_rent, 0) AS DECIMAL(10,2)) desc,
</if>
```
**优化点:**
- 使用 `CASE WHEN` 处理NULL值将NULL值排在最后
- 使用 `CAST` 确保数值类型正确转换
- 使用 `COALESCE` 处理NULL值默认为0
### 4. 创建单元测试
创建了 `SortSceneUtilTest` 测试类验证工具类功能:
```java
@Test
public void testNormalizeSortScene() {
// 测试URL编码参数
String urlEncoded = "%E4%BB%B7%E6%A0%BC(%E4%BD%8E-%E9%AB%98)";
String result = SortSceneUtil.normalizeSortScene(urlEncoded);
assertEquals("价格(低-高)", result);
}
```
## 修复效果
**问题已完全解决!**
1. **URL编码兼容**: 自动处理URL编码的中文参数
2. **字符串标准化**: 统一排序场景参数格式
3. **价格排序正常**: 价格从低到高、从高到低排序完全正常
4. **价格区间筛选**: 支持 `priceScene=3000~5000` 格式的价格区间筛选
5. **Service层修复**: 修复了Service层默认排序覆盖问题
6. **向后兼容**: 支持原有的排序参数格式
### 测试结果
- ✅ 价格从低到高排序:`sortScene=%E4%BB%B7%E6%A0%BC(%E4%BD%8E-%E9%AB%98)`
- ✅ 价格从高到低排序:`sortScene=%E4%BB%B7%E6%A0%BC(%E9%AB%98-%E4%BD%8E)`
- ✅ 价格区间筛选:`priceScene=3000~5000`
- ✅ 组合使用:排序 + 筛选同时生效
## 测试验证
可以通过以下URL测试修复效果
```bash
# 价格从低到高
curl "https://cms-api.websoft.top/api/house/house-info/page?sortScene=%E4%BB%B7%E6%A0%BC(%E4%BD%8E-%E9%AB%98)"
# 价格从高到低
curl "https://cms-api.websoft.top/api/house/house-info/page?sortScene=%E4%BB%B7%E6%A0%BC(%E9%AB%98-%E4%BD%8E)"
# 面积从小到大
curl "https://cms-api.websoft.top/api/house/house-info/page?sortScene=%E9%9D%A2%E7%A7%AF(%E5%B0%8F-%E5%A4%A7)"
```
## 相关文件
- `HouseInfoController.java` - 控制器层修改
- `HouseInfoMapper.xml` - MyBatis映射文件优化
- `SortSceneUtil.java` - 新增工具类
- `SortSceneUtilTest.java` - 单元测试
## 注意事项
1. 修改后需要重新编译和部署应用
2. 建议在测试环境先验证功能正常
3. 可以通过日志观察参数标准化过程
4. 如有其他排序场景需求,可扩展 `SortSceneUtil` 工具类