Compare commits

..

13 Commits

Author SHA1 Message Date
fde4deb370 Merge branch 'refs/heads/price_inquery'
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
2025-09-17 17:19:53 +08:00
5212547b3a docs 2025-09-17 17:18:48 +08:00
9a39592a98 Merge branch 'refs/heads/price_inquery' 2025-09-17 17:03:37 +08:00
f3fdb44742 refactor(mybatis): 移除 XML 配置,使用注解替代
- 在 PriceVoucherBatchConfigMapper、PriceVoucherCodeMapper 和 VoucherPrintRecordMapper 中添加了 @Select 和 @Update 注解
- 删除了对应的 XML 配置文件
- 优化了 SQL 查询,直接在 Java 接口中定义
2025-09-17 17:03:12 +08:00
ad111cdebb Merge branch 'page_info' into price_inquery 2025-09-17 16:54:59 +08:00
1c0c0393aa feat(pricing): 实现批次统计功能
- 查询批次配置和券码数据
- 统计每个券码的使用情况,包括状态、使用次数、剩余次数等信息
- 计算是否还能使用和剩余使用次数
- 获取使用记录数和最后使用时间
- 返回批次统计结果列表
2025-09-17 16:36:50 +08:00
04f7c79679 Merge branch 'refs/heads/page_info' 2025-09-17 16:22:26 +08:00
6d3fecc1c8 feat(AppClaimController): 优化优惠券领取结果展示
- 在 ClaimResp 中添加 couponType 字段,用于展示优惠券类型
-根据 CouponType 枚举值,设置不同的优惠券类型描述- 优化折扣优惠券和满减优惠券的描述生成逻辑- 保留原有的通用优惠券描述配置
2025-09-17 15:49:15 +08:00
5626498002 refactor(coupon): 重构优惠券领取结果封装
- 在 CouponClaimResult 类中添加 PriceCouponConfig 类型的 coupon 字段
- 修改 success 静态方法,接收 PriceCouponConfig 对象作为参数
- 更新方法内部逻辑,使用 coupon 对象替代单独的 couponName 字段
- 调整 CouponServiceImpl 中的代码,适应新的 CouponClaimResult 结构
2025-09-17 15:29:16 +08:00
8975ce404c feat(FaceServiceImpl): 实现人脸重复匹配逻辑
- 新增旅游时间和项目匹配逻辑
-增加识别次数、旅游匹配和项目匹配的规则判断
-根据不同匹配模式返回相应的结果
2025-09-17 15:13:39 +08:00
2a8bdaec28 feat(mapper): 添加获取用户项目 ID 列表的方法
- 在 StatisticsMapper 接口中新增 getProjectIdListForUser 方法
- 在 StatisticsMapper.xml 中添加对应的 SQL 查询语句
- 该方法用于获取用户在指定时间之前的项目 ID 列表
2025-09-17 15:13:17 +08:00
b323450708 refactor(paging): 重构分页查询使用 PageHelper
-将 MyBatis-Plus 的分页插件替换为 PageHelper
- 更新了相关控制器、服务接口和实现类中的分页查询方法
- 优化了分页查询的逻辑,提高了代码的可读性和维护性
2025-09-17 12:53:32 +08:00
a5e882e693 feat(basic): 添加视频更新检查功能
- 新增 VideoUpdateConfig 类用于配置视频更新检查参数
- 添加 VideoUpdateCheckVO 类作为视频更新检查响应模型
-功能包括检测片段变化、判断是否可更新以及统计片段数量等
2025-09-17 09:39:47 +08:00
27 changed files with 847 additions and 653 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.idea/ .idea/
logs/ logs/
target/ target/
.serena
.claude .claude
.vscode .vscode

269
CLAUDE.md
View File

@@ -21,11 +21,20 @@ mvn spring-boot:run
# 运行特定测试类 # 运行特定测试类
mvn test -Dtest=FaceCleanerTest mvn test -Dtest=FaceCleanerTest
# 运行特定测试方法
mvn test -Dtest=FaceCleanerTest#testSpecificMethod
# 运行特定包的测试 # 运行特定包的测试
mvn test -Dtest="com.ycwl.basic.storage.adapters.*Test" mvn test -Dtest="com.ycwl.basic.storage.adapters.*Test"
# 运行pricing模块测试
mvn test -Dtest="com.ycwl.basic.pricing.*Test"
# 运行所有测试 # 运行所有测试
mvn test -DskipTests=false mvn test -DskipTests=false
# 运行测试并生成详细报告
mvn test -DskipTests=false -Dsurefire.printSummary=true
``` ```
### 开发环境配置 ### 开发环境配置
@@ -134,12 +143,15 @@ mvn test -DskipTests=false
## 价格查询系统 (Pricing Module) ## 价格查询系统 (Pricing Module)
### 核心架构 ### 核心架构
价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理和价格计算功能。 价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理、券码管理和统一优惠检测功能。
#### 关键组件 #### 关键组件
- **PriceCalculationController** (`/api/pricing/calculate`):价格计算API - **PriceCalculationController** (`/api/pricing/calculate`):统一价格计算API,支持自动优惠组合
- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券管理API - **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券配置和统计管理
- **PricingConfigController** (`/api/pricing/config/`):价格配置管理API - **VoucherManagementController** (`/api/pricing/voucher/`):券码批次和券码管理
- **VoucherUsageController** (`/api/pricing/voucher/usage/`):券码使用记录和统计
- **PricingConfigController** (`/api/pricing/config/`):商品价格配置管理
- **OnePricePurchaseController** (`/api/pricing/admin/one-price/`):一口价配置管理
#### 商品类型支持 #### 商品类型支持
```java ```java
@@ -151,12 +163,16 @@ ProductType枚举定义了支持的商品类型:
- MACHINE_PRINT: 一体机打印 - MACHINE_PRINT: 一体机打印
``` ```
#### 价格计算流程 #### 价格计算流程(统一优惠检测)
1. 接收PriceCalculationRequest(包含商品列表用户ID) 1. 接收PriceCalculationRequest(包含商品列表用户ID、券码等
2. 查找商品基础配置和分层定价 2. 查找商品基础配置和分层定价
3. 处理套餐商品(BundleProductItem) 3. 处理套餐商品(BundleProductItem)
4. 自动应用最优优惠 4. **统一优惠检测**:通过IDiscountDetectionService自动检测所有可用优惠
5. 返回PriceCalculationResult(包含原价、最终价格、优惠详情 - 券码优惠(VoucherDiscountProvider,优先级100
- 优惠券优惠(CouponDiscountProvider,优先级80)
- 一口价优惠(OnePricePurchaseDiscountProvider,优先级60)
5. **智能优惠组合**:按优先级和叠加规则应用最优优惠组合
6. 返回PriceCalculationResult(包含原价、最终价格、使用的优惠详情、可用优惠列表)
#### 优惠券系统 #### 优惠券系统
- **CouponType**: PERCENTAGE(百分比)、FIXED_AMOUNT(固定金额) - **CouponType**: PERCENTAGE(百分比)、FIXED_AMOUNT(固定金额)
@@ -166,15 +182,93 @@ ProductType枚举定义了支持的商品类型:
- 时间有效期控制 - 时间有效期控制
#### 分页查询功能 #### 分页查询功能
所有管理接口都支持分页查询,使用PageHelper实现 所有管理接口都支持分页查询:
- 优惠券配置分页:支持按状态、名称筛选 - **优惠券系统**:使用PageHelper实现
- 领取记录分页:支持按用户、优惠券、状态、时间范围筛选 - 优惠券配置分页:支持按状态、名称筛选
- 领取记录分页:支持按用户、优惠券、状态、时间范围筛选
- **券码系统**:使用MyBatis-Plus Page实现
- 券码批次分页:支持按景区、批次名称、状态筛选
- 券码列表分页:支持按批次、状态、用户筛选
- 使用记录分页:支持按券码、用户、时间范围筛选
#### 统计功能 #### 统计功能
- 基础统计领取数、使用数、可用数 - **优惠券统计**:基础统计领取数、使用数、可用数)、详细统计(使用率、平均使用天数)
- 详细统计:使用率、平均使用天数 - **券码统计**:支持可重复使用的统计(使用率、重复使用率、平均使用次数)
- 时间范围统计:指定时间段的整体数据分析 - 时间范围统计:指定时间段的整体数据分析
## 券码管理系统 (Voucher System)
### 核心特性
券码系统支持**可重复使用**的优惠券管理,与传统优惠券系统并行工作。
#### 关键优势
- **可重复使用**:支持单个券码多次使用,通过`maxUseCount`配置最大使用次数
- **用户使用限制**:支持单个用户对券码的使用次数限制(`maxUsePerUser`)
- **使用间隔控制**:支持设置使用时间间隔(`useIntervalHours`)
- **时间范围控制**:支持设置券码的有效期开始和结束时间
#### 券码优惠类型
```java
public enum VoucherDiscountType {
FREE_ALL(0, "全场免费"), // 优先级最高,且不可叠加
REDUCE_PRICE(1, "商品降价"), // 每个商品减免固定金额
DISCOUNT(2, "商品打折"); // 每个商品按百分比打折
}
```
#### 数据库表结构
- **price_voucher_batch_config**:券码批次配置表,支持按景区、推客创建券码批次
- **price_voucher_code**:券码表,每个券码全局唯一,支持同一用户在同一景区只能领取一次
- **price_voucher_usage_record**:券码使用记录表,记录每次使用的完整信息
- **voucher_print_record**:券码打印记录表,用于移动端打印功能
## 统一优惠检测系统 (Unified Discount Detection)
### 设计模式
采用**策略模式**的可扩展优惠检测系统,统一管理并自动组合多种优惠类型。
#### 核心接口
```java
// 优惠提供者接口
public interface IDiscountProvider {
String getProviderType(); // 提供者类型
int getPriority(); // 优先级(数字越大越高)
List<DiscountInfo> detectAvailableDiscounts(); // 检测可用优惠
DiscountResult applyDiscount(); // 应用优惠
}
// 优惠检测服务接口
public interface IDiscountDetectionService {
DiscountCombinationResult calculateOptimalCombination(); // 计算最优组合
DiscountCombinationResult previewOptimalCombination(); // 预览优惠组合
}
```
#### 优惠提供者实现(按优先级排序)
1. **VoucherDiscountProvider** (优先级: 100)
- 处理券码优惠逻辑
- 支持用户主动输入券码或自动选择最优券码
- 全场免费券码不可与其他优惠叠加
2. **CouponDiscountProvider** (优先级: 80)
- 处理优惠券优惠逻辑
- 自动选择最优优惠券
- 可与券码叠加使用(除全场免费券码外)
3. **OnePricePurchaseDiscountProvider** (优先级: 60)
- 处理一口价优惠逻辑(景区级统一价格)
- 仅当一口价小于当前金额时产生优惠
- 叠加性由配置`canUseCoupon/canUseVoucher`控制
#### 优惠应用策略
```java
原价 券码 优惠券 一口价 最终价格
特殊情况
- 全场免费券码直接最终价=0停止后续优惠
- 一口价可叠加性由配置 canUseCoupon / canUseVoucher 控制
```
### 开发模式 ### 开发模式
#### 添加新商品类型 #### 添加新商品类型
@@ -188,15 +282,92 @@ ProductType枚举定义了支持的商品类型:
2. 在CouponServiceImpl中实现计算逻辑 2. 在CouponServiceImpl中实现计算逻辑
3. 更新applicableProducts验证规则 3. 更新applicableProducts验证规则
#### 添加新优惠提供者(策略扩展)
```java
@Component
public class FlashSaleDiscountProvider implements IDiscountProvider {
@Override
public String getProviderType() { return "FLASH_SALE"; }
@Override
public int getPriority() { return 90; } // 介于券码和优惠券之间
@Override
public List<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
// 实现限时抢购优惠检测逻辑
return discountInfoList;
}
@Override
public DiscountResult applyDiscount(DiscountDetectionContext context, DiscountInfo discount) {
// 实现优惠应用逻辑
return discountResult;
}
}
```
#### 创建可重复使用券码批次
```java
VoucherBatchCreateReqV2 request = new VoucherBatchCreateReqV2();
request.setBatchName("限时活动券码");
request.setMaxUseCount(3); // 每个券码最多使用三次
request.setMaxUsePerUser(2); // 每个用户最多使用两次
request.setUseIntervalHours(12); // 使用间隔12小时
request.setValidStartTime(startTime); // 有效期开始时间
request.setValidEndTime(endTime); // 有效期结束时间
```
#### 自定义TypeHandler使用 #### 自定义TypeHandler使用
项目使用自定义TypeHandler处理复杂JSON字段: 项目使用自定义TypeHandler处理复杂JSON字段:
- `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化 - `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化
### 测试策略 ### 测试策略
- 单元测试:每个服务类都有对应测试类 针对pricing模块的全面测试策略:
- 配置验证测试:DefaultConfigValidationTest验证default配置
- JSON序列化测试:验证复杂对象的数据库存储 #### 单元测试类型
- 分页功能测试:验证PageHelper集成 - **服务层测试**:每个服务类都有对应测试类
- `PriceBundleServiceTest` - 套餐价格计算测试
- `ReusableVoucherServiceTest` - 可重复使用券码测试
- `VoucherTimeRangeTest` - 券码时间范围功能测试
- `VoucherPrintServiceCodeGenerationTest` - 券码生成测试
- **实体映射测试**:验证数据库映射和JSON序列化
- `PriceBundleConfigStructureTest` - 实体结构测试
- `PriceBundleConfigJsonTest` - JSON序列化测试
- `CouponSwitchFieldsMappingTest` - 字段映射测试
- **类型处理器测试**:验证自定义TypeHandler
- `BundleProductListTypeHandlerTest` - 套餐商品列表序列化测试
- **配置验证测试**:验证系统配置完整性
- `DefaultConfigValidationTest` - 验证所有ProductType的default配置
- `CodeGenerationStandaloneTest` - 独立代码生成测试
#### 测试执行命令
```bash
# 运行单个测试类
mvn test -Dtest=VoucherTimeRangeTest
mvn test -Dtest=ReusableVoucherServiceTest
mvn test -Dtest=BundleProductListTypeHandlerTest
# 运行整个pricing模块测试
mvn test -Dtest="com.ycwl.basic.pricing.*Test"
# 运行特定分类的测试
mvn test -Dtest="com.ycwl.basic.pricing.service.*Test" # 服务层测试
mvn test -Dtest="com.ycwl.basic.pricing.handler.*Test" # TypeHandler测试
mvn test -Dtest="com.ycwl.basic.pricing.entity.*Test" # 实体测试
mvn test -Dtest="com.ycwl.basic.pricing.mapper.*Test" # Mapper测试
# 运行带详细报告的测试
mvn test -Dtest="com.ycwl.basic.pricing.*Test" -Dsurefire.printSummary=true
```
#### 重点测试场景
- **价格计算核心流程**:验证统一优惠检测和组合逻辑
- **可重复使用券码**:验证多次使用、时间间隔、用户限制逻辑
- **时间范围控制**:验证券码有效期开始和结束时间
- **优惠叠加规则**:验证券码、优惠券、一口价的叠加逻辑
- **JSON序列化**:验证复杂对象在数据库中的存储和读取
- **分页功能**:验证PageHelper和MyBatis-Plus分页集成
- **异常处理**:验证业务异常和全局异常处理器
## 关键架构模式 ## 关键架构模式
@@ -290,3 +461,67 @@ logging:
level: level:
com.ycwl.basic.integration: DEBUG com.ycwl.basic.integration: DEBUG
``` ```
## 开发环境和调试
### 端口配置
- **开发环境端口**: 8030
- **应用名称**: zt
### 日志配置
开发环境默认启用详细的集成服务日志,便于调试外部服务调用问题。
### CI/CD 配置
项目使用 Jenkins 进行持续集成:
- **JDK 版本**: OpenJDK 21
- **构建命令**: `mvn clean package -DskipTests=true`
- **构建产物**: 自动归档和发布 JAR 文件
## 重要开发约定
### 测试文件组织
测试按功能模块组织,包括:
- **适配器测试**: `*AdapterTest.java` 测试第三方集成
- **实体测试**: 验证数据库映射和JSON序列化
- **Mapper测试**: 验证数据访问层逻辑
- **Handler测试**: 测试自定义TypeHandler
### 模块化架构
每个业务模块(如 `pricing``integration``order`)都有完整的分层结构:
```
module/
├── controller/ # REST API控制器
├── service/ # 业务逻辑层
├── repository/ # 数据访问抽象
├── mapper/ # MyBatis数据映射
├── entity/ # JPA/MyBatis实体
├── dto/ # 数据传输对象
├── enums/ # 枚举定义
└── exception/ # 模块特定异常
```
### 外部服务集成
集成服务统一使用以下模式:
- **Feign客户端**: 声明式HTTP客户端调用
- **错误处理**: 统一的`handleResponse`模式
- **配置管理**: 通过`IntegrationProperties`集中配置
- **超时配置**: 连接超时5秒,读取超时10秒
## Windows 开发环境注意事项
### 路径处理
- 项目在Windows系统上运行,注意路径分隔符使用反斜杠 `\`
- 配置文件中的资源路径已适配Windows环境
- 日志文件和临时文件路径会自动适配系统环境
### 开发工具兼容性
- 确保使用Java 21兼容的IDE
- Maven命令在Windows Command Prompt和PowerShell中均可使用
- 建议使用UTF-8编码避免中文字符问题
### 端口占用检查
开发时如遇端口冲突,使用以下命令检查:
```cmd
netstat -ano | findstr :8030
taskkill /f /pid <PID>
```

View File

@@ -0,0 +1,32 @@
package com.ycwl.basic.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 视频更新检查配置
* @author Claude
*/
@Data
@Component
@ConfigurationProperties(prefix = "video.update")
public class VideoUpdateConfig {
/**
* 是否将片段变化检测为新增
* true: 任何变化都视为新增
* false: 只有数量增加才视为新增
*/
private boolean detectChangesAsNew = true;
/**
* 最小新增片段数量才认为可更新
*/
private int minNewSegmentCount = 1;
/**
* 是否启用视频更新检查功能
*/
private boolean enabled = true;
}

View File

@@ -7,6 +7,7 @@ import com.ycwl.basic.pricing.dto.CouponClaimRequest;
import com.ycwl.basic.pricing.dto.CouponClaimResult; import com.ycwl.basic.pricing.dto.CouponClaimResult;
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
import com.ycwl.basic.pricing.enums.CouponType;
import com.ycwl.basic.pricing.service.ICouponService; import com.ycwl.basic.pricing.service.ICouponService;
import com.ycwl.basic.pricing.service.VoucherPrintService; import com.ycwl.basic.pricing.service.VoucherPrintService;
import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.repository.FaceRepository;
@@ -19,6 +20,9 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.math.RoundingMode;
@RestController @RestController
@RequestMapping("/api/mobile/claim/v1") @RequestMapping("/api/mobile/claim/v1")
@AllArgsConstructor @AllArgsConstructor
@@ -80,6 +84,24 @@ public class AppClaimController {
if (claimResult.isSuccess()) { if (claimResult.isSuccess()) {
// 领到了 // 领到了
claimResp.setHasCoupon(true); claimResp.setHasCoupon(true);
switch (claimResult.getCoupon().getCouponType()) {
case CouponType.PERCENTAGE:
claimResp.setCouponType("折扣优惠券");
claimResp.setCouponDesc("" + (BigDecimal.valueOf(1).setScale(2, RoundingMode.HALF_UP).subtract(claimResult.getCoupon().getDiscountValue())).multiply(BigDecimal.valueOf(10)) + "");
break;
case CouponType.FIXED_AMOUNT:
if (claimResult.getCoupon().getMinAmount().compareTo(BigDecimal.ZERO) > 0) {
claimResp.setCouponType("满减优惠券");
claimResp.setCouponDesc("" + claimResult.getCoupon().getMinAmount() + "" + claimResult.getCoupon().getDiscountValue());
} else {
claimResp.setCouponType("直减优惠券");
claimResp.setCouponDesc("直减" + claimResult.getCoupon().getDiscountValue());
}
break;
default:
claimResp.setCouponType("普通优惠券");
break;
}
claimResp.setCouponDesc(scenicConfig.getString("coupon_desc_for_type_" + req.getType(), "专属折扣券")); claimResp.setCouponDesc(scenicConfig.getString("coupon_desc_for_type_" + req.getType(), "专属折扣券"));
claimResp.setCouponCountdown(scenicConfig.getString("coupon_countdown_for_type_" + req.getType(), "送你优惠,保存美好!")); claimResp.setCouponCountdown(scenicConfig.getString("coupon_countdown_for_type_" + req.getType(), "送你优惠,保存美好!"));
return ApiResponse.success(claimResp); return ApiResponse.success(claimResp);

View File

@@ -93,6 +93,7 @@ public interface StatisticsMapper {
int addStatisticsRecord(StatisticsRecordAddReq req); int addStatisticsRecord(StatisticsRecordAddReq req);
List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime); List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime);
List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime);
Long getUserRecentEnterType(Long memberId, Date endTime); Long getUserRecentEnterType(Long memberId, Date endTime);

View File

@@ -8,6 +8,7 @@ public class ClaimResp {
private String printType; private String printType;
private String printCode; private String printCode;
private Boolean hasCoupon; private Boolean hasCoupon;
private String couponType;
private String couponDesc; private String couponDesc;
private String couponCountdown; private String couponCountdown;
} }

View File

@@ -0,0 +1,50 @@
package com.ycwl.basic.model.mobile.goods;
import lombok.Data;
/**
* 视频更新检查响应VO
* @author Claude
*/
@Data
public class VideoUpdateCheckVO {
/**
* 是否可更新
*/
private boolean canUpdate;
/**
* 新增片段数量
*/
private int newSegmentCount;
/**
* 当前总片段数量
*/
private int totalSegmentCount;
/**
* 原始片段数量
*/
private int originalSegmentCount;
/**
* 视频ID
*/
private Long videoId;
/**
* 任务ID
*/
private Long taskId;
/**
* 人脸ID
*/
private Long faceId;
/**
* 模板ID
*/
private Long templateId;
}

View File

@@ -1,90 +1,75 @@
# 价格查询系统 (Pricing Module) 开发指南 # 价格查询系统 (Pricing Module) 开发指南
此文档为pricing包的专用开发指南,提供该模块的详细架构说明和开发最佳实践 此文档为 pricing 包的专用开发指南,基于当前代码实际结构进行说明与示例
## 模块概览 ## 模块概览
价格查询系统 (`com.ycwl.basic.pricing`) 是一个独立的业务模块,提供商品定价、优惠券管理、券码管理和价格计算功能。采用分层架构设计,具备完整的CRUD操作、异常处理和数据统计功能。支持优惠券和券码的同时使用,券码优先级更高 `com.ycwl.basic.pricing` 提供商品定价、分层/套餐与一口价配置、优惠券管理、券码管理与使用记录、统一优惠检测与价格计算等能力。采用分层架构(controller/dto/entity/mapper/service),同时整合了 PageHelper 与 MyBatis‑Plus 的分页能力。优惠叠加采用统一检测与排序:券码 > 优惠券 > 一口价(受配置限制)
## 目录结构 ## 目录结构(已对齐当前代码)
``` ```
com.ycwl.basic.pricing/ com.ycwl.basic.pricing/
├── controller/ # REST API控制器层 ├── controller/ # REST API 控制器层
│ ├── CouponManagementController.java # 优惠券管理API │ ├── PriceCalculationController.java # 价格计算 + 用户可用券查询
│ ├── PriceCalculationController.java # 价格计算API │ ├── PricingConfigController.java # 产品/阶梯/套餐(一口价)配置管理(含 admin 查询)
── PricingConfigController.java # 价格配置管理API ── CouponManagementController.java # 优惠券配置/领取记录/统计(admin)
├── dto/ # 数据传输对象 │ ├── OnePricePurchaseController.java # 一口价配置管理(admin)
│ ├── BundleProductItem.java # 套餐商品项 │ ├── VoucherManagementController.java # 券码批次/券码/移动端领取与查询
── CouponInfo.java # 优惠券信息 ── VoucherUsageController.java # 券码使用记录与统计查询
│ ├── CouponUseRequest.java # 优惠券使用请求 ├── dto/ # 数据传输对象
│ ├── CouponUseResult.java # 优惠券使用结果 │ ├── PriceCalculationRequest.java # 价格计算请求(含voucherCode/faceId/scenicId...)
│ ├── DiscountDetail.java # 折扣详情 │ ├── PriceCalculationResult.java # 价格计算结果(含usedCoupon/usedVoucher/availableDiscounts)
│ ├── PriceCalculationRequest.java # 价格计算请求 │ ├── ProductItem.java, ProductPriceInfo.java, PriceDetails.java
│ ├── PriceCalculationResult.java # 价格计算结果 │ ├── DiscountDetectionContext.java, DiscountInfo.java, DiscountDetail.java,
├── PriceDetails.java # 价格详情 │ DiscountResult.java, DiscountCombinationResult.java
│ ├── ProductItem.java # 商品项 │ ├── BundleProductItem.java, MobilePriceCalculationRequest.java
│ ├── ProductPriceInfo.java # 商品价格信息 │ ├── OnePriceConfigRequest.java, OnePriceInfo.java
│ ├── VoucherInfo.java # 券码信息 │ ├── req/ # 券码管理请求DTO
│ ├── DiscountDetectionContext.java # 优惠检测上下文 │ ├── VoucherBatchCreateReq(.java|V2)
│ ├── DiscountInfo.java # 优惠信息 │ ├── VoucherBatchQueryReq.java, VoucherCodeQueryReq.java, VoucherClaimReq.java
├── DiscountResult.java # 优惠结果 │ └── VoucherUsageHistoryReq.java
│ └── DiscountCombinationResult.java # 优惠组合结果 │ └── resp/ # 券码管理响应DTO
├── entity/ # 数据库实体类 │ ├── VoucherBatchResp.java, VoucherBatchStatsResp.java
├── BaseEntity.java # 基础实体类 ├── VoucherCodeResp.java, VoucherValidationResp.java
├── PriceBundleConfig.java # 套餐配置 ├── VoucherUsageRecordResp.java, VoucherUsageStatsResp.java, VoucherUsageSummaryResp.java
├── PriceCouponClaimRecord.java # 优惠券领取记录 └── VoucherPrintResp.java
│ ├── PriceCouponConfig.java # 优惠券配置 ├── entity/ # 数据库实体类(MyBatis‑Plus)
│ ├── PriceProductConfig.java # 商品价格配置 │ ├── PriceProductConfig.java, PriceTierConfig.java, PriceBundleConfig.java
│ ├── PriceTierConfig.java # 分层价格配置 │ ├── PriceCouponConfig.java, PriceCouponClaimRecord.java
│ ├── PriceVoucherBatchConfig.java # 券码批次配置 │ ├── PriceVoucherBatchConfig.java, PriceVoucherCode.java, PriceVoucherUsageRecord.java
── PriceVoucherCode.java # 券码实体 ── PriceOnePriceConfig.java # 一口价配置
├── enums/ # 枚举类 │ └── VoucherPrintRecord.java # 券码打印记录
│ ├── CouponStatus.java # 优惠券状态 ├── enums/ # 枚举类
│ ├── CouponType.java # 优惠券类型 │ ├── ProductType.java, CouponType.java, CouponStatus.java
│ ├── ProductType.java # 商品类型 │ ├── VoucherDiscountType.java, VoucherCodeStatus.java
│ ├── VoucherDiscountType.java # 券码优惠类型 ├── exception/ # 统一异常与业务异常
── VoucherCodeStatus.java # 券码状态 ── PricingExceptionHandler.java, PriceCalculationException.java
├── exception/ # 异常处理 │ ├── ProductConfigNotFoundException.java, DiscountDetectionException.java
│ ├── CouponInvalidException.java # 优惠券无效异常 │ ├── CouponInvalidException.java, VoucherInvalidException.java,
│ ├── PriceCalculationException.java # 价格计算异常 │ ├── VoucherAlreadyUsedException.java, VoucherNotClaimableException.java
│ ├── PricingExceptionHandler.java # 定价异常处理器 ├── handler/ # MyBatis 类型处理器
│ ├── ProductConfigNotFoundException.java # 商品配置未找到异常 │ ├── BundleProductListTypeHandler.java # 套餐商品列表 JSON ↔ 对象列表
── VoucherInvalidException.java # 券码无效异常 ── ProductTypeListTypeHandler.java # 商品类型列表 JSON ↔ 枚举列表
│ ├── VoucherAlreadyUsedException.java # 券码已使用异常 ├── mapper/ # 数据访问接口(多为 MyBatis‑Plus Mapper)
│ ├── VoucherNotClaimableException.java # 券码不可领取异常 │ ├── PriceProductConfigMapper.java, PriceTierConfigMapper.java, PriceBundleConfigMapper.java
── DiscountDetectionException.java # 优惠检测异常 ── PriceCouponConfigMapper.java, PriceCouponClaimRecordMapper.java
├── handler/ # 自定义处理器 │ ├── PriceVoucherBatchConfigMapper.java, PriceVoucherCodeMapper.java, PriceVoucherUsageRecordMapper.java
│ └── BundleProductListTypeHandler.java # 套餐商品列表类型处理器 │ └── PriceOnePriceConfigMapper.java, VoucherPrintRecordMapper.java
── mapper/ # MyBatis数据访问层 ── service/ # 业务层接口与实现
├── PriceBundleConfigMapper.java ├── IPriceCalculationService.java, IDiscountDetectionService.java, IDiscountProvider.java
├── PriceCouponClaimRecordMapper.java ├── IProductConfigService.java, IPricingManagementService.java, IPriceBundleService.java
├── PriceCouponConfigMapper.java ├── ICouponService.java, ICouponManagementService.java
├── PriceProductConfigMapper.java ├── IOnePricePurchaseService.java, IVoucherService.java, IVoucherUsageService.java
├── PriceTierConfigMapper.java ├── VoucherBatchService.java, VoucherCodeService.java, VoucherPrintService.java
── PriceVoucherBatchConfigMapper.java ── impl/
── PriceVoucherCodeMapper.java ── PriceCalculationServiceImpl.java, DiscountDetectionServiceImpl.java
└── service/ # 业务逻辑层 ├── ProductConfigServiceImpl.java, PricingManagementServiceImpl.java, PriceBundleServiceImpl.java
├── ICouponManagementService.java # 优惠券管理服务接口 ├── CouponServiceImpl.java, CouponManagementServiceImpl.java, CouponDiscountProvider.java
├── ICouponService.java # 优惠券服务接口 ├── VoucherServiceImpl.java, VoucherDiscountProvider.java,
├── IPriceBundleService.java # 套餐服务接口 ├── VoucherBatchServiceImpl.java, VoucherCodeServiceImpl.java, VoucherPrintServiceImpl.java,
├── IPriceCalculationService.java # 价格计算服务接口 ├── VoucherUsageServiceImpl.java,
├── IPricingManagementService.java # 定价管理服务接口 └── OnePricePurchaseServiceImpl.java, OnePricePurchaseDiscountProvider.java
├── IProductConfigService.java # 商品配置服务接口
├── IVoucherService.java # 券码服务接口
├── IDiscountProvider.java # 优惠提供者接口
├── IDiscountDetectionService.java # 优惠检测服务接口
└── impl/ # 服务实现类
├── CouponManagementServiceImpl.java
├── CouponServiceImpl.java
├── PriceBundleServiceImpl.java
├── PriceCalculationServiceImpl.java
├── PricingManagementServiceImpl.java
├── ProductConfigServiceImpl.java
├── VoucherServiceImpl.java
├── CouponDiscountProvider.java
├── VoucherDiscountProvider.java
└── DiscountDetectionServiceImpl.java
``` ```
## 核心功能 ## 核心功能
@@ -92,94 +77,91 @@ com.ycwl.basic.pricing/
### 1. 价格计算引擎 ### 1. 价格计算引擎
#### API端点 #### API端点
- `POST /api/pricing/calculate` - 执行价格计算 - `POST /api/pricing/calculate` 执行价格计算(预览模式默认开启)
- `GET /api/pricing/coupons/my-coupons` — 查询用户可用优惠券
#### 计算流程 #### 计算流程
```java ```java
// 价格计算核心流程 // 价格计算核心流程(IPriceCalculationService / PriceCalculationServiceImpl)
1. 验证PriceCalculationRequest请求参数 1. 校验 PriceCalculationRequest包含 voucherCode / scenicId / faceId / autoUseXXX
2. 加载商品基础配置 (PriceProductConfig) 2. 加载商品基础配置 (PriceProductConfig)匹配分层规则 (PriceTierConfig)套餐等
3. 应用分层定价规则 (PriceTierConfig) 3. 构造 DiscountDetectionContext统一检测可用优惠券码/优惠券/一口价
4. 处理套餐商品逻辑 (BundleProductItem) 4. 按优先级与可叠加规则应用优惠券码 > 优惠券 > 一口价 canUseCoupon/canUseVoucher 控制
5. 使用统一优惠检测系统处理券码和优惠券 5. 汇总 DiscountDetail计算 finalAmount / discountAmount 并返回 PriceCalculationResult
6. 按优先级应用优惠券码 > 优惠券
7. 计算最终价格并返回详细结果
``` ```
#### 关键类 #### 关键类
- `PriceCalculationService`: 价格计算核心逻辑 - `IPriceCalculationService` / `PriceCalculationServiceImpl`: 价格计算核心逻辑
- `PriceCalculationRequest`: 计算请求DTO - `PriceCalculationRequest`: 计算请求DTO
- `PriceCalculationResult`: 计算结果DTO - `PriceCalculationResult`: 计算结果DTO
- `IDiscountDetectionService`: 统一优惠检测与组合
### 2. 优惠券管理系统 ### 2. 优惠券管理系统
#### API端点 #### API端点(管理端,见 `CouponManagementController`)
- `GET /api/pricing/admin/coupons/` - 分页查询优惠券配置 - `POST /api/pricing/admin/coupons/configs` — 创建配置
- `POST /api/pricing/admin/coupons/` - 创建优惠券配置 - `PUT /api/pricing/admin/coupons/configs/{id}` — 更新配置
- `PUT /api/pricing/admin/coupons/{id}` - 更新优惠券配置 - `DELETE /api/pricing/admin/coupons/configs/{id}` — 删除配置
- `DELETE /api/pricing/admin/coupons/{id}` - 删除优惠券配置 - `PUT /api/pricing/admin/coupons/configs/{id}/status` — 启用/禁用
- `GET /api/pricing/admin/coupons/{id}/claims` - 查询优惠券领取记录 - `GET /api/pricing/admin/coupons/configs` — 全量配置(含禁用)
- `GET /api/pricing/admin/coupons/{id}/stats` - 获取优惠券统计信息 - `GET /api/pricing/admin/coupons/configs/page` — 分页查询
- `GET /api/pricing/admin/coupons/claim-records[/*]` — 领取记录列表/分页/按条件查询
- `GET /api/pricing/admin/coupons/stats/{couponId}[/*]` — 使用统计/明细/概览
#### 优惠券类型 #### 枚举定义(简述)
```java ```java
public enum CouponType { // 实际枚举包含 code/description 字段,并提供 fromCode 工具方法
PERCENTAGE, // 百分比折扣 public enum CouponType { PERCENTAGE("percentage", ...), FIXED_AMOUNT("fixed_amount", ...) }
FIXED_AMOUNT // 固定金额减免 public enum CouponStatus { CLAIMED("claimed", ...), USED("used", ...), EXPIRED("expired", ...) }
}
public enum CouponStatus {
CLAIMED, // 已领取
USED, // 已使用
EXPIRED // 已过期
}
``` ```
#### 关键特性 #### 关键特性
- **商品类型限制**: 通过`applicableProducts` JSON字段控制适用商品 - 商品类型限制:通过 JSON 字段(结合 `ProductTypeListTypeHandler`控制适用商品
- **消费限制**: 支持最小消费金额最大折扣限制 - 消费限制支持最小消费金额最大折扣限制
- **时效性**: 基于时间的有效期控制 - 时效性基于时间的有效期控制
- **统计分析**: 完整的使用统计分析 - 统计分析完整的使用统计分析能
### 3. 商品配置管理 ### 3. 商品配置管理
#### API端点 #### API端点(摘)
- `GET /api/pricing/config/products` - 查询商品配置 - `GET /api/pricing/config/products` 查询商品配置
- `POST /api/pricing/config/products` - 创建商品配置 - `POST /api/pricing/config/products` 创建商品配置
- `PUT /api/pricing/config/products/{id}` - 更新商品配置 - `PUT /api/pricing/config/products/{id}` 更新商品配置
- `GET /api/pricing/config/tiers[/*]``/bundles[/*]` — 阶梯/一口价配置查询
#### 商品类型定义 #### 商品类型定义(对齐实际)
```java ```java
// 实际定义(带 code/description,并提供 fromCode):
public enum ProductType { public enum ProductType {
VLOG_VIDEO("vlog_video", "Vlog视频"), VLOG_VIDEO("VLOG_VIDEO", "Vlog视频"),
RECORDING_SET("recording_set", "录像集"), RECORDING_SET("RECORDING_SET", "录像集"),
PHOTO_SET("photo_set", "照相集"), PHOTO_SET("PHOTO_SET", "照相集"),
PHOTO_PRINT("photo_print", "照片打印"), PHOTO_PRINT("PHOTO_PRINT", "照片打印"),
MACHINE_PRINT("machine_print", "一体机打印"); MACHINE_PRINT("MACHINE_PRINT", "一体机打印");
} }
``` ```
#### 分层定价 #### 分层定价
支持基于数量的分层定价策略,通过`PriceTierConfig`实体配置不同数量区间的单价。 支持基于数量的分层定价策略,通过 `PriceTierConfig` 配置不同数量区间的单价。
## 开发最佳实践 ## 开发最佳实践
### 1. 添加新商品类型 ### 1. 添加新商品类型
```java ```java
// 步骤1: 在ProductType枚举中添加新类型 // 步骤1: 在 ProductType 枚举中添加新类型(含 code/description)
public enum ProductType { public enum ProductType {
// 现有类型... // 现有类型...
NEW_PRODUCT("new_product", "新商品类型"); NEW_PRODUCT("NEW_PRODUCT", "新商品类型");
} }
// 步骤2: 在数据库中添加default配置 // 步骤2: 在数据库中添加 default 配置
INSERT INTO price_product_config (product_type, base_price, ...) INSERT INTO price_product_config (product_type, base_price, ...)
VALUES ('new_product', 100.00, ...); VALUES ('NEW_PRODUCT', 100.00, ...);
// 步骤3: 添加分层定价配置(可选) // 步骤3: 添加分层定价配置(可选)
INSERT INTO price_tier_config (product_type, min_quantity, max_quantity, unit_price, ...) INSERT INTO price_tier_config (product_type, min_quantity, max_quantity, unit_price, ...)
VALUES ('new_product', 1, 10, 95.00, ...); VALUES ('NEW_PRODUCT', 1, 10, 95.00, ...);
// 步骤4: 更新前端产品类型映射 // 步骤4: 更新前端产品类型映射
``` ```
@@ -187,14 +169,14 @@ VALUES ('new_product', 1, 10, 95.00, ...);
### 2. 扩展优惠券类型 ### 2. 扩展优惠券类型
```java ```java
// 步骤1: 在CouponType枚举中添加新类型 // 步骤1: 在 CouponType 中添加新类型
public enum CouponType { public enum CouponType {
PERCENTAGE, PERCENTAGE,
FIXED_AMOUNT, FIXED_AMOUNT,
NEW_COUPON_TYPE // 新增类型 NEW_COUPON_TYPE // 新增类型
} }
// 步骤2: 在CouponServiceImpl中实现计算逻辑 // 步骤2: 在 CouponServiceImpl 中实现计算逻辑
@Override @Override
public BigDecimal calculateDiscount(CouponConfig coupon, BigDecimal originalPrice) { public BigDecimal calculateDiscount(CouponConfig coupon, BigDecimal originalPrice) {
return switch (coupon.getCouponType()) { return switch (coupon.getCouponType()) {
@@ -204,19 +186,17 @@ public BigDecimal calculateDiscount(CouponConfig coupon, BigDecimal originalPric
}; };
} }
// 步骤3: 更新applicableProducts验证规则 // 步骤3: 更新 applicableProducts 验证规则(如有)
``` ```
### 3. 自定义TypeHandler使用 ### 3. 自定义 TypeHandler 使用
项目使用MyBatis自定义TypeHandler处理复杂JSON字段:
```java ```java
// BundleProductListTypeHandler处理套餐商品列表 // BundleProductListTypeHandler 处理套餐商品列表
@MappedTypes(List.class) @MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR) @MappedJdbcTypes(JdbcType.VARCHAR)
public class BundleProductListTypeHandler extends BaseTypeHandler<List<BundleProductItem>> { public class BundleProductListTypeHandler extends BaseTypeHandler<List<BundleProductItem>> {
// JSON序列化/反序列化逻辑 // JSON 序列化/反序列化逻辑
} }
// 在实体类中使用 // 在实体类中使用
@@ -224,6 +204,9 @@ public class SomeEntity {
@TableField(typeHandler = BundleProductListTypeHandler.class) @TableField(typeHandler = BundleProductListTypeHandler.class)
private List<BundleProductItem> bundleProducts; private List<BundleProductItem> bundleProducts;
} }
// ProductTypeListTypeHandler 处理商品类型列表(券码可用商品类型)
public class ProductTypeListTypeHandler extends BaseTypeHandler<List<ProductType>> { /* ... */ }
``` ```
### 4. 异常处理模式 ### 4. 异常处理模式
@@ -231,12 +214,10 @@ public class SomeEntity {
```java ```java
// 自定义异常类 // 自定义异常类
public class PriceCalculationException extends RuntimeException { public class PriceCalculationException extends RuntimeException {
public PriceCalculationException(String message) { public PriceCalculationException(String message) { super(message); }
super(message);
}
} }
// 在PricingExceptionHandler中统一处理 // 在 PricingExceptionHandler 中统一处理
@ExceptionHandler(PriceCalculationException.class) @ExceptionHandler(PriceCalculationException.class)
public ApiResponse<String> handlePriceCalculationException(PriceCalculationException e) { public ApiResponse<String> handlePriceCalculationException(PriceCalculationException e) {
return ApiResponse.fail(ErrorCode.PRICE_CALCULATION_ERROR, e.getMessage()); return ApiResponse.fail(ErrorCode.PRICE_CALCULATION_ERROR, e.getMessage());
@@ -245,107 +226,39 @@ public ApiResponse<String> handlePriceCalculationException(PriceCalculationExcep
### 5. 分页查询实现 ### 5. 分页查询实现
```java ```
// 使用PageHelper实现分页 // 使用 PageHelper 实现分页(优惠券相关)
@Override @Override
public PageInfo<CouponConfig> getCouponsByPage(int pageNum, int pageSize, String status, String name) { public PageInfo<CouponConfig> getCouponsByPage(int pageNum, int pageSize, String status, String name) {
PageHelper.startPage(pageNum, pageSize); PageHelper.startPage(pageNum, pageSize);
// ...
QueryWrapper<CouponConfig> queryWrapper = new QueryWrapper<>();
if (StringUtils.hasText(status)) {
queryWrapper.eq("status", status);
}
if (StringUtils.hasText(name)) {
queryWrapper.like("name", name);
}
List<CouponConfig> list = couponConfigMapper.selectList(queryWrapper);
return new PageInfo<>(list);
} }
``` ```
## 测试策略 ```
// 使用 MyBatis‑Plus 的 Page(券码相关)
### 1. 单元测试 Page<VoucherBatchResp> page = voucherBatchService.queryBatchList(req);
每个服务类都应有对应的测试类,重点测试: Page<VoucherCodeResp> page = voucherCodeService.queryCodeList(req);
- 价格计算逻辑的准确性 ```
- 优惠券适用性验证
- 边界条件处理
- 异常场景覆盖
### 2. 集成测试
- 数据库操作测试
- JSON序列化测试
- 分页功能测试
- API端点测试
### 3. 配置验证测试
- `DefaultConfigValidationTest`: 验证default配置的完整性和正确性
- 确保所有ProductType都有对应的基础配置
## 数据库设计
### 核心表结构
- `price_product_config`: 商品价格基础配置
- `price_tier_config`: 分层定价配置
- `price_bundle_config`: 套餐配置
- `price_coupon_config`: 优惠券配置
- `price_coupon_claim_record`: 优惠券领取记录
### 关键字段设计
- JSON字段处理: `applicable_products`使用JSON存储适用商品类型列表
- 时间字段: 统一使用`LocalDateTime`类型
- 价格字段: 使用`BigDecimal`确保精度
- 状态字段: 使用枚举类型确保数据一致性
## 性能优化建议
1. **数据库查询优化**
- 为常用查询字段添加索引
- 使用分页查询避免大量数据加载
- 优化复杂的JOIN查询
2. **缓存策略**
- 对商品配置进行缓存,减少数据库访问
- 使用Redis缓存热点优惠券信息
3. **计算性能**
- 价格计算使用BigDecimal确保精度
- 批量计算时考虑并行处理
## 安全考虑
1. **输入验证**
- 严格验证所有输入参数
- 防止SQL注入和XSS攻击
2. **权限控制**
- 管理接口需要适当的权限验证
- 用户只能访问自己的优惠券记录
3. **数据完整性**
- 使用事务确保数据一致性
- 关键操作添加审计日志
## 券码管理系统 (Voucher System) ## 券码管理系统 (Voucher System)
### 1. 核心特性 ### 1. 核心特性
券码系统是从原`voucher`包迁移而来,现已完全集成到pricing包中,与优惠券系统并行工作。 券码系统由原独立模块迁移并完全集成到 pricing 包中,与优惠券系统并行工作。
#### 券码优惠类型 #### 券码优惠类型(简述)
```java ```java
public enum VoucherDiscountType { public enum VoucherDiscountType {
FREE_ALL(0, "全场免费"), // 所有商品免费,优先级最高且不可叠加 FREE_ALL(0, "全场免费"), // 优先级最高且不可叠加
REDUCE_PRICE(1, "商品降价"), // 每个商品减免固定金额 REDUCE_PRICE(1, "商品降价"), // 每个商品减免固定金额
DISCOUNT(2, "商品打折") // 每个商品按百分比打折 DISCOUNT(2, "商品打折"); // 每个商品按百分比打折
} }
``` ```
#### 券码状态流转 #### 券码状态(简述)
``` ```
UNCLAIMED(0) → CLAIMED_UNUSED(1) → USED(2) UNCLAIMED(0) → CLAIMED_AVAILABLE/CLAIMED_UNUSED(1) → USED(2) → CLAIMED_EXHAUSTED(3) → EXPIRED(4)
未领取 → 已领取未使用 → 已使用
``` ```
### 2. 数据库表结构 ### 2. 数据库表结构
@@ -360,15 +273,38 @@ UNCLAIMED(0) → CLAIMED_UNUSED(1) → USED(2)
- 用户限制:同一用户在同一景区只能领取一次券码 - 用户限制:同一用户在同一景区只能领取一次券码
- 时间追踪:记录领取时间、使用时间 - 时间追踪:记录领取时间、使用时间
### 3. 关键业务规则 ### 3. API 概览(对齐当前控制器)
管理端/服务端:`VoucherManagementController`
- `POST /api/pricing/voucher/batch/create``/batch/create/v2` — 创建券码批次
- `POST /api/pricing/voucher/batch/list` — 批次分页列表
- `GET /api/pricing/voucher/batch/{id}` — 批次详情
- `GET /api/pricing/voucher/batch/{id}/stats` — 批次统计
- `PUT /api/pricing/voucher/batch/{id}/status` — 批次启用/禁用
- `POST /api/pricing/voucher/codes` — 券码分页列表
- `PUT /api/pricing/voucher/code/{id}/use` — 标记某券码为已使用
- `GET /api/pricing/voucher/scenic/{scenicId}/users` — 景区用户券码概览
移动端:`VoucherManagementController`
- `POST /api/pricing/voucher/mobile/claim` — 领取券码
- `GET /api/pricing/voucher/mobile/my-codes` — 我的券码列表
使用记录:`VoucherUsageController`
- `POST /api/pricing/voucher/usage/history` — 使用记录分页
- `GET /api/pricing/voucher/usage/{voucherCode}/records` — 按券码查询记录
- `GET /api/pricing/voucher/usage/{voucherCode}/stats` — 券码统计
- `GET /api/pricing/voucher/usage/user/{faceId}/scenic/{scenicId}` — 用户在景区的记录
- `GET /api/pricing/voucher/usage/batch/{batchId}/stats` — 批次使用统计
### 4. 关键业务规则
#### 领取限制 #### 领取限制
- 同一`faceId`在同一`scenicId`中只能领取一次券码 - 同一 `faceId` 在同一 `scenicId` 中只能领取一次券码
- 只有启用状态的批次才能领取券码 - 只有启用状态的批次才能领取券码
- 批次必须有可用券码才能成功领取 - 批次必须有可用券码才能成功领取
#### 使用验证 #### 使用验证
- 券码必须`CLAIMED_UNUSED`状态才能使用 - 券码必须为可用状态(CLAIMED_AVAILABLE/CLAIMED_UNUSED
- 必须验证券码与景区的匹配关系 - 必须验证券码与景区的匹配关系
- 使用后自动更新批次统计数据 - 使用后自动更新批次统计数据
@@ -376,7 +312,7 @@ UNCLAIMED(0) → CLAIMED_UNUSED(1) → USED(2)
### 1. 架构设计 ### 1. 架构设计
采用策略模式设计的可扩展优惠检测系统,支持多种优惠类型的统一管理自动优化组合。 采用策略模式的可扩展优惠检测系统,统一管理自动组合多种优惠类型
#### 核心接口 #### 核心接口
```java ```java
@@ -395,7 +331,7 @@ public interface IDiscountDetectionService {
} }
``` ```
### 2. 优惠提供者实现 ### 2. 优惠提供者实现(当前实现与优先级)
#### VoucherDiscountProvider (优先级: 100) #### VoucherDiscountProvider (优先级: 100)
- 处理券码优惠逻辑 - 处理券码优惠逻辑
@@ -407,33 +343,29 @@ public interface IDiscountDetectionService {
- 自动选择最优优惠券 - 自动选择最优优惠券
- 可与券码叠加使用(除全场免费券码外) - 可与券码叠加使用(除全场免费券码外)
#### OnePricePurchaseDiscountProvider (优先级: 60)
- 处理一口价优惠逻辑(景区级统一价格)
- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定
### 3. 优惠应用策略 ### 3. 优惠应用策略
#### 优先级规则 #### 优先级规则
``` ```
券码优惠 (Priority: 100) → 优惠券优惠 (Priority: 80) 券码 (100) → 优惠券 (80) → 一口价 (60)
``` ```
#### 叠加逻辑 #### 叠加逻辑
```java ```java
原价 应用券码优惠 应用优惠券优惠 最终价格 原价 券码 优惠券 一口价 最终价格
特殊情况 特殊情况
- 全场免费券码最终价格直接为0不再应用其他优惠 - 全场免费券码直接最终价=0停止后续优惠
- 其他券码类型可与优惠券叠加使用 - 一口价可叠加性由配置 canUseCoupon / canUseVoucher 控制
``` ```
#### 显示顺序 #### 扩展支持
```
1. 券码优惠 (sortOrder: 1)
2. 限时立减 (sortOrder: 2)
3. 优惠券优惠 (sortOrder: 3)
4. 一口价优惠 (sortOrder: 4)
```
### 4. 扩展支持 ##### 添加新优惠类型
#### 添加新优惠类型
```java ```java
@Component @Component
public class FlashSaleDiscountProvider implements IDiscountProvider { public class FlashSaleDiscountProvider implements IDiscountProvider {
@@ -441,23 +373,23 @@ public class FlashSaleDiscountProvider implements IDiscountProvider {
public String getProviderType() { return "LIMITED_TIME"; } public String getProviderType() { return "LIMITED_TIME"; }
@Override @Override
public int getPriority() { return 90; } // 介于券码和优惠券之间 public int getPriority() { return 90; } // 示例:介于券码和优惠券之间
// 实现其他方法... // 实现其他方法...
} }
``` ```
#### 动态注册 ##### 动态注册
```java ```java
// 系统启动时自动扫描所有IDiscountProvider实现类 // 系统启动时自动扫描所有 IDiscountProvider 实现类
// 按优先级排序并注册到DiscountDetectionService中 // 按优先级排序并注册到 DiscountDetectionService
``` ```
## API接口扩展 ## API 接口扩展
### 1. 价格计算接口扩展 ### 1. 价格计算接口扩展
#### 新增请求参数 #### 新增请求参数(已存在)
```java ```java
public class PriceCalculationRequest { public class PriceCalculationRequest {
// 原有字段... // 原有字段...
@@ -469,7 +401,7 @@ public class PriceCalculationRequest {
} }
``` ```
#### 新增响应字段 #### 新增响应字段(已存在)
```java ```java
public class PriceCalculationResult { public class PriceCalculationResult {
// 原有字段... // 原有字段...
@@ -478,70 +410,53 @@ public class PriceCalculationResult {
} }
``` ```
### 2. 券码管理接口 ### 2. 一口价配置管理(OnePrice)
#### 移动端接口 `OnePricePurchaseController`(管理端):
- `POST /api/pricing/mobile/voucher/claim` - 领取券码 - `GET /api/pricing/admin/one-price/` — 分页查询
- `GET /api/pricing/mobile/voucher/my-codes` - 我的券码列表 - `GET /api/pricing/admin/one-price/all` — 全量查询
- `GET /api/pricing/admin/one-price/{id}` — 详情
- `POST /api/pricing/admin/one-price/` — 创建
- `PUT /api/pricing/admin/one-price/{id}` — 更新
- `DELETE /api/pricing/admin/one-price/{id}` — 删除
- `PUT /api/pricing/admin/one-price/{id}/status` — 启用/禁用
- `GET /api/pricing/admin/one-price/scenic/{scenicId}` — 按景区查询启用配置
- `GET /api/pricing/admin/one-price/check/{scenicId}` — 景区是否适用一口价
#### 管理端接口 ## 测试策略
- `POST /api/pricing/admin/voucher/batch/create` - 创建券码批次
- `GET /api/pricing/admin/voucher/batch/list` - 批次列表查询
- `GET /api/pricing/admin/voucher/codes` - 券码列表查询
## 开发最佳实践更新 ### 1. 单元测试
建议覆盖:
- 价格计算核心流程与边界
- 优惠券/券码/一口价适用性与叠加规则
- 异常场景与异常处理器
### 1. 优惠检测开发 ### 2. 集成测试
```java - 数据库读写与分页
// 检测上下文构建 - JSON 序列化/反序列化(TypeHandler)
DiscountDetectionContext context = new DiscountDetectionContext(); - API 端点的入参/出参校验
context.setUserId(userId);
context.setFaceId(faceId);
context.setScenicId(scenicId);
context.setProducts(products);
context.setCurrentAmount(amount);
context.setVoucherCode(voucherCode);
// 使用统一服务检测优惠 ### 3. 配置校验
DiscountCombinationResult result = discountDetectionService - 校验各 ProductType 的默认配置完整性
.calculateOptimalCombination(context); - 关键枚举与配置代码路径的兼容性
```
### 2. 券码服务使用 ## 数据库设计
```java
// 验证券码
VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo(
voucherCode, faceId, scenicId);
// 计算券码优惠 ### 核心表结构(摘)
BigDecimal discount = voucherService.calculateVoucherDiscount( - `price_product_config`: 商品价格基础配置
voucherInfo, context); - `price_tier_config`: 分层定价配置
- `price_bundle_config`: 套餐配置
// 标记券码已使用 - `price_coupon_config`: 优惠券配置
voucherService.markVoucherAsUsed(voucherCode, "订单使用"); - `price_coupon_claim_record`: 优惠券领取记录
```
### 3. 异常处理扩展
```java
// 券码相关异常
try {
// 券码操作
} catch (VoucherInvalidException e) {
// 券码无效
} catch (VoucherAlreadyUsedException e) {
// 券码已使用
} catch (VoucherNotClaimableException e) {
// 券码不可领取
}
```
## 数据库扩展
### 新增表结构 ### 新增表结构
- `price_voucher_batch_config`: 券码批次配置表 - `price_voucher_batch_config`: 券码批次配置表
- `price_voucher_code`: 券码表 - `price_voucher_code`: 券码表
- `price_voucher_usage_record`: 券码使用记录表
- `voucher_print_record`: 券码打印记录表
- `price_one_price_config`: 一口价配置表
### 索引优化 ### 索引优化(示例)
```sql ```sql
-- 券码查询优化 -- 券码查询优化
CREATE INDEX idx_voucher_code ON price_voucher_code(code); CREATE INDEX idx_voucher_code ON price_voucher_code(code);
@@ -549,9 +464,21 @@ CREATE INDEX idx_face_scenic ON price_voucher_code(face_id, scenic_id);
-- 批次查询优化 -- 批次查询优化
CREATE INDEX idx_scenic_broker ON price_voucher_batch_config(scenic_id, broker_id); CREATE INDEX idx_scenic_broker ON price_voucher_batch_config(scenic_id, broker_id);
-- 使用记录与打印记录查询优化(示例)
CREATE INDEX idx_usage_code ON price_voucher_usage_record(voucher_code);
CREATE INDEX idx_usage_face_scenic ON price_voucher_usage_record(face_id, scenic_id);
CREATE INDEX idx_print_face_scenic ON voucher_print_record(face_id, scenic_id);
``` ```
### 性能考虑 ### 性能考虑
- 券码表可能数据量较大,考虑按景区分表 - 券码表可能数据量较大,考虑按景区维度分表或归档
- 定期清理已删除的过期数据 - 定期清理已删除的过期数据
- 使用数据完整性检查SQL验证统计数据准确性 - 使用数据完整性检查 SQL 验证统计数据准确性
## 兼容性与注意事项
- 本模块使用 PageHelper(优惠券相关)与 MyBatis‑Plus(券码/一口价等)并存,请根据对应 Service/Mapper 选择分页与查询方式。
- 优惠优先级及叠加规则以各 Provider 与业务配置为准,避免在外层重复实现优先级判断逻辑。
- 若扩展新的优惠类型,务必实现 `IDiscountProvider` 并在 `IDiscountDetectionService` 中完成注册(当前实现通过组件扫描自动注册并排序)。

View File

@@ -1,6 +1,6 @@
package com.ycwl.basic.pricing.controller; package com.ycwl.basic.pricing.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2;
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
@@ -42,8 +42,8 @@ public class VoucherManagementController {
} }
@PostMapping("/batch/list") @PostMapping("/batch/list")
public ApiResponse<Page<VoucherBatchResp>> getBatchList(@RequestBody VoucherBatchQueryReq req) { public ApiResponse<PageInfo<VoucherBatchResp>> getBatchList(@RequestBody VoucherBatchQueryReq req) {
Page<VoucherBatchResp> page = voucherBatchService.queryBatchList(req); PageInfo<VoucherBatchResp> page = voucherBatchService.queryBatchList(req);
return ApiResponse.success(page); return ApiResponse.success(page);
} }
@@ -66,8 +66,8 @@ public class VoucherManagementController {
} }
@PostMapping("/codes") @PostMapping("/codes")
public ApiResponse<Page<VoucherCodeResp>> getCodeList(@RequestBody VoucherCodeQueryReq req) { public ApiResponse<PageInfo<VoucherCodeResp>> getCodeList(@RequestBody VoucherCodeQueryReq req) {
Page<VoucherCodeResp> page = voucherCodeService.queryCodeList(req); PageInfo<VoucherCodeResp> page = voucherCodeService.queryCodeList(req);
return ApiResponse.success(page); return ApiResponse.success(page);
} }
@@ -78,14 +78,14 @@ public class VoucherManagementController {
} }
@GetMapping("/scenic/{scenicId}/users") @GetMapping("/scenic/{scenicId}/users")
public ApiResponse<Page<VoucherCodeResp>> getUsersInScenic(@PathVariable Long scenicId, public ApiResponse<PageInfo<VoucherCodeResp>> getUsersInScenic(@PathVariable Long scenicId,
@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) { @RequestParam(defaultValue = "10") Integer pageSize) {
VoucherCodeQueryReq req = new VoucherCodeQueryReq(); VoucherCodeQueryReq req = new VoucherCodeQueryReq();
req.setScenicId(scenicId); req.setScenicId(scenicId);
req.setPageNum(pageNum); req.setPageNum(pageNum);
req.setPageSize(pageSize); req.setPageSize(pageSize);
Page<VoucherCodeResp> page = voucherCodeService.queryCodeList(req); PageInfo<VoucherCodeResp> page = voucherCodeService.queryCodeList(req);
return ApiResponse.success(page); return ApiResponse.success(page);
} }

View File

@@ -1,6 +1,6 @@
package com.ycwl.basic.pricing.controller; package com.ycwl.basic.pricing.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq; import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq;
import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp; import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp;
@@ -24,9 +24,9 @@ public class VoucherUsageController {
private final IVoucherUsageService voucherUsageService; private final IVoucherUsageService voucherUsageService;
@PostMapping("/history") @PostMapping("/history")
public ApiResponse<Page<VoucherUsageRecordResp>> getUsageHistory(@RequestBody VoucherUsageHistoryReq req) { public ApiResponse<PageInfo<VoucherUsageRecordResp>> getUsageHistory(@RequestBody VoucherUsageHistoryReq req) {
try { try {
Page<VoucherUsageRecordResp> result = voucherUsageService.getUsageHistory(req); PageInfo<VoucherUsageRecordResp> result = voucherUsageService.getUsageHistory(req);
return ApiResponse.success(result); return ApiResponse.success(result);
} catch (Exception e) { } catch (Exception e) {
log.error("查询券码使用记录失败", e); log.error("查询券码使用记录失败", e);

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.pricing.dto; package com.ycwl.basic.pricing.dto;
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;
@@ -55,16 +56,18 @@ public class CouponClaimResult {
* 景区ID * 景区ID
*/ */
private String scenicId; private String scenicId;
private PriceCouponConfig coupon;
/** /**
* 创建成功结果 * 创建成功结果
*/ */
public static CouponClaimResult success(PriceCouponClaimRecord record, String couponName) { public static CouponClaimResult success(PriceCouponClaimRecord record, PriceCouponConfig coupon) {
CouponClaimResult result = new CouponClaimResult(); CouponClaimResult result = new CouponClaimResult();
result.coupon = coupon;
result.success = true; result.success = true;
result.claimRecordId = record.getId(); result.claimRecordId = record.getId();
result.couponId = record.getCouponId(); result.couponId = record.getCouponId();
result.couponName = couponName; result.couponName = coupon.getCouponName();
result.claimTime = record.getClaimTime(); result.claimTime = record.getClaimTime();
result.userId = record.getUserId(); result.userId = record.getUserId();
result.scenicId = record.getScenicId(); result.scenicId = record.getScenicId();

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List; import java.util.List;
@@ -19,6 +21,11 @@ public interface PriceVoucherBatchConfigMapper extends BaseMapper<PriceVoucherBa
* @param brokerId 推客ID * @param brokerId 推客ID
* @return 批次列表 * @return 批次列表
*/ */
@Select("SELECT id, batch_name, scenic_id, broker_id, discount_type, discount_value, applicable_products, " +
"total_count, used_count, claimed_count, status, create_time, update_time, " +
"create_by, update_by, deleted, deleted_at " +
"FROM price_voucher_batch_config WHERE scenic_id = #{scenicId} AND broker_id = #{brokerId} " +
"AND status = 1 AND deleted = 0 ORDER BY create_time DESC")
List<PriceVoucherBatchConfig> selectActiveBatchesByScenicAndBroker(@Param("scenicId") Long scenicId, List<PriceVoucherBatchConfig> selectActiveBatchesByScenicAndBroker(@Param("scenicId") Long scenicId,
@Param("brokerId") Long brokerId); @Param("brokerId") Long brokerId);
@@ -28,6 +35,8 @@ public interface PriceVoucherBatchConfigMapper extends BaseMapper<PriceVoucherBa
* @param increment 增量(可为负数) * @param increment 增量(可为负数)
* @return 影响行数 * @return 影响行数
*/ */
@Update("UPDATE price_voucher_batch_config SET claimed_count = claimed_count + #{increment}, " +
"update_time = NOW() WHERE id = #{batchId} AND deleted = 0")
int updateClaimedCount(@Param("batchId") Long batchId, @Param("increment") Integer increment); int updateClaimedCount(@Param("batchId") Long batchId, @Param("increment") Integer increment);
/** /**
@@ -36,6 +45,8 @@ public interface PriceVoucherBatchConfigMapper extends BaseMapper<PriceVoucherBa
* @param increment 增量(可为负数) * @param increment 增量(可为负数)
* @return 影响行数 * @return 影响行数
*/ */
@Update("UPDATE price_voucher_batch_config SET used_count = used_count + #{increment}, " +
"update_time = NOW() WHERE id = #{batchId} AND deleted = 0")
int updateUsedCount(@Param("batchId") Long batchId, @Param("increment") Integer increment); int updateUsedCount(@Param("batchId") Long batchId, @Param("increment") Integer increment);
/** /**
@@ -43,6 +54,10 @@ public interface PriceVoucherBatchConfigMapper extends BaseMapper<PriceVoucherBa
* @param batchId 批次ID * @param batchId 批次ID
* @return 统计信息 * @return 统计信息
*/ */
@Select("SELECT id, batch_name, scenic_id, broker_id, discount_type, discount_value, applicable_products, " +
"total_count, used_count, claimed_count, status, create_time, update_time, " +
"create_by, update_by, deleted, deleted_at " +
"FROM price_voucher_batch_config WHERE id = #{batchId} AND deleted = 0")
PriceVoucherBatchConfig selectBatchStats(@Param("batchId") Long batchId); PriceVoucherBatchConfig selectBatchStats(@Param("batchId") Long batchId);
/** /**
@@ -50,5 +65,10 @@ public interface PriceVoucherBatchConfigMapper extends BaseMapper<PriceVoucherBa
* @param voucherCode 券码 * @param voucherCode 券码
* @return 券码批次配置 * @return 券码批次配置
*/ */
@Select("SELECT b.id, b.batch_name, b.scenic_id, b.broker_id, b.discount_type, b.discount_value, b.applicable_products, " +
"b.total_count, b.used_count, b.claimed_count, b.status, b.create_time, b.update_time, " +
"b.create_by, b.update_by, b.deleted, b.deleted_at " +
"FROM price_voucher_batch_config b INNER JOIN price_voucher_code c ON b.id = c.batch_id " +
"WHERE c.code = #{voucherCode} AND b.deleted = 0 AND c.deleted = 0")
PriceVoucherBatchConfig selectByVoucherCode(@Param("voucherCode") String voucherCode); PriceVoucherBatchConfig selectByVoucherCode(@Param("voucherCode") String voucherCode);
} }

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycwl.basic.pricing.entity.PriceVoucherCode; import com.ycwl.basic.pricing.entity.PriceVoucherCode;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -19,6 +21,9 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param code 券码 * @param code 券码
* @return 券码信息 * @return 券码信息
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE code = #{code} AND deleted = 0 LIMIT 1")
PriceVoucherCode selectByCode(@Param("code") String code); PriceVoucherCode selectByCode(@Param("code") String code);
/** /**
@@ -27,6 +32,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param scenicId 景区ID * @param scenicId 景区ID
* @return 数量 * @return 数量
*/ */
@Select("SELECT COUNT(1) FROM price_voucher_code WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND deleted = 0")
Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId);
/** /**
@@ -35,6 +41,10 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param scenicId 景区ID * @param scenicId 景区ID
* @return 券码列表 * @return 券码列表
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND status = 1 AND deleted = 0 " +
"ORDER BY claimed_time DESC")
List<PriceVoucherCode> selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId, List<PriceVoucherCode> selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId,
@Param("scenicId") Long scenicId); @Param("scenicId") Long scenicId);
@@ -44,6 +54,9 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param limit 限制数量 * @param limit 限制数量
* @return 券码列表 * @return 券码列表
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT #{limit}")
List<PriceVoucherCode> selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId, List<PriceVoucherCode> selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId,
@Param("limit") Integer limit); @Param("limit") Integer limit);
@@ -54,6 +67,8 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param claimedTime 领取时间 * @param claimedTime 领取时间
* @return 影响行数 * @return 影响行数
*/ */
@Update("UPDATE price_voucher_code SET status = 1, face_id = #{faceId}, claimed_time = #{claimedTime}, " +
"update_time = NOW() WHERE id = #{id} AND status = 0 AND deleted = 0")
int claimVoucher(@Param("id") Long id, int claimVoucher(@Param("id") Long id,
@Param("faceId") Long faceId, @Param("faceId") Long faceId,
@Param("claimedTime") LocalDateTime claimedTime); @Param("claimedTime") LocalDateTime claimedTime);
@@ -63,6 +78,9 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param batchId 批次ID * @param batchId 批次ID
* @return 券码列表 * @return 券码列表
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND deleted = 0 ORDER BY create_time DESC")
List<PriceVoucherCode> selectByBatchId(@Param("batchId") Long batchId); List<PriceVoucherCode> selectByBatchId(@Param("batchId") Long batchId);
/** /**
@@ -71,6 +89,13 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param scenicId 景区ID(可选) * @param scenicId 景区ID(可选)
* @return 券码列表 * @return 券码列表
*/ */
@Select("<script>" +
"SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE face_id = #{faceId}" +
"<if test='scenicId != null'> AND scenic_id = #{scenicId}</if>" +
" AND deleted = 0 ORDER BY claimed_time DESC" +
"</script>")
List<PriceVoucherCode> selectUserVouchers(@Param("faceId") Long faceId, List<PriceVoucherCode> selectUserVouchers(@Param("faceId") Long faceId,
@Param("scenicId") Long scenicId); @Param("scenicId") Long scenicId);
@@ -79,6 +104,9 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param batchId 批次ID * @param batchId 批次ID
* @return 可用券码 * @return 可用券码
*/ */
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1")
PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId); PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId);
/** /**
@@ -86,5 +114,10 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param scenicId 景区ID * @param scenicId 景区ID
* @return 可用券码 * @return 可用券码
*/ */
@Select("SELECT pvc.id, pvc.batch_id, pvc.scenic_id, pvc.code, pvc.status, pvc.face_id, pvc.claimed_time, pvc.used_time, " +
"pvc.current_use_count, pvc.last_used_time, pvc.remark, pvc.create_time, pvc.update_time, pvc.deleted, pvc.deleted_at " +
"FROM price_voucher_code pvc WHERE pvc.scenic_id = #{scenicId} AND pvc.status = 0 AND pvc.deleted = 0 " +
"AND NOT EXISTS (SELECT 1 FROM voucher_print_record vpr WHERE vpr.voucher_code_id = pvc.id AND vpr.deleted = 0) " +
"ORDER BY RAND() LIMIT 1")
PriceVoucherCode findRandomUnprintedVoucher(@Param("scenicId") Long scenicId); PriceVoucherCode findRandomUnprintedVoucher(@Param("scenicId") Long scenicId);
} }

View File

@@ -1,7 +1,6 @@
package com.ycwl.basic.pricing.mapper; package com.ycwl.basic.pricing.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ycwl.basic.pricing.entity.PriceVoucherUsageRecord; import com.ycwl.basic.pricing.entity.PriceVoucherUsageRecord;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -121,16 +120,15 @@ public interface PriceVoucherUsageRecordMapper extends BaseMapper<PriceVoucherUs
Date getLastUseTimeByFaceIdAndBatchId(@Param("faceId") Long faceId, @Param("batchId") Long batchId); Date getLastUseTimeByFaceIdAndBatchId(@Param("faceId") Long faceId, @Param("batchId") Long batchId);
/** /**
* 分页查询券码使用记录 * 查询券码使用记录(用于PageHelper分页)
* *
* @param page 分页参数
* @param batchId 批次ID(可选) * @param batchId 批次ID(可选)
* @param voucherCode 券码(可选) * @param voucherCode 券码(可选)
* @param faceId 用户faceId(可选) * @param faceId 用户faceId(可选)
* @param scenicId 景区ID(可选) * @param scenicId 景区ID(可选)
* @param startTime 开始时间(可选) * @param startTime 开始时间(可选)
* @param endTime 结束时间(可选) * @param endTime 结束时间(可选)
* @return 分页结果 * @return 使用记录列表
*/ */
@Select("<script>" + @Select("<script>" +
"SELECT * FROM price_voucher_usage_record WHERE deleted = 0" + "SELECT * FROM price_voucher_usage_record WHERE deleted = 0" +
@@ -142,8 +140,7 @@ public interface PriceVoucherUsageRecordMapper extends BaseMapper<PriceVoucherUs
"<if test=\"endTime != null\">AND use_time &lt;= #{endTime}</if>" + "<if test=\"endTime != null\">AND use_time &lt;= #{endTime}</if>" +
"ORDER BY use_time DESC" + "ORDER BY use_time DESC" +
"</script>") "</script>")
Page<PriceVoucherUsageRecord> selectPageWithConditions(Page<PriceVoucherUsageRecord> page, List<PriceVoucherUsageRecord> selectListWithConditions(@Param("batchId") Long batchId,
@Param("batchId") Long batchId,
@Param("voucherCode") String voucherCode, @Param("voucherCode") String voucherCode,
@Param("faceId") Long faceId, @Param("faceId") Long faceId,
@Param("scenicId") Long scenicId, @Param("scenicId") Long scenicId,

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycwl.basic.pricing.entity.VoucherPrintRecord; import com.ycwl.basic.pricing.entity.VoucherPrintRecord;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
/** /**
* 优惠券打印记录Mapper * 优惠券打印记录Mapper
@@ -17,6 +19,9 @@ public interface VoucherPrintRecordMapper extends BaseMapper<VoucherPrintRecord>
* @param scenicId 景区ID * @param scenicId 景区ID
* @return 打印记录 * @return 打印记录
*/ */
@Select("SELECT id, code, face_id, broker_id, scenic_id, voucher_code_id, voucher_code, " +
"print_status, error_message, create_time, update_time, deleted, deleted_at " +
"FROM voucher_print_record WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND deleted = 0 LIMIT 1")
VoucherPrintRecord selectByFaceBrokerScenic(@Param("faceId") Long faceId, VoucherPrintRecord selectByFaceBrokerScenic(@Param("faceId") Long faceId,
@Param("scenicId") Long scenicId); @Param("scenicId") Long scenicId);
@@ -25,6 +30,9 @@ public interface VoucherPrintRecordMapper extends BaseMapper<VoucherPrintRecord>
* @param voucherCodeId 券码ID * @param voucherCodeId 券码ID
* @return 打印记录 * @return 打印记录
*/ */
@Select("SELECT id, code, face_id, broker_id, scenic_id, voucher_code_id, voucher_code, " +
"print_status, error_message, create_time, update_time, deleted, deleted_at " +
"FROM voucher_print_record WHERE voucher_code_id = #{voucherCodeId} AND deleted = 0 LIMIT 1")
VoucherPrintRecord selectByVoucherCodeId(@Param("voucherCodeId") Long voucherCodeId); VoucherPrintRecord selectByVoucherCodeId(@Param("voucherCodeId") Long voucherCodeId);
/** /**
@@ -34,6 +42,8 @@ public interface VoucherPrintRecordMapper extends BaseMapper<VoucherPrintRecord>
* @param errorMessage 错误信息(可为null) * @param errorMessage 错误信息(可为null)
* @return 影响行数 * @return 影响行数
*/ */
@Update("UPDATE voucher_print_record SET print_status = #{printStatus}, error_message = #{errorMessage}, " +
"update_time = NOW() WHERE id = #{id}")
int updatePrintStatus(@Param("id") Long id, int updatePrintStatus(@Param("id") Long id,
@Param("printStatus") Integer printStatus, @Param("printStatus") Integer printStatus,
@Param("errorMessage") String errorMessage); @Param("errorMessage") String errorMessage);

View File

@@ -1,6 +1,6 @@
package com.ycwl.basic.pricing.service; package com.ycwl.basic.pricing.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq; import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq;
import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp; import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp;
import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp; import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp;
@@ -18,7 +18,7 @@ public interface IVoucherUsageService {
* @param req 查询请求 * @param req 查询请求
* @return 分页结果 * @return 分页结果
*/ */
Page<VoucherUsageRecordResp> getUsageHistory(VoucherUsageHistoryReq req); PageInfo<VoucherUsageRecordResp> getUsageHistory(VoucherUsageHistoryReq req);
/** /**
* 获取指定券码的使用记录 * 获取指定券码的使用记录

View File

@@ -1,6 +1,6 @@
package com.ycwl.basic.pricing.service; package com.ycwl.basic.pricing.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2;
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
@@ -17,7 +17,7 @@ public interface VoucherBatchService {
*/ */
Long createBatchV2(VoucherBatchCreateReqV2 req); Long createBatchV2(VoucherBatchCreateReqV2 req);
Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req); PageInfo<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req);
VoucherBatchResp getBatchDetail(Long id); VoucherBatchResp getBatchDetail(Long id);

View File

@@ -1,6 +1,6 @@
package com.ycwl.basic.pricing.service; package com.ycwl.basic.pricing.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
@@ -13,7 +13,7 @@ public interface VoucherCodeService {
VoucherCodeResp claimVoucher(VoucherClaimReq req); VoucherCodeResp claimVoucher(VoucherClaimReq req);
Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req); PageInfo<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req);
List<VoucherCodeResp> getMyVoucherCodes(Long faceId); List<VoucherCodeResp> getMyVoucherCodes(Long faceId);

View File

@@ -274,7 +274,7 @@ public class CouponServiceImpl implements ICouponService {
request.getUserId(), request.getCouponId(), claimRecord.getId()); request.getUserId(), request.getCouponId(), claimRecord.getId());
// 10. 返回成功结果 // 10. 返回成功结果
return CouponClaimResult.success(claimRecord, coupon.getCouponName()); return CouponClaimResult.success(claimRecord, coupon);
} catch (Exception e) { } catch (Exception e) {
log.error("领取优惠券失败: userId={}, couponId={}", log.error("领取优惠券失败: userId={}, couponId={}",

View File

@@ -1,7 +1,8 @@
package com.ycwl.basic.pricing.service.impl; package com.ycwl.basic.pricing.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.exception.BizException; import com.ycwl.basic.exception.BizException;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
@@ -153,8 +154,8 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
} }
@Override @Override
public Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) { public PageInfo<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
Page<PriceVoucherBatchConfig> page = new Page<>(req.getPageNum(), req.getPageSize()); PageHelper.startPage(req.getPageNum(), req.getPageSize());
LambdaQueryWrapper<PriceVoucherBatchConfig> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<PriceVoucherBatchConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PriceVoucherBatchConfig::getDeleted, 0) wrapper.eq(PriceVoucherBatchConfig::getDeleted, 0)
@@ -164,14 +165,10 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
.like(StringUtils.hasText(req.getBatchName()), PriceVoucherBatchConfig::getBatchName, req.getBatchName()) .like(StringUtils.hasText(req.getBatchName()), PriceVoucherBatchConfig::getBatchName, req.getBatchName())
.orderByDesc(PriceVoucherBatchConfig::getCreateTime); .orderByDesc(PriceVoucherBatchConfig::getCreateTime);
Page<PriceVoucherBatchConfig> entityPage = voucherBatchMapper.selectPage(page, wrapper); java.util.List<PriceVoucherBatchConfig> list = voucherBatchMapper.selectList(wrapper);
java.util.List<VoucherBatchResp> respList = list.stream().map(this::convertToResp).toList();
Page<VoucherBatchResp> respPage = new Page<>(); return new PageInfo<>(respList);
BeanUtils.copyProperties(entityPage, respPage);
respPage.setRecords(entityPage.getRecords().stream().map(this::convertToResp).toList());
return respPage;
} }
@Override @Override

View File

@@ -1,7 +1,8 @@
package com.ycwl.basic.pricing.service.impl; package com.ycwl.basic.pricing.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.exception.BizException; import com.ycwl.basic.exception.BizException;
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
@@ -124,8 +125,8 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
} }
@Override @Override
public Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) { public PageInfo<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) {
Page<PriceVoucherCode> page = new Page<>(req.getPageNum(), req.getPageSize()); PageHelper.startPage(req.getPageNum(), req.getPageSize());
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PriceVoucherCode::getDeleted, 0) wrapper.eq(PriceVoucherCode::getDeleted, 0)
@@ -136,19 +137,15 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
.like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode()) .like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode())
.orderByDesc(PriceVoucherCode::getId); .orderByDesc(PriceVoucherCode::getId);
Page<PriceVoucherCode> entityPage = voucherCodeMapper.selectPage(page, wrapper); List<PriceVoucherCode> list = voucherCodeMapper.selectList(wrapper);
Page<VoucherCodeResp> respPage = new Page<>();
BeanUtils.copyProperties(entityPage, respPage);
List<VoucherCodeResp> respList = new ArrayList<>(); List<VoucherCodeResp> respList = new ArrayList<>();
for (PriceVoucherCode code : entityPage.getRecords()) { for (PriceVoucherCode code : list) {
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId()); PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId());
respList.add(convertToResp(code, batch)); respList.add(convertToResp(code, batch));
} }
respPage.setRecords(respList);
return respPage; return new PageInfo<>(respList);
} }
@Override @Override

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.pricing.service.impl; package com.ycwl.basic.pricing.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq; import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq;
import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp; import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp;
import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp; import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp;
@@ -36,24 +37,20 @@ public class VoucherUsageServiceImpl implements IVoucherUsageService {
private final PriceVoucherBatchConfigMapper batchConfigMapper; private final PriceVoucherBatchConfigMapper batchConfigMapper;
@Override @Override
public Page<VoucherUsageRecordResp> getUsageHistory(VoucherUsageHistoryReq req) { public PageInfo<VoucherUsageRecordResp> getUsageHistory(VoucherUsageHistoryReq req) {
Page<PriceVoucherUsageRecord> page = new Page<>(req.getPageNum(), req.getPageSize()); PageHelper.startPage(req.getPageNum(), req.getPageSize());
Page<PriceVoucherUsageRecord> entityPage = usageRecordMapper.selectPageWithConditions( List<PriceVoucherUsageRecord> list = usageRecordMapper.selectListWithConditions(
page, req.getBatchId(), req.getVoucherCode(), req.getFaceId(), req.getBatchId(), req.getVoucherCode(), req.getFaceId(),
req.getScenicId(), req.getStartTime(), req.getEndTime()); req.getScenicId(), req.getStartTime(), req.getEndTime());
Page<VoucherUsageRecordResp> respPage = new Page<>();
BeanUtils.copyProperties(entityPage, respPage);
List<VoucherUsageRecordResp> respList = new ArrayList<>(); List<VoucherUsageRecordResp> respList = new ArrayList<>();
for (PriceVoucherUsageRecord record : entityPage.getRecords()) { for (PriceVoucherUsageRecord record : list) {
VoucherUsageRecordResp resp = convertToResp(record); VoucherUsageRecordResp resp = convertToResp(record);
respList.add(resp); respList.add(resp);
} }
respPage.setRecords(respList);
return respPage; return new PageInfo<>(respList);
} }
@Override @Override
@@ -140,8 +137,81 @@ public class VoucherUsageServiceImpl implements IVoucherUsageService {
@Override @Override
public List<VoucherUsageStatsResp> getBatchUsageStats(Long batchId) { public List<VoucherUsageStatsResp> getBatchUsageStats(Long batchId) {
// 这里可以实现批次统计,暂时返回空列表 if (batchId == null) {
return new ArrayList<>(); return new ArrayList<>();
}
// 查询批次配置
PriceVoucherBatchConfig batchConfig = batchConfigMapper.selectById(batchId);
if (batchConfig == null || batchConfig.getDeleted() == 1) {
log.warn("批次配置不存在或已删除: batchId={}", batchId);
return new ArrayList<>();
}
// 查询该批次下的所有券码
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectByBatchId(batchId);
if (voucherCodes == null || voucherCodes.isEmpty()) {
log.info("批次下无券码数据: batchId={}", batchId);
return new ArrayList<>();
}
List<VoucherUsageStatsResp> statsList = new ArrayList<>();
for (PriceVoucherCode voucherCode : voucherCodes) {
if (voucherCode.getDeleted() == 1) {
continue;
}
VoucherUsageStatsResp stats = new VoucherUsageStatsResp();
stats.setVoucherCodeId(voucherCode.getId());
stats.setVoucherCode(voucherCode.getCode());
stats.setBatchId(batchConfig.getId());
stats.setBatchName(batchConfig.getBatchName());
stats.setScenicId(voucherCode.getScenicId());
stats.setStatus(voucherCode.getStatus());
// 设置状态名称
VoucherCodeStatus statusEnum = VoucherCodeStatus.getByCode(voucherCode.getStatus());
if (statusEnum != null) {
stats.setStatusName(statusEnum.getName());
}
// 设置使用次数相关信息
Integer currentUseCount = voucherCode.getCurrentUseCount() != null ?
voucherCode.getCurrentUseCount() : 0;
stats.setCurrentUseCount(currentUseCount);
stats.setMaxUseCount(batchConfig.getMaxUseCount());
stats.setMaxUsePerUser(batchConfig.getMaxUsePerUser());
stats.setUseIntervalHours(batchConfig.getUseIntervalHours());
// 计算是否还能使用
boolean canUseMore = true;
if (batchConfig.getMaxUseCount() != null) {
canUseMore = currentUseCount < batchConfig.getMaxUseCount();
}
stats.setCanUseMore(canUseMore);
// 计算剩余使用次数
if (batchConfig.getMaxUseCount() != null) {
int remaining = batchConfig.getMaxUseCount() - currentUseCount;
stats.setRemainingUseCount(Math.max(0, remaining));
}
// 获取使用记录数
List<PriceVoucherUsageRecord> usageRecords = usageRecordMapper.selectByVoucherCodeId(voucherCode.getId());
stats.setTotalUsageRecords(usageRecords != null ? usageRecords.size() : 0);
// 格式化最后使用时间
if (voucherCode.getLastUsedTime() != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
stats.setLastUsedTime(sdf.format(voucherCode.getLastUsedTime()));
}
statsList.add(stats);
}
log.info("批次统计完成: batchId={}, 券码数量={}", batchId, statsList.size());
return statsList;
} }
@Override @Override

View File

@@ -12,6 +12,7 @@ import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem; import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.ProjectMapper;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.StatisticsMapper; import com.ycwl.basic.mapper.StatisticsMapper;
import com.ycwl.basic.mapper.FaceMapper; import com.ycwl.basic.mapper.FaceMapper;
@@ -32,6 +33,7 @@ import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO; import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.mp.MpConfigEntity; import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager; import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery; import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
@@ -129,6 +131,8 @@ public class FaceServiceImpl implements FaceService {
private FaceSampleMapper faceSampleMapper; private FaceSampleMapper faceSampleMapper;
@Autowired @Autowired
private GoodsService goodsService; private GoodsService goodsService;
@Autowired
private ProjectMapper projectMapper;
@Override @Override
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) { public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
@@ -904,6 +908,35 @@ public class FaceServiceImpl implements FaceService {
if (face == null) { if (face == null) {
return false; return false;
} }
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
Integer maxTourTime = scenicConfig.getInteger("tour_time");
Integer minTourTime = scenicConfig.getInteger("tour_min_time");
boolean tourMatch = false;
if (maxTourTime != null && minTourTime != null) {
if ((new Date().getTime()) - face.getCreateAt().getTime() < maxTourTime * 60 * 1000
&& (new Date().getTime()) - face.getCreateAt().getTime() > minTourTime * 60 * 1000) {
tourMatch = true;
}
}
// 判断是否项目匹配
boolean projectMatch = false;
if (maxTourTime != null) {
List<Long> projectIdListForUser = statisticsMapper.getProjectIdListForUser(face.getMemberId(), new Date((new Date().getTime()) - maxTourTime * 60 * 1000), new Date());
if (projectIdListForUser != null && !projectIdListForUser.isEmpty()) {
Long projectId = projectIdListForUser.getFirst();
ProjectRespVO projectRespVO = projectMapper.getById(projectId);
if (projectRespVO != null) {
Integer maxPlayTime = projectRespVO.getMaxPlayTime();
Integer minPlayTime = projectRespVO.getMinPlayTime();
if (maxPlayTime != null && minPlayTime != null) {
if ((new Date().getTime()) - face.getCreateAt().getTime() < maxPlayTime * 60 * 1000
&& (new Date().getTime()) - face.getCreateAt().getTime() > minPlayTime * 60 * 1000) {
projectMatch = true;
}
}
}
}
}
String countKey = FACE_RECOGNITION_COUNT_PFX + faceId; String countKey = FACE_RECOGNITION_COUNT_PFX + faceId;
String countStr = redisTemplate.opsForValue().get(countKey); String countStr = redisTemplate.opsForValue().get(countKey);
long recognitionCount = 0L; long recognitionCount = 0L;
@@ -914,10 +947,26 @@ public class FaceServiceImpl implements FaceService {
log.warn("识别次数解析失败,faceId={}, count={}", faceId, countStr); log.warn("识别次数解析失败,faceId={}, count={}", faceId, countStr);
} }
} }
int ruleMatched = 0;
if (recognitionCount > 1) {
ruleMatched++;
}
if (tourMatch) {
ruleMatched++;
}
if (projectMatch) {
ruleMatched++;
}
// 查询是否触发过低阈值检测 // 查询是否触发过低阈值检测
String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId; String lowThresholdKey = FACE_LOW_THRESHOLD_PFX + faceId;
Boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey); boolean hasLowThreshold = redisTemplate.hasKey(lowThresholdKey);
return true; Integer mode = scenicConfig.getInteger("re_match_mode");
return switch (mode) {
case 1 -> tourMatch || recognitionCount > 1 || hasLowThreshold;
case 5 -> hasLowThreshold || (ruleMatched >= 2);
case 9 -> hasLowThreshold && ruleMatched >= 2;
default -> false;
};
} }
@Override @Override

View File

@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig">
<id column="id" property="id" />
<result column="batch_name" property="batchName" />
<result column="scenic_id" property="scenicId" />
<result column="broker_id" property="brokerId" />
<result column="discount_type" property="discountType" />
<result column="discount_value" property="discountValue" />
<result column="applicable_products" property="applicableProductsJson" />
<result column="total_count" property="totalCount" />
<result column="used_count" property="usedCount" />
<result column="claimed_count" property="claimedCount" />
<result column="status" property="status" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="create_by" property="createBy" />
<result column="update_by" property="updateBy" />
<result column="deleted" property="deleted" />
<result column="deleted_at" property="deletedAt" />
</resultMap>
<!-- 基础字段 -->
<sql id="Base_Column_List">
id, batch_name, scenic_id, broker_id, discount_type, discount_value, applicable_products,
total_count, used_count, claimed_count, status, create_time, update_time,
create_by, update_by, deleted, deleted_at
</sql>
<!-- 根据景区ID和推客ID查询有效的批次列表 -->
<select id="selectActiveBatchesByScenicAndBroker" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM price_voucher_batch_config
WHERE scenic_id = #{scenicId}
AND broker_id = #{brokerId}
AND status = 1
AND deleted = 0
ORDER BY create_time DESC
</select>
<!-- 更新批次的已领取数量 -->
<update id="updateClaimedCount">
UPDATE price_voucher_batch_config
SET claimed_count = claimed_count + #{increment},
update_time = NOW()
WHERE id = #{batchId}
AND deleted = 0
</update>
<!-- 更新批次的已使用数量 -->
<update id="updateUsedCount">
UPDATE price_voucher_batch_config
SET used_count = used_count + #{increment},
update_time = NOW()
WHERE id = #{batchId}
AND deleted = 0
</update>
<!-- 获取批次统计信息 -->
<select id="selectBatchStats" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM price_voucher_batch_config
WHERE id = #{batchId}
AND deleted = 0
</select>
<!-- 根据券码查询对应的券码批次配置 -->
<select id="selectByVoucherCode" resultMap="BaseResultMap">
SELECT
b.id, b.batch_name, b.scenic_id, b.broker_id, b.discount_type, b.discount_value, b.applicable_products,
b.total_count, b.used_count, b.claimed_count, b.status, b.create_time, b.update_time,
b.create_by, b.update_by, b.deleted, b.deleted_at
FROM price_voucher_batch_config b
INNER JOIN price_voucher_code c ON b.id = c.batch_id
WHERE c.code = #{voucherCode}
AND b.deleted = 0
AND c.deleted = 0
</select>
</mapper>

View File

@@ -1,119 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper">
<resultMap id="BaseResultMap" type="com.ycwl.basic.pricing.entity.PriceVoucherCode">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="batch_id" property="batchId" jdbcType="BIGINT"/>
<result column="scenic_id" property="scenicId" jdbcType="BIGINT"/>
<result column="code" property="code" jdbcType="VARCHAR"/>
<result column="status" property="status" jdbcType="TINYINT"/>
<result column="face_id" property="faceId" jdbcType="BIGINT"/>
<result column="claimed_time" property="claimedTime" jdbcType="TIMESTAMP"/>
<result column="used_time" property="usedTime" jdbcType="TIMESTAMP"/>
<result column="current_use_count" property="currentUseCount" jdbcType="INTEGER"/>
<result column="last_used_time" property="lastUsedTime" jdbcType="TIMESTAMP"/>
<result column="remark" property="remark" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="TINYINT"/>
<result column="deleted_at" property="deletedAt" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time,
current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at
</sql>
<select id="selectByCode" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM price_voucher_code
WHERE code = #{code}
AND deleted = 0
LIMIT 1
</select>
<select id="countByFaceIdAndScenicId" resultType="java.lang.Integer">
SELECT COUNT(1)
FROM price_voucher_code
WHERE face_id = #{faceId}
AND scenic_id = #{scenicId}
AND deleted = 0
</select>
<select id="selectAvailableVouchersByFaceIdAndScenicId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM price_voucher_code
WHERE face_id = #{faceId}
AND scenic_id = #{scenicId}
AND status = 1
AND deleted = 0
ORDER BY claimed_time DESC
</select>
<select id="selectUnclaimedVouchersByBatchId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM price_voucher_code
WHERE batch_id = #{batchId}
AND status = 0
AND deleted = 0
LIMIT #{limit}
</select>
<update id="claimVoucher">
UPDATE price_voucher_code
SET status = 1,
face_id = #{faceId},
claimed_time = #{claimedTime},
update_time = NOW()
WHERE id = #{id}
AND status = 0
AND deleted = 0
</update>
<select id="selectByBatchId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM price_voucher_code
WHERE batch_id = #{batchId}
AND deleted = 0
ORDER BY create_time DESC
</select>
<select id="selectUserVouchers" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM price_voucher_code
WHERE face_id = #{faceId}
<if test="scenicId != null">
AND scenic_id = #{scenicId}
</if>
AND deleted = 0
ORDER BY claimed_time DESC
</select>
<select id="findFirstAvailableByBatchId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM price_voucher_code
WHERE batch_id = #{batchId}
AND status = 0
AND deleted = 0
LIMIT 1
</select>
<select id="findRandomUnprintedVoucher" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM price_voucher_code pvc
WHERE pvc.scenic_id = #{scenicId}
AND pvc.status = 0
AND pvc.deleted = 0
AND NOT EXISTS (
SELECT 1 FROM voucher_print_record vpr
WHERE vpr.voucher_code_id = pvc.id
AND vpr.deleted = 0
)
ORDER BY RAND()
LIMIT 1
</select>
</mapper>

View File

@@ -525,5 +525,10 @@
</if> </if>
) stats_data ) stats_data
</select> </select>
<select id="getProjectIdListForUser" resultType="java.lang.Long">
select identifier from t_stats_record r left join t_stats s on r.trace_id = s.trace_id where s.member_id = #{memberId}
and r.action = 'ENTER_PROJECT' and r.create_time &lt; #{endTime} and r.create_time &gt; #{startTime}
order by r.create_time desc limit 1
</select>
</mapper> </mapper>

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.pricing.mapper.VoucherPrintRecordMapper">
<resultMap id="BaseResultMap" type="com.ycwl.basic.pricing.entity.VoucherPrintRecord">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="code" property="code" jdbcType="VARCHAR"/>
<result column="face_id" property="faceId" jdbcType="BIGINT"/>
<result column="broker_id" property="brokerId" jdbcType="BIGINT"/>
<result column="scenic_id" property="scenicId" jdbcType="BIGINT"/>
<result column="voucher_code_id" property="voucherCodeId" jdbcType="BIGINT"/>
<result column="voucher_code" property="voucherCode" jdbcType="VARCHAR"/>
<result column="print_status" property="printStatus" jdbcType="TINYINT"/>
<result column="error_message" property="errorMessage" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="TINYINT"/>
<result column="deleted_at" property="deletedAt" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, code, face_id, broker_id, scenic_id, voucher_code_id, voucher_code,
print_status, error_message, create_time, update_time, deleted, deleted_at
</sql>
<select id="selectByFaceBrokerScenic" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM voucher_print_record
WHERE face_id = #{faceId}
AND scenic_id = #{scenicId}
AND deleted = 0
LIMIT 1
</select>
<select id="selectByVoucherCodeId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM voucher_print_record
WHERE voucher_code_id = #{voucherCodeId}
AND deleted = 0
LIMIT 1
</select>
<update id="updatePrintStatus">
UPDATE voucher_print_record
SET print_status = #{printStatus},
error_message = #{errorMessage},
update_time = NOW()
WHERE id = #{id}
</update>
</mapper>