From f3fdb447429d5c454ee33110bd0df8ef199c5c4b Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 17 Sep 2025 17:03:12 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(mybatis):=20=E7=A7=BB=E9=99=A4=20X?= =?UTF-8?q?ML=20=E9=85=8D=E7=BD=AE=EF=BC=8C=E4=BD=BF=E7=94=A8=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3=E6=9B=BF=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 PriceVoucherBatchConfigMapper、PriceVoucherCodeMapper 和 VoucherPrintRecordMapper 中添加了 @Select 和 @Update 注解 - 删除了对应的 XML 配置文件 - 优化了 SQL 查询,直接在 Java 接口中定义 --- .../mapper/PriceVoucherBatchConfigMapper.java | 22 +++- .../mapper/PriceVoucherCodeMapper.java | 43 ++++++- .../mapper/VoucherPrintRecordMapper.java | 14 ++- .../mapper/PriceVoucherBatchConfigMapper.xml | 85 ------------- .../mapper/PriceVoucherCodeMapper.xml | 119 ------------------ .../mapper/VoucherPrintRecordMapper.xml | 51 -------- 6 files changed, 71 insertions(+), 263 deletions(-) delete mode 100644 src/main/resources/mapper/PriceVoucherBatchConfigMapper.xml delete mode 100644 src/main/resources/mapper/PriceVoucherCodeMapper.xml delete mode 100644 src/main/resources/mapper/VoucherPrintRecordMapper.xml diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherBatchConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherBatchConfigMapper.java index 7b5925a8..72821cff 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherBatchConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherBatchConfigMapper.java @@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; import java.util.List; @@ -19,7 +21,12 @@ public interface PriceVoucherBatchConfigMapper extends BaseMapper selectActiveBatchesByScenicAndBroker(@Param("scenicId") Long scenicId, + @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 selectActiveBatchesByScenicAndBroker(@Param("scenicId") Long scenicId, @Param("brokerId") Long brokerId); /** @@ -28,6 +35,8 @@ public interface PriceVoucherBatchConfigMapper extends BaseMapper { * @param code 券码 * @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); /** @@ -27,6 +32,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @param scenicId 景区ID * @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); /** @@ -35,7 +41,11 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @param scenicId 景区ID * @return 券码列表 */ - List selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId, + @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 selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); /** @@ -44,7 +54,10 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @param limit 限制数量 * @return 券码列表 */ - List selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId, + @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 selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId, @Param("limit") Integer limit); /** @@ -54,8 +67,10 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @param claimedTime 领取时间 * @return 影响行数 */ - int claimVoucher(@Param("id") Long id, - @Param("faceId") Long faceId, + @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, + @Param("faceId") Long faceId, @Param("claimedTime") LocalDateTime claimedTime); /** @@ -63,6 +78,9 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @param batchId 批次ID * @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 selectByBatchId(@Param("batchId") Long batchId); /** @@ -71,7 +89,14 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @param scenicId 景区ID(可选) * @return 券码列表 */ - List selectUserVouchers(@Param("faceId") Long faceId, + @Select("") + List selectUserVouchers(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); /** @@ -79,6 +104,9 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @param batchId 批次ID * @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); /** @@ -86,5 +114,10 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @param scenicId 景区ID * @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); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java index 61a61050..880c8482 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java @@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ycwl.basic.pricing.entity.VoucherPrintRecord; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; /** * 优惠券打印记录Mapper @@ -17,6 +19,9 @@ public interface VoucherPrintRecordMapper extends BaseMapper * @param scenicId 景区ID * @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, @Param("scenicId") Long scenicId); @@ -25,6 +30,9 @@ public interface VoucherPrintRecordMapper extends BaseMapper * @param voucherCodeId 券码ID * @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); /** @@ -34,7 +42,9 @@ public interface VoucherPrintRecordMapper extends BaseMapper * @param errorMessage 错误信息(可为null) * @return 影响行数 */ - int updatePrintStatus(@Param("id") Long id, - @Param("printStatus") Integer printStatus, + @Update("UPDATE voucher_print_record SET print_status = #{printStatus}, error_message = #{errorMessage}, " + + "update_time = NOW() WHERE id = #{id}") + int updatePrintStatus(@Param("id") Long id, + @Param("printStatus") Integer printStatus, @Param("errorMessage") String errorMessage); } \ No newline at end of file diff --git a/src/main/resources/mapper/PriceVoucherBatchConfigMapper.xml b/src/main/resources/mapper/PriceVoucherBatchConfigMapper.xml deleted file mode 100644 index 406657df..00000000 --- a/src/main/resources/mapper/PriceVoucherBatchConfigMapper.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 - - - - - - - - UPDATE price_voucher_batch_config - SET claimed_count = claimed_count + #{increment}, - update_time = NOW() - WHERE id = #{batchId} - AND deleted = 0 - - - - - UPDATE price_voucher_batch_config - SET used_count = used_count + #{increment}, - update_time = NOW() - WHERE id = #{batchId} - AND deleted = 0 - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/mapper/PriceVoucherCodeMapper.xml b/src/main/resources/mapper/PriceVoucherCodeMapper.xml deleted file mode 100644 index ea20b164..00000000 --- a/src/main/resources/mapper/PriceVoucherCodeMapper.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - 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 - - - - - - - - - - - - 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 - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/mapper/VoucherPrintRecordMapper.xml b/src/main/resources/mapper/VoucherPrintRecordMapper.xml deleted file mode 100644 index 8d33bae6..00000000 --- a/src/main/resources/mapper/VoucherPrintRecordMapper.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - id, code, face_id, broker_id, scenic_id, voucher_code_id, voucher_code, - print_status, error_message, create_time, update_time, deleted, deleted_at - - - - - - - - UPDATE voucher_print_record - SET print_status = #{printStatus}, - error_message = #{errorMessage}, - update_time = NOW() - WHERE id = #{id} - - - \ No newline at end of file From 5212547b3acac4897969204f4e624452c0720014 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 17 Sep 2025 17:03:18 +0800 Subject: [PATCH 2/2] docs --- .gitignore | 2 +- CLAUDE.md | 269 ++++++++- .../java/com/ycwl/basic/pricing/CLAUDE.md | 527 ++++++++---------- 3 files changed, 480 insertions(+), 318 deletions(-) diff --git a/.gitignore b/.gitignore index 0532c1f3..bc69398b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .idea/ logs/ target/ - +.serena .claude .vscode \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index c51a0665..800dd2f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,11 +21,20 @@ mvn spring-boot:run # 运行特定测试类 mvn test -Dtest=FaceCleanerTest +# 运行特定测试方法 +mvn test -Dtest=FaceCleanerTest#testSpecificMethod + # 运行特定包的测试 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 -Dsurefire.printSummary=true ``` ### 开发环境配置 @@ -134,12 +143,15 @@ mvn test -DskipTests=false ## 价格查询系统 (Pricing Module) ### 核心架构 -价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理和价格计算功能。 +价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理、券码管理和统一优惠检测功能。 #### 关键组件 -- **PriceCalculationController** (`/api/pricing/calculate`):价格计算API -- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券管理API -- **PricingConfigController** (`/api/pricing/config/`):价格配置管理API +- **PriceCalculationController** (`/api/pricing/calculate`):统一价格计算API,支持自动优惠组合 +- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券配置和统计管理 +- **VoucherManagementController** (`/api/pricing/voucher/`):券码批次和券码管理 +- **VoucherUsageController** (`/api/pricing/voucher/usage/`):券码使用记录和统计 +- **PricingConfigController** (`/api/pricing/config/`):商品价格配置管理 +- **OnePricePurchaseController** (`/api/pricing/admin/one-price/`):一口价配置管理 #### 商品类型支持 ```java @@ -151,12 +163,16 @@ ProductType枚举定义了支持的商品类型: - MACHINE_PRINT: 一体机打印 ``` -#### 价格计算流程 -1. 接收PriceCalculationRequest(包含商品列表和用户ID) +#### 价格计算流程(统一优惠检测) +1. 接收PriceCalculationRequest(包含商品列表、用户ID、券码等) 2. 查找商品基础配置和分层定价 3. 处理套餐商品(BundleProductItem) -4. 自动应用最优优惠券 -5. 返回PriceCalculationResult(包含原价、最终价格、优惠详情) +4. **统一优惠检测**:通过IDiscountDetectionService自动检测所有可用优惠 + - 券码优惠(VoucherDiscountProvider,优先级100) + - 优惠券优惠(CouponDiscountProvider,优先级80) + - 一口价优惠(OnePricePurchaseDiscountProvider,优先级60) +5. **智能优惠组合**:按优先级和叠加规则应用最优优惠组合 +6. 返回PriceCalculationResult(包含原价、最终价格、使用的优惠详情、可用优惠列表) #### 优惠券系统 - **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 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中实现计算逻辑 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 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处理复杂JSON字段: - `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化 ### 测试策略 -- 单元测试:每个服务类都有对应测试类 -- 配置验证测试:DefaultConfigValidationTest验证default配置 -- JSON序列化测试:验证复杂对象的数据库存储 -- 分页功能测试:验证PageHelper集成 +针对pricing模块的全面测试策略: + +#### 单元测试类型 +- **服务层测试**:每个服务类都有对应测试类 + - `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分页集成 +- **异常处理**:验证业务异常和全局异常处理器 ## 关键架构模式 @@ -289,4 +460,68 @@ mvn test -Dtest="com.ycwl.basic.integration.*Test" logging: level: 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 ``` \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md index 088b6f43..1774c367 100644 --- a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md +++ b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md @@ -1,90 +1,75 @@ # 价格查询系统 (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/ -├── controller/ # REST API控制器层 -│ ├── CouponManagementController.java # 优惠券管理API -│ ├── PriceCalculationController.java # 价格计算API -│ └── PricingConfigController.java # 价格配置管理API -├── dto/ # 数据传输对象 -│ ├── BundleProductItem.java # 套餐商品项 -│ ├── CouponInfo.java # 优惠券信息 -│ ├── CouponUseRequest.java # 优惠券使用请求 -│ ├── CouponUseResult.java # 优惠券使用结果 -│ ├── DiscountDetail.java # 折扣详情 -│ ├── PriceCalculationRequest.java # 价格计算请求 -│ ├── PriceCalculationResult.java # 价格计算结果 -│ ├── PriceDetails.java # 价格详情 -│ ├── ProductItem.java # 商品项 -│ ├── ProductPriceInfo.java # 商品价格信息 -│ ├── VoucherInfo.java # 券码信息 -│ ├── DiscountDetectionContext.java # 优惠检测上下文 -│ ├── DiscountInfo.java # 优惠信息 -│ ├── DiscountResult.java # 优惠结果 -│ └── DiscountCombinationResult.java # 优惠组合结果 -├── entity/ # 数据库实体类 -│ ├── BaseEntity.java # 基础实体类 -│ ├── PriceBundleConfig.java # 套餐配置 -│ ├── PriceCouponClaimRecord.java # 优惠券领取记录 -│ ├── PriceCouponConfig.java # 优惠券配置 -│ ├── PriceProductConfig.java # 商品价格配置 -│ ├── PriceTierConfig.java # 分层价格配置 -│ ├── PriceVoucherBatchConfig.java # 券码批次配置 -│ └── PriceVoucherCode.java # 券码实体 -├── enums/ # 枚举类 -│ ├── CouponStatus.java # 优惠券状态 -│ ├── CouponType.java # 优惠券类型 -│ ├── ProductType.java # 商品类型 -│ ├── VoucherDiscountType.java # 券码优惠类型 -│ └── VoucherCodeStatus.java # 券码状态 -├── exception/ # 异常处理 -│ ├── CouponInvalidException.java # 优惠券无效异常 -│ ├── PriceCalculationException.java # 价格计算异常 -│ ├── PricingExceptionHandler.java # 定价异常处理器 -│ ├── ProductConfigNotFoundException.java # 商品配置未找到异常 -│ ├── VoucherInvalidException.java # 券码无效异常 -│ ├── VoucherAlreadyUsedException.java # 券码已使用异常 -│ ├── VoucherNotClaimableException.java # 券码不可领取异常 -│ └── DiscountDetectionException.java # 优惠检测异常 -├── handler/ # 自定义处理器 -│ └── BundleProductListTypeHandler.java # 套餐商品列表类型处理器 -├── mapper/ # MyBatis数据访问层 -│ ├── PriceBundleConfigMapper.java -│ ├── PriceCouponClaimRecordMapper.java -│ ├── PriceCouponConfigMapper.java -│ ├── PriceProductConfigMapper.java -│ ├── PriceTierConfigMapper.java -│ ├── PriceVoucherBatchConfigMapper.java -│ └── PriceVoucherCodeMapper.java -└── service/ # 业务逻辑层 - ├── ICouponManagementService.java # 优惠券管理服务接口 - ├── ICouponService.java # 优惠券服务接口 - ├── IPriceBundleService.java # 套餐服务接口 - ├── IPriceCalculationService.java # 价格计算服务接口 - ├── IPricingManagementService.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 +├── controller/ # REST API 控制器层 +│ ├── PriceCalculationController.java # 价格计算 + 用户可用券查询 +│ ├── PricingConfigController.java # 产品/阶梯/套餐(一口价)配置管理(含 admin 查询) +│ ├── CouponManagementController.java # 优惠券配置/领取记录/统计(admin) +│ ├── OnePricePurchaseController.java # 一口价配置管理(admin) +│ ├── VoucherManagementController.java # 券码批次/券码/移动端领取与查询 +│ └── VoucherUsageController.java # 券码使用记录与统计查询 +├── dto/ # 数据传输对象 +│ ├── PriceCalculationRequest.java # 价格计算请求(含voucherCode/faceId/scenicId...) +│ ├── PriceCalculationResult.java # 价格计算结果(含usedCoupon/usedVoucher/availableDiscounts) +│ ├── ProductItem.java, ProductPriceInfo.java, PriceDetails.java +│ ├── DiscountDetectionContext.java, DiscountInfo.java, DiscountDetail.java, +│ │ DiscountResult.java, DiscountCombinationResult.java +│ ├── BundleProductItem.java, MobilePriceCalculationRequest.java +│ ├── OnePriceConfigRequest.java, OnePriceInfo.java +│ ├── req/ # 券码管理请求DTO +│ │ ├── VoucherBatchCreateReq(.java|V2) +│ │ ├── VoucherBatchQueryReq.java, VoucherCodeQueryReq.java, VoucherClaimReq.java +│ │ └── VoucherUsageHistoryReq.java +│ └── resp/ # 券码管理响应DTO +│ ├── VoucherBatchResp.java, VoucherBatchStatsResp.java +│ ├── VoucherCodeResp.java, VoucherValidationResp.java +│ ├── VoucherUsageRecordResp.java, VoucherUsageStatsResp.java, VoucherUsageSummaryResp.java +│ └── VoucherPrintResp.java +├── entity/ # 数据库实体类(MyBatis‑Plus) +│ ├── PriceProductConfig.java, PriceTierConfig.java, PriceBundleConfig.java +│ ├── PriceCouponConfig.java, PriceCouponClaimRecord.java +│ ├── PriceVoucherBatchConfig.java, PriceVoucherCode.java, PriceVoucherUsageRecord.java +│ ├── PriceOnePriceConfig.java # 一口价配置 +│ └── VoucherPrintRecord.java # 券码打印记录 +├── enums/ # 枚举类 +│ ├── ProductType.java, CouponType.java, CouponStatus.java +│ ├── VoucherDiscountType.java, VoucherCodeStatus.java +├── exception/ # 统一异常与业务异常 +│ ├── PricingExceptionHandler.java, PriceCalculationException.java +│ ├── ProductConfigNotFoundException.java, DiscountDetectionException.java +│ ├── CouponInvalidException.java, VoucherInvalidException.java, +│ ├── VoucherAlreadyUsedException.java, VoucherNotClaimableException.java +├── handler/ # MyBatis 类型处理器 +│ ├── BundleProductListTypeHandler.java # 套餐商品列表 JSON ↔ 对象列表 +│ └── ProductTypeListTypeHandler.java # 商品类型列表 JSON ↔ 枚举列表 +├── mapper/ # 数据访问接口(多为 MyBatis‑Plus Mapper) +│ ├── PriceProductConfigMapper.java, PriceTierConfigMapper.java, PriceBundleConfigMapper.java +│ ├── PriceCouponConfigMapper.java, PriceCouponClaimRecordMapper.java +│ ├── PriceVoucherBatchConfigMapper.java, PriceVoucherCodeMapper.java, PriceVoucherUsageRecordMapper.java +│ └── PriceOnePriceConfigMapper.java, VoucherPrintRecordMapper.java +└── service/ # 业务层接口与实现 + ├── IPriceCalculationService.java, IDiscountDetectionService.java, IDiscountProvider.java + ├── IProductConfigService.java, IPricingManagementService.java, IPriceBundleService.java + ├── ICouponService.java, ICouponManagementService.java + ├── IOnePricePurchaseService.java, IVoucherService.java, IVoucherUsageService.java + ├── VoucherBatchService.java, VoucherCodeService.java, VoucherPrintService.java + └── impl/ + ├── PriceCalculationServiceImpl.java, DiscountDetectionServiceImpl.java + ├── ProductConfigServiceImpl.java, PricingManagementServiceImpl.java, PriceBundleServiceImpl.java + ├── CouponServiceImpl.java, CouponManagementServiceImpl.java, CouponDiscountProvider.java + ├── VoucherServiceImpl.java, VoucherDiscountProvider.java, + ├── VoucherBatchServiceImpl.java, VoucherCodeServiceImpl.java, VoucherPrintServiceImpl.java, + ├── VoucherUsageServiceImpl.java, + └── OnePricePurchaseServiceImpl.java, OnePricePurchaseDiscountProvider.java ``` ## 核心功能 @@ -92,94 +77,91 @@ com.ycwl.basic.pricing/ ### 1. 价格计算引擎 #### API端点 -- `POST /api/pricing/calculate` - 执行价格计算 +- `POST /api/pricing/calculate` — 执行价格计算(预览模式默认开启) +- `GET /api/pricing/coupons/my-coupons` — 查询用户可用优惠券 #### 计算流程 ```java -// 价格计算核心流程 -1. 验证PriceCalculationRequest请求参数 -2. 加载商品基础配置 (PriceProductConfig) -3. 应用分层定价规则 (PriceTierConfig) -4. 处理套餐商品逻辑 (BundleProductItem) -5. 使用统一优惠检测系统处理券码和优惠券 -6. 按优先级应用优惠:券码 > 优惠券 -7. 计算最终价格并返回详细结果 +// 价格计算核心流程(IPriceCalculationService / PriceCalculationServiceImpl) +1. 校验 PriceCalculationRequest(包含 voucherCode / scenicId / faceId / autoUseXXX) +2. 加载商品基础配置 (PriceProductConfig),匹配分层规则 (PriceTierConfig)、套餐等 +3. 构造 DiscountDetectionContext,统一检测可用优惠(券码/优惠券/一口价) +4. 按优先级与可叠加规则应用优惠:券码 > 优惠券 > 一口价(受 canUseCoupon/canUseVoucher 控制) +5. 汇总 DiscountDetail,计算 finalAmount / discountAmount 并返回 PriceCalculationResult ``` #### 关键类 -- `PriceCalculationService`: 价格计算核心逻辑 +- `IPriceCalculationService` / `PriceCalculationServiceImpl`: 价格计算核心逻辑 - `PriceCalculationRequest`: 计算请求DTO - `PriceCalculationResult`: 计算结果DTO +- `IDiscountDetectionService`: 统一优惠检测与组合 ### 2. 优惠券管理系统 -#### API端点 -- `GET /api/pricing/admin/coupons/` - 分页查询优惠券配置 -- `POST /api/pricing/admin/coupons/` - 创建优惠券配置 -- `PUT /api/pricing/admin/coupons/{id}` - 更新优惠券配置 -- `DELETE /api/pricing/admin/coupons/{id}` - 删除优惠券配置 -- `GET /api/pricing/admin/coupons/{id}/claims` - 查询优惠券领取记录 -- `GET /api/pricing/admin/coupons/{id}/stats` - 获取优惠券统计信息 +#### API端点(管理端,见 `CouponManagementController`) +- `POST /api/pricing/admin/coupons/configs` — 创建配置 +- `PUT /api/pricing/admin/coupons/configs/{id}` — 更新配置 +- `DELETE /api/pricing/admin/coupons/configs/{id}` — 删除配置 +- `PUT /api/pricing/admin/coupons/configs/{id}/status` — 启用/禁用 +- `GET /api/pricing/admin/coupons/configs` — 全量配置(含禁用) +- `GET /api/pricing/admin/coupons/configs/page` — 分页查询 +- `GET /api/pricing/admin/coupons/claim-records[/*]` — 领取记录列表/分页/按条件查询 +- `GET /api/pricing/admin/coupons/stats/{couponId}[/*]` — 使用统计/明细/概览 -#### 优惠券类型 +#### 枚举定义(简述) ```java -public enum CouponType { - PERCENTAGE, // 百分比折扣 - FIXED_AMOUNT // 固定金额减免 -} - -public enum CouponStatus { - CLAIMED, // 已领取 - USED, // 已使用 - EXPIRED // 已过期 -} +// 实际枚举包含 code/description 字段,并提供 fromCode 工具方法 +public enum CouponType { PERCENTAGE("percentage", ...), FIXED_AMOUNT("fixed_amount", ...) } +public enum CouponStatus { CLAIMED("claimed", ...), USED("used", ...), EXPIRED("expired", ...) } ``` #### 关键特性 -- **商品类型限制**: 通过`applicableProducts` JSON字段控制适用商品 -- **消费限制**: 支持最小消费金额和最大折扣限制 -- **时效性**: 基于时间的有效期控制 -- **统计分析**: 完整的使用统计和分析功能 +- 商品类型限制:通过 JSON 字段(结合 `ProductTypeListTypeHandler`)控制适用商品 +- 消费限制:支持最小消费金额、最大折扣限制 +- 时效性:基于时间的有效期控制 +- 统计分析:完整的使用统计与分析能力 ### 3. 商品配置管理 -#### API端点 -- `GET /api/pricing/config/products` - 查询商品配置 -- `POST /api/pricing/config/products` - 创建商品配置 -- `PUT /api/pricing/config/products/{id}` - 更新商品配置 +#### API端点(摘) +- `GET /api/pricing/config/products` — 查询商品配置 +- `POST /api/pricing/config/products` — 创建商品配置 +- `PUT /api/pricing/config/products/{id}` — 更新商品配置 +- `GET /api/pricing/config/tiers[/*]`、`/bundles[/*]` — 阶梯/一口价配置查询 -#### 商品类型定义 +#### 商品类型定义(对齐实际) ```java +// 实际定义(带 code/description,并提供 fromCode): public enum ProductType { - VLOG_VIDEO("vlog_video", "Vlog视频"), - RECORDING_SET("recording_set", "录像集"), - PHOTO_SET("photo_set", "照相集"), - PHOTO_PRINT("photo_print", "照片打印"), - MACHINE_PRINT("machine_print", "一体机打印"); + VLOG_VIDEO("VLOG_VIDEO", "Vlog视频"), + RECORDING_SET("RECORDING_SET", "录像集"), + PHOTO_SET("PHOTO_SET", "照相集"), + PHOTO_PRINT("PHOTO_PRINT", "照片打印"), + MACHINE_PRINT("MACHINE_PRINT", "一体机打印"); } ``` #### 分层定价 -支持基于数量的分层定价策略,通过`PriceTierConfig`实体配置不同数量区间的单价。 +支持基于数量的分层定价策略,通过 `PriceTierConfig` 配置不同数量区间的单价。 ## 开发最佳实践 ### 1. 添加新商品类型 ```java -// 步骤1: 在ProductType枚举中添加新类型 +// 步骤1: 在 ProductType 枚举中添加新类型(含 code/description) public enum ProductType { // 现有类型... - NEW_PRODUCT("new_product", "新商品类型"); + NEW_PRODUCT("NEW_PRODUCT", "新商品类型"); } -// 步骤2: 在数据库中添加default配置 +// 步骤2: 在数据库中添加 default 配置 INSERT INTO price_product_config (product_type, base_price, ...) -VALUES ('new_product', 100.00, ...); +VALUES ('NEW_PRODUCT', 100.00, ...); // 步骤3: 添加分层定价配置(可选) 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: 更新前端产品类型映射 ``` @@ -187,14 +169,14 @@ VALUES ('new_product', 1, 10, 95.00, ...); ### 2. 扩展优惠券类型 ```java -// 步骤1: 在CouponType枚举中添加新类型 +// 步骤1: 在 CouponType 中添加新类型 public enum CouponType { PERCENTAGE, FIXED_AMOUNT, NEW_COUPON_TYPE // 新增类型 } -// 步骤2: 在CouponServiceImpl中实现计算逻辑 +// 步骤2: 在 CouponServiceImpl 中实现计算逻辑 @Override public BigDecimal calculateDiscount(CouponConfig coupon, BigDecimal originalPrice) { return switch (coupon.getCouponType()) { @@ -204,19 +186,17 @@ public BigDecimal calculateDiscount(CouponConfig coupon, BigDecimal originalPric }; } -// 步骤3: 更新applicableProducts验证规则 +// 步骤3: 更新 applicableProducts 验证规则(如有) ``` -### 3. 自定义TypeHandler使用 - -项目使用MyBatis自定义TypeHandler处理复杂JSON字段: +### 3. 自定义 TypeHandler 使用 ```java -// BundleProductListTypeHandler处理套餐商品列表 +// BundleProductListTypeHandler 处理套餐商品列表 @MappedTypes(List.class) @MappedJdbcTypes(JdbcType.VARCHAR) public class BundleProductListTypeHandler extends BaseTypeHandler> { - // JSON序列化/反序列化逻辑 + // JSON 序列化/反序列化逻辑 } // 在实体类中使用 @@ -224,6 +204,9 @@ public class SomeEntity { @TableField(typeHandler = BundleProductListTypeHandler.class) private List bundleProducts; } + +// ProductTypeListTypeHandler 处理商品类型列表(券码可用商品类型) +public class ProductTypeListTypeHandler extends BaseTypeHandler> { /* ... */ } ``` ### 4. 异常处理模式 @@ -231,12 +214,10 @@ public class SomeEntity { ```java // 自定义异常类 public class PriceCalculationException extends RuntimeException { - public PriceCalculationException(String message) { - super(message); - } + public PriceCalculationException(String message) { super(message); } } -// 在PricingExceptionHandler中统一处理 +// 在 PricingExceptionHandler 中统一处理 @ExceptionHandler(PriceCalculationException.class) public ApiResponse handlePriceCalculationException(PriceCalculationException e) { return ApiResponse.fail(ErrorCode.PRICE_CALCULATION_ERROR, e.getMessage()); @@ -245,107 +226,39 @@ public ApiResponse handlePriceCalculationException(PriceCalculationExcep ### 5. 分页查询实现 -```java -// 使用PageHelper实现分页 +``` +// 使用 PageHelper 实现分页(优惠券相关) @Override public PageInfo getCouponsByPage(int pageNum, int pageSize, String status, String name) { PageHelper.startPage(pageNum, pageSize); - - QueryWrapper queryWrapper = new QueryWrapper<>(); - if (StringUtils.hasText(status)) { - queryWrapper.eq("status", status); - } - if (StringUtils.hasText(name)) { - queryWrapper.like("name", name); - } - - List list = couponConfigMapper.selectList(queryWrapper); - return new PageInfo<>(list); + // ... } ``` -## 测试策略 - -### 1. 单元测试 -每个服务类都应有对应的测试类,重点测试: -- 价格计算逻辑的准确性 -- 优惠券适用性验证 -- 边界条件处理 -- 异常场景覆盖 - -### 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. **数据完整性** - - 使用事务确保数据一致性 - - 关键操作添加审计日志 +``` +// 使用 MyBatis‑Plus 的 Page(券码相关) +Page page = voucherBatchService.queryBatchList(req); +Page page = voucherCodeService.queryCodeList(req); +``` ## 券码管理系统 (Voucher System) ### 1. 核心特性 -券码系统是从原`voucher`包迁移而来,现已完全集成到pricing包中,与优惠券系统并行工作。 +券码系统由原独立模块迁移并完全集成到 pricing 包中,与优惠券系统并行工作。 -#### 券码优惠类型 +#### 券码优惠类型(简述) ```java public enum VoucherDiscountType { - FREE_ALL(0, "全场免费"), // 所有商品免费,优先级最高且不可叠加 + FREE_ALL(0, "全场免费"), // 优先级最高,且不可叠加 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. 数据库表结构 @@ -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. 架构设计 -采用策略模式设计的可扩展优惠检测系统,支持多种优惠类型的统一管理和自动优化组合。 +采用策略模式的可扩展优惠检测系统,统一管理并自动组合多种优惠类型。 #### 核心接口 ```java @@ -395,7 +331,7 @@ public interface IDiscountDetectionService { } ``` -### 2. 优惠提供者实现 +### 2. 优惠提供者实现(当前实现与优先级) #### VoucherDiscountProvider (优先级: 100) - 处理券码优惠逻辑 @@ -407,33 +343,29 @@ public interface IDiscountDetectionService { - 自动选择最优优惠券 - 可与券码叠加使用(除全场免费券码外) +#### OnePricePurchaseDiscountProvider (优先级: 60) +- 处理一口价优惠逻辑(景区级统一价格) +- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定 + ### 3. 优惠应用策略 #### 优先级规则 ``` -券码优惠 (Priority: 100) → 优惠券优惠 (Priority: 80) +券码 (100) → 优惠券 (80) → 一口价 (60) ``` #### 叠加逻辑 ```java -原价 → 应用券码优惠 → 应用优惠券优惠 → 最终价格 +原价 → 券码 → 优惠券 → 一口价 → 最终价格 特殊情况: -- 全场免费券码:最终价格直接为0,不再应用其他优惠 -- 其他券码类型:可与优惠券叠加使用 +- 全场免费券码:直接最终价=0,停止后续优惠 +- 一口价:可叠加性由配置 canUseCoupon / canUseVoucher 控制 ``` -#### 显示顺序 -``` -1. 券码优惠 (sortOrder: 1) -2. 限时立减 (sortOrder: 2) -3. 优惠券优惠 (sortOrder: 3) -4. 一口价优惠 (sortOrder: 4) -``` +#### 扩展支持 -### 4. 扩展支持 - -#### 添加新优惠类型 +##### 添加新优惠类型 ```java @Component public class FlashSaleDiscountProvider implements IDiscountProvider { @@ -441,23 +373,23 @@ public class FlashSaleDiscountProvider implements IDiscountProvider { public String getProviderType() { return "LIMITED_TIME"; } @Override - public int getPriority() { return 90; } // 介于券码和优惠券之间 + public int getPriority() { return 90; } // 示例:介于券码和优惠券之间 // 实现其他方法... } ``` -#### 动态注册 +##### 动态注册 ```java -// 系统启动时自动扫描所有IDiscountProvider实现类 -// 按优先级排序并注册到DiscountDetectionService中 +// 系统启动时自动扫描所有 IDiscountProvider 实现类 +// 按优先级排序并注册到 DiscountDetectionService 中 ``` -## API接口扩展 +## API 接口扩展 ### 1. 价格计算接口扩展 -#### 新增请求参数 +#### 新增请求参数(已存在) ```java public class PriceCalculationRequest { // 原有字段... @@ -469,7 +401,7 @@ public class PriceCalculationRequest { } ``` -#### 新增响应字段 +#### 新增响应字段(已存在) ```java public class PriceCalculationResult { // 原有字段... @@ -478,70 +410,53 @@ public class PriceCalculationResult { } ``` -### 2. 券码管理接口 +### 2. 一口价配置管理(OnePrice) -#### 移动端接口 -- `POST /api/pricing/mobile/voucher/claim` - 领取券码 -- `GET /api/pricing/mobile/voucher/my-codes` - 我的券码列表 +`OnePricePurchaseController`(管理端): +- `GET /api/pricing/admin/one-price/` — 分页查询 +- `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. 优惠检测开发 -```java -// 检测上下文构建 -DiscountDetectionContext context = new DiscountDetectionContext(); -context.setUserId(userId); -context.setFaceId(faceId); -context.setScenicId(scenicId); -context.setProducts(products); -context.setCurrentAmount(amount); -context.setVoucherCode(voucherCode); +### 2. 集成测试 +- 数据库读写与分页 +- JSON 序列化/反序列化(TypeHandler) +- API 端点的入参/出参校验 -// 使用统一服务检测优惠 -DiscountCombinationResult result = discountDetectionService - .calculateOptimalCombination(context); -``` +### 3. 配置校验 +- 校验各 ProductType 的默认配置完整性 +- 关键枚举与配置代码路径的兼容性 -### 2. 券码服务使用 -```java -// 验证券码 -VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo( - voucherCode, faceId, scenicId); +## 数据库设计 -// 计算券码优惠 -BigDecimal discount = voucherService.calculateVoucherDiscount( - voucherInfo, context); - -// 标记券码已使用 -voucherService.markVoucherAsUsed(voucherCode, "订单使用"); -``` - -### 3. 异常处理扩展 -```java -// 券码相关异常 -try { - // 券码操作 -} catch (VoucherInvalidException e) { - // 券码无效 -} catch (VoucherAlreadyUsedException e) { - // 券码已使用 -} catch (VoucherNotClaimableException e) { - // 券码不可领取 -} -``` - -## 数据库扩展 +### 核心表结构(摘) +- `price_product_config`: 商品价格基础配置 +- `price_tier_config`: 分层定价配置 +- `price_bundle_config`: 套餐配置 +- `price_coupon_config`: 优惠券配置 +- `price_coupon_claim_record`: 优惠券领取记录 ### 新增表结构 - `price_voucher_batch_config`: 券码批次配置表 - `price_voucher_code`: 券码表 +- `price_voucher_usage_record`: 券码使用记录表 +- `voucher_print_record`: 券码打印记录表 +- `price_one_price_config`: 一口价配置表 -### 索引优化 +### 索引优化(示例) ```sql -- 券码查询优化 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_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验证统计数据准确性 \ No newline at end of file +- 使用数据完整性检查 SQL 验证统计数据准确性 + +## 兼容性与注意事项 + +- 本模块使用 PageHelper(优惠券相关)与 MyBatis‑Plus(券码/一口价等)并存,请根据对应 Service/Mapper 选择分页与查询方式。 +- 优惠优先级及叠加规则以各 Provider 与业务配置为准,避免在外层重复实现优先级判断逻辑。 +- 若扩展新的优惠类型,务必实现 `IDiscountProvider` 并在 `IDiscountDetectionService` 中完成注册(当前实现通过组件扫描自动注册并排序)。 +