diff --git a/CLAUDE.md b/CLAUDE.md index 63c9ce5..6c92571 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md -本文件为 Claude Code (claude.ai/code) 在此代码仓库中工作时提供指导。 +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 构建和开发命令 @@ -120,4 +120,71 @@ mvn test -DskipTests=false 1. 在 `com.ycwl.basic.task` 包中创建类 2. 添加 `@Component` 和 `@Profile("prod")` 注解 3. 使用 `@Scheduled` 进行基于 cron 的执行 -4. 遵循现有的错误处理和日志记录模式 \ No newline at end of file +4. 遵循现有的错误处理和日志记录模式 + +## 价格查询系统 (Pricing Module) + +### 核心架构 +价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理和价格计算功能。 + +#### 关键组件 +- **PriceCalculationController** (`/api/pricing/calculate`):价格计算API +- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券管理API +- **PricingConfigController** (`/api/pricing/config/`):价格配置管理API + +#### 商品类型支持 +```java +ProductType枚举定义了支持的商品类型: +- VLOG_VIDEO: Vlog视频 +- RECORDING_SET: 录像集 +- PHOTO_SET: 照相集 +- PHOTO_PRINT: 照片打印 +- MACHINE_PRINT: 一体机打印 +``` + +#### 价格计算流程 +1. 接收PriceCalculationRequest(包含商品列表和用户ID) +2. 查找商品基础配置和分层定价 +3. 处理套餐商品(BundleProductItem) +4. 自动应用最优优惠券 +5. 返回PriceCalculationResult(包含原价、最终价格、优惠详情) + +#### 优惠券系统 +- **CouponType**: PERCENTAGE(百分比)、FIXED_AMOUNT(固定金额) +- **CouponStatus**: CLAIMED(已领取)、USED(已使用)、EXPIRED(已过期) +- 支持商品类型限制 (`applicableProducts` JSON字段) +- 最小消费金额和最大折扣限制 +- 时间有效期控制 + +#### 分页查询功能 +所有管理接口都支持分页查询,使用PageHelper实现: +- 优惠券配置分页:支持按状态、名称筛选 +- 领取记录分页:支持按用户、优惠券、状态、时间范围筛选 + +#### 统计功能 +- 基础统计:领取数、使用数、可用数 +- 详细统计:使用率、平均使用天数 +- 时间范围统计:指定时间段的整体数据分析 + +### 开发模式 + +#### 添加新商品类型 +1. 在ProductType枚举中添加新类型 +2. 在PriceProductConfig表中配置default配置 +3. 根据需要添加分层定价(PriceTierConfig) +4. 更新前端产品类型映射 + +#### 添加新优惠券类型 +1. 在CouponType枚举中添加类型 +2. 在CouponServiceImpl中实现计算逻辑 +3. 更新applicableProducts验证规则 + +#### 自定义TypeHandler使用 +项目使用自定义TypeHandler处理复杂JSON字段: +- `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化 + +### 测试策略 +- 单元测试:每个服务类都有对应测试类 +- 配置验证测试:DefaultConfigValidationTest验证default配置 +- JSON序列化测试:验证复杂对象的数据库存储 +- 分页功能测试:验证PageHelper集成 \ No newline at end of file diff --git a/pom.xml b/pom.xml index c2aac13..d839403 100644 --- a/pom.xml +++ b/pom.xml @@ -181,6 +181,10 @@ com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java b/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java index ba2892a..70711a4 100644 --- a/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java +++ b/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java @@ -1,10 +1,16 @@ package com.ycwl.basic.config; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + @Configuration public class JacksonConfiguration { @@ -13,6 +19,16 @@ public class JacksonConfiguration { return builder -> { // 把 Long 类型序列化为 String builder.serializerByType(Long.class, ToStringSerializer.instance); + + // 添加 JavaTimeModule 以支持 Java 8 时间类型 + builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss"); + builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }; } -} + + @Bean + public JavaTimeModule javaTimeModule() { + return new JavaTimeModule(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java new file mode 100644 index 0000000..7fcd369 --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.controller.mobile; + +import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; +import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; +import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; +import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; +import com.ycwl.basic.pricing.service.VoucherCodeService; +import com.ycwl.basic.pricing.service.VoucherPrintService; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.utils.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@Slf4j +@RestController +@RequestMapping("/api/mobile/voucher/v1") +public class AppVoucherController { + + @Autowired + private VoucherPrintService voucherPrintService; + @Autowired + private VoucherCodeService voucherCodeService; + @Autowired + private FaceRepository faceRepository; + + /** + * 打印小票 + * @param request 打印请求 + * @return 打印结果 + */ + @PostMapping("/print") + public ApiResponse printVoucherTicket(@RequestBody VoucherPrintReq request) { + log.info("收到打印小票请求: faceId={}, brokerId={}, scenicId={}", + request.getFaceId(), request.getBrokerId(), request.getScenicId()); + + VoucherPrintResp response = voucherPrintService.printVoucherTicket(request); + + log.info("打印小票完成: code={}, voucherCode={}, status={}", + response.getCode(), response.getVoucherCode(), response.getPrintStatus()); + + return ApiResponse.success(response); + } + + @GetMapping("/printed") + public ApiResponse queryPrintedVoucher( + @RequestParam Long faceId + ) { + return ApiResponse.success(voucherPrintService.queryPrintedVoucher(faceId)); + } + + @PostMapping("/claim") + public ApiResponse claimVoucher(@RequestBody VoucherClaimReq req) { + FaceEntity face = faceRepository.getFace(req.getFaceId()); + if (face == null) { + throw new BaseException("请选择人脸"); + } + if (!face.getMemberId().equals(Long.valueOf(BaseContextHandler.getUserId()))) { + throw new BaseException("自动领取失败"); + } + req.setScenicId(face.getScenicId()); + VoucherCodeResp result = voucherCodeService.claimVoucher(req); + return ApiResponse.success(result); + } + +} diff --git a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md new file mode 100644 index 0000000..c1c0ebf --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md @@ -0,0 +1,557 @@ +# 价格查询系统 (Pricing Module) 开发指南 + +此文档为pricing包的专用开发指南,提供该模块的详细架构说明和开发最佳实践。 + +## 模块概览 + +价格查询系统 (`com.ycwl.basic.pricing`) 是一个独立的业务模块,提供商品定价、优惠券管理、券码管理和价格计算功能。采用分层架构设计,具备完整的CRUD操作、异常处理和数据统计功能。支持优惠券和券码的同时使用,券码优先级更高。 + +## 目录结构 + +``` +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 +``` + +## 核心功能 + +### 1. 价格计算引擎 + +#### API端点 +- `POST /api/pricing/calculate` - 执行价格计算 + +#### 计算流程 +```java +// 价格计算核心流程 +1. 验证PriceCalculationRequest请求参数 +2. 加载商品基础配置 (PriceProductConfig) +3. 应用分层定价规则 (PriceTierConfig) +4. 处理套餐商品逻辑 (BundleProductItem) +5. 使用统一优惠检测系统处理券码和优惠券 +6. 按优先级应用优惠:券码 > 优惠券 +7. 计算最终价格并返回详细结果 +``` + +#### 关键类 +- `PriceCalculationService`: 价格计算核心逻辑 +- `PriceCalculationRequest`: 计算请求DTO +- `PriceCalculationResult`: 计算结果DTO + +### 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` - 获取优惠券统计信息 + +#### 优惠券类型 +```java +public enum CouponType { + PERCENTAGE, // 百分比折扣 + FIXED_AMOUNT // 固定金额减免 +} + +public enum CouponStatus { + CLAIMED, // 已领取 + USED, // 已使用 + EXPIRED // 已过期 +} +``` + +#### 关键特性 +- **商品类型限制**: 通过`applicableProducts` JSON字段控制适用商品 +- **消费限制**: 支持最小消费金额和最大折扣限制 +- **时效性**: 基于时间的有效期控制 +- **统计分析**: 完整的使用统计和分析功能 + +### 3. 商品配置管理 + +#### API端点 +- `GET /api/pricing/config/products` - 查询商品配置 +- `POST /api/pricing/config/products` - 创建商品配置 +- `PUT /api/pricing/config/products/{id}` - 更新商品配置 + +#### 商品类型定义 +```java +public enum ProductType { + VLOG_VIDEO("vlog_video", "Vlog视频"), + RECORDING_SET("recording_set", "录像集"), + PHOTO_SET("photo_set", "照相集"), + PHOTO_PRINT("photo_print", "照片打印"), + MACHINE_PRINT("machine_print", "一体机打印"); +} +``` + +#### 分层定价 +支持基于数量的分层定价策略,通过`PriceTierConfig`实体配置不同数量区间的单价。 + +## 开发最佳实践 + +### 1. 添加新商品类型 + +```java +// 步骤1: 在ProductType枚举中添加新类型 +public enum ProductType { + // 现有类型... + NEW_PRODUCT("new_product", "新商品类型"); +} + +// 步骤2: 在数据库中添加default配置 +INSERT INTO price_product_config (product_type, base_price, ...) +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, ...); + +// 步骤4: 更新前端产品类型映射 +``` + +### 2. 扩展优惠券类型 + +```java +// 步骤1: 在CouponType枚举中添加新类型 +public enum CouponType { + PERCENTAGE, + FIXED_AMOUNT, + NEW_COUPON_TYPE // 新增类型 +} + +// 步骤2: 在CouponServiceImpl中实现计算逻辑 +@Override +public BigDecimal calculateDiscount(CouponConfig coupon, BigDecimal originalPrice) { + return switch (coupon.getCouponType()) { + case PERCENTAGE -> calculatePercentageDiscount(coupon, originalPrice); + case FIXED_AMOUNT -> calculateFixedAmountDiscount(coupon, originalPrice); + case NEW_COUPON_TYPE -> calculateNewTypeDiscount(coupon, originalPrice); + }; +} + +// 步骤3: 更新applicableProducts验证规则 +``` + +### 3. 自定义TypeHandler使用 + +项目使用MyBatis自定义TypeHandler处理复杂JSON字段: + +```java +// BundleProductListTypeHandler处理套餐商品列表 +@MappedTypes(List.class) +@MappedJdbcTypes(JdbcType.VARCHAR) +public class BundleProductListTypeHandler extends BaseTypeHandler> { + // JSON序列化/反序列化逻辑 +} + +// 在实体类中使用 +public class SomeEntity { + @TableField(typeHandler = BundleProductListTypeHandler.class) + private List bundleProducts; +} +``` + +### 4. 异常处理模式 + +```java +// 自定义异常类 +public class PriceCalculationException extends RuntimeException { + public PriceCalculationException(String message) { + super(message); + } +} + +// 在PricingExceptionHandler中统一处理 +@ExceptionHandler(PriceCalculationException.class) +public ApiResponse handlePriceCalculationException(PriceCalculationException e) { + return ApiResponse.error(ErrorCode.PRICE_CALCULATION_ERROR, e.getMessage()); +} +``` + +### 5. 分页查询实现 + +```java +// 使用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. **数据完整性** + - 使用事务确保数据一致性 + - 关键操作添加审计日志 + +## 券码管理系统 (Voucher System) + +### 1. 核心特性 + +券码系统是从原`voucher`包迁移而来,现已完全集成到pricing包中,与优惠券系统并行工作。 + +#### 券码优惠类型 +```java +public enum VoucherDiscountType { + FREE_ALL(0, "全场免费"), // 所有商品免费,优先级最高且不可叠加 + REDUCE_PRICE(1, "商品降价"), // 每个商品减免固定金额 + DISCOUNT(2, "商品打折") // 每个商品按百分比打折 +} +``` + +#### 券码状态流转 +``` +UNCLAIMED(0) → CLAIMED_UNUSED(1) → USED(2) + 未领取 → 已领取未使用 → 已使用 +``` + +### 2. 数据库表结构 + +#### 券码批次配置表 (price_voucher_batch_config) +- 批次管理:支持按景区、推客创建券码批次 +- 统计功能:实时统计已领取数、已使用数 +- 状态控制:支持启用/禁用批次 + +#### 券码表 (price_voucher_code) +- 唯一性约束:每个券码全局唯一 +- 用户限制:同一用户在同一景区只能领取一次券码 +- 时间追踪:记录领取时间、使用时间 + +### 3. 关键业务规则 + +#### 领取限制 +- 同一`faceId`在同一`scenicId`中只能领取一次券码 +- 只有启用状态的批次才能领取券码 +- 批次必须有可用券码才能成功领取 + +#### 使用验证 +- 券码必须是`CLAIMED_UNUSED`状态才能使用 +- 必须验证券码与景区的匹配关系 +- 使用后自动更新批次统计数据 + +## 统一优惠检测系统 (Unified Discount Detection) + +### 1. 架构设计 + +采用策略模式设计的可扩展优惠检测系统,支持多种优惠类型的统一管理和自动优化组合。 + +#### 核心接口 +```java +// 优惠提供者接口 +public interface IDiscountProvider { + String getProviderType(); // 提供者类型 + int getPriority(); // 优先级(数字越大越高) + List detectAvailableDiscounts(); // 检测可用优惠 + DiscountResult applyDiscount(); // 应用优惠 +} + +// 优惠检测服务接口 +public interface IDiscountDetectionService { + DiscountCombinationResult calculateOptimalCombination(); // 计算最优组合 + DiscountCombinationResult previewOptimalCombination(); // 预览优惠组合 +} +``` + +### 2. 优惠提供者实现 + +#### VoucherDiscountProvider (优先级: 100) +- 处理券码优惠逻辑 +- 支持用户主动输入券码或自动选择最优券码 +- 全场免费券码不可与其他优惠叠加 + +#### CouponDiscountProvider (优先级: 80) +- 处理优惠券优惠逻辑 +- 自动选择最优优惠券 +- 可与券码叠加使用(除全场免费券码外) + +### 3. 优惠应用策略 + +#### 优先级规则 +``` +券码优惠 (Priority: 100) → 优惠券优惠 (Priority: 80) +``` + +#### 叠加逻辑 +```java +原价 → 应用券码优惠 → 应用优惠券优惠 → 最终价格 + +特殊情况: +- 全场免费券码:最终价格直接为0,不再应用其他优惠 +- 其他券码类型:可与优惠券叠加使用 +``` + +#### 显示顺序 +``` +1. 券码优惠 (sortOrder: 1) +2. 限时立减 (sortOrder: 2) +3. 优惠券优惠 (sortOrder: 3) +4. 一口价优惠 (sortOrder: 4) +``` + +### 4. 扩展支持 + +#### 添加新优惠类型 +```java +@Component +public class FlashSaleDiscountProvider implements IDiscountProvider { + @Override + public String getProviderType() { return "FLASH_SALE"; } + + @Override + public int getPriority() { return 90; } // 介于券码和优惠券之间 + + // 实现其他方法... +} +``` + +#### 动态注册 +```java +// 系统启动时自动扫描所有IDiscountProvider实现类 +// 按优先级排序并注册到DiscountDetectionService中 +``` + +## API接口扩展 + +### 1. 价格计算接口扩展 + +#### 新增请求参数 +```java +public class PriceCalculationRequest { + // 原有字段... + private String voucherCode; // 用户输入的券码 + private Long scenicId; // 景区ID + private Long faceId; // 用户faceId + private Boolean autoUseVoucher; // 是否自动使用券码 + private Boolean previewOnly; // 是否仅预览优惠 +} +``` + +#### 新增响应字段 +```java +public class PriceCalculationResult { + // 原有字段... + private VoucherInfo usedVoucher; // 使用的券码信息 + private List availableDiscounts; // 可用优惠列表(预览模式) +} +``` + +### 2. 券码管理接口 + +#### 移动端接口 +- `POST /api/pricing/mobile/voucher/claim` - 领取券码 +- `GET /api/pricing/mobile/voucher/my-codes` - 我的券码列表 + +#### 管理端接口 +- `POST /api/pricing/admin/voucher/batch/create` - 创建券码批次 +- `GET /api/pricing/admin/voucher/batch/list` - 批次列表查询 +- `GET /api/pricing/admin/voucher/codes` - 券码列表查询 + +## 开发最佳实践更新 + +### 1. 优惠检测开发 +```java +// 检测上下文构建 +DiscountDetectionContext context = new DiscountDetectionContext(); +context.setUserId(userId); +context.setFaceId(faceId); +context.setScenicId(scenicId); +context.setProducts(products); +context.setCurrentAmount(amount); +context.setVoucherCode(voucherCode); + +// 使用统一服务检测优惠 +DiscountCombinationResult result = discountDetectionService + .calculateOptimalCombination(context); +``` + +### 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_voucher_batch_config`: 券码批次配置表 +- `price_voucher_code`: 券码表 + +### 索引优化 +```sql +-- 券码查询优化 +CREATE INDEX idx_voucher_code ON price_voucher_code(code); +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); +``` + +### 性能考虑 +- 券码表可能数据量较大,考虑按景区分表 +- 定期清理已删除的过期数据 +- 使用数据完整性检查SQL验证统计数据准确性 \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java b/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java new file mode 100644 index 0000000..c061981 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java @@ -0,0 +1,233 @@ +package com.ycwl.basic.pricing.controller; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; +import com.ycwl.basic.pricing.service.ICouponManagementService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 优惠券管理控制器(管理端) + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing/admin/coupons") +@RequiredArgsConstructor +public class CouponManagementController { + + private final ICouponManagementService couponManagementService; + + // ==================== 优惠券配置管理 ==================== + + /** + * 创建优惠券配置 + */ + @PostMapping("/configs") + public ApiResponse createCouponConfig(@RequestBody PriceCouponConfig config) { + log.info("创建优惠券配置: {}", config.getCouponName()); + Long id = couponManagementService.createCouponConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新优惠券配置 + */ + @PutMapping("/configs/{id}") + public ApiResponse updateCouponConfig(@PathVariable Long id, @RequestBody PriceCouponConfig config) { + log.info("更新优惠券配置: id={}, name={}", id, config.getCouponName()); + config.setId(id); + boolean success = couponManagementService.updateCouponConfig(config); + return ApiResponse.success(success); + } + + /** + * 删除优惠券配置 + */ + @DeleteMapping("/configs/{id}") + public ApiResponse deleteCouponConfig(@PathVariable Long id) { + log.info("删除优惠券配置: id={}", id); + boolean success = couponManagementService.deleteCouponConfig(id); + return ApiResponse.success(success); + } + + /** + * 启用/禁用优惠券配置 + */ + @PutMapping("/configs/{id}/status") + public ApiResponse updateCouponConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("修改优惠券配置状态: id={}, isActive={}", id, isActive); + boolean success = couponManagementService.updateCouponConfigStatus(id, isActive); + return ApiResponse.success(success); + } + + /** + * 查询所有优惠券配置(包含禁用的) + */ + @GetMapping("/configs") + public ApiResponse> getAllCouponConfigs() { + log.info("管理端获取所有优惠券配置"); + List configs = couponManagementService.getAllCouponConfigs(); + return ApiResponse.success(configs); + } + + /** + * 分页查询优惠券配置 + */ + @GetMapping("/configs/page") + public ApiResponse> getCouponConfigsPage( + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Boolean isActive, + @RequestParam(required = false) String couponName, + @RequestParam(required = false) String scenicId) { + log.info("分页查询优惠券配置: pageNum={}, pageSize={}, isActive={}, couponName={}, scenicId={}", + pageNum, pageSize, isActive, couponName, scenicId); + PageInfo pageInfo = couponManagementService.getCouponConfigsPage( + pageNum, pageSize, isActive, couponName, scenicId); + return ApiResponse.success(pageInfo); + } + + /** + * 根据状态查询优惠券配置 + */ + @GetMapping("/configs/status/{isActive}") + public ApiResponse> getCouponConfigsByStatus(@PathVariable Boolean isActive) { + log.info("根据状态查询优惠券配置: {}", isActive); + List configs = couponManagementService.getCouponConfigsByStatus(isActive); + return ApiResponse.success(configs); + } + + /** + * 根据ID查询优惠券配置 + */ + @GetMapping("/configs/{id}") + public ApiResponse getCouponConfigById(@PathVariable Long id) { + log.info("根据ID查询优惠券配置: {}", id); + PriceCouponConfig config = couponManagementService.getCouponConfigById(id); + return ApiResponse.success(config); + } + + // ==================== 优惠券领取记录查询 ==================== + + /** + * 查询所有优惠券领取记录 + */ + @GetMapping("/claim-records") + public ApiResponse> getAllClaimRecords() { + log.info("查询所有优惠券领取记录"); + List records = couponManagementService.getAllClaimRecords(); + return ApiResponse.success(records); + } + + /** + * 分页查询优惠券领取记录 + */ + @GetMapping("/claim-records/page") + public ApiResponse> getClaimRecordsPage( + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Long userId, + @RequestParam(required = false) Long couponId, + @RequestParam(required = false) CouponStatus status, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime, + @RequestParam(required = false) String scenicId) { + log.info("分页查询优惠券领取记录: pageNum={}, pageSize={}, userId={}, couponId={}, status={}, startTime={}, endTime={}, scenicId={}", + pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId); + PageInfo pageInfo = couponManagementService.getClaimRecordsPage( + pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId); + return ApiResponse.success(pageInfo); + } + + /** + * 根据用户ID查询优惠券领取记录 + */ + @GetMapping("/claim-records/user/{userId}") + public ApiResponse> getClaimRecordsByUserId(@PathVariable Long userId) { + log.info("根据用户ID查询优惠券领取记录: {}", userId); + List records = couponManagementService.getClaimRecordsByUserId(userId); + return ApiResponse.success(records); + } + + /** + * 根据优惠券ID查询领取记录 + */ + @GetMapping("/claim-records/coupon/{couponId}") + public ApiResponse> getClaimRecordsByCouponId(@PathVariable Long couponId) { + log.info("根据优惠券ID查询领取记录: {}", couponId); + List records = couponManagementService.getClaimRecordsByCouponId(couponId); + return ApiResponse.success(records); + } + + /** + * 根据状态查询领取记录 + */ + @GetMapping("/claim-records/status/{status}") + public ApiResponse> getClaimRecordsByStatus(@PathVariable CouponStatus status) { + log.info("根据状态查询领取记录: {}", status); + List records = couponManagementService.getClaimRecordsByStatus(status); + return ApiResponse.success(records); + } + + // ==================== 统计功能 ==================== + + /** + * 查询优惠券使用统计 + */ + @GetMapping("/stats/{couponId}") + public ApiResponse> getCouponUsageStats(@PathVariable Long couponId) { + log.info("查询优惠券使用统计: {}", couponId); + Map stats = couponManagementService.getCouponUsageStats(couponId); + return ApiResponse.success(stats); + } + + /** + * 查询优惠券详细统计 + */ + @GetMapping("/stats/{couponId}/detail") + public ApiResponse> getCouponDetailStats(@PathVariable Long couponId) { + log.info("查询优惠券详细统计: {}", couponId); + Map stats = couponManagementService.getCouponDetailStats(couponId); + return ApiResponse.success(stats); + } + + /** + * 查询时间范围内的统计数据 + */ + @GetMapping("/stats/period") + public ApiResponse> getPeriodStats( + @RequestParam String startDate, + @RequestParam String endDate, + @RequestParam(required = false) String scenicId) { + log.info("查询时间范围统计: startDate={}, endDate={}, scenicId={}", startDate, endDate, scenicId); + Map stats = couponManagementService.getPeriodStats(startDate, endDate, scenicId); + return ApiResponse.success(stats); + } + + /** + * 查询景区优惠券统计 + */ + @GetMapping("/stats/scenic/{scenicId}") + public ApiResponse> getScenicCouponStats(@PathVariable String scenicId) { + log.info("查询景区优惠券统计: scenicId={}", scenicId); + Map stats = couponManagementService.getScenicCouponStats(scenicId); + return ApiResponse.success(stats); + } + + /** + * 查询所有优惠券的使用统计概览 + */ + @GetMapping("/stats/overview") + public ApiResponse>> getAllCouponUsageOverview() { + log.info("查询所有优惠券使用统计概览"); + List> overview = couponManagementService.getAllCouponUsageOverview(); + return ApiResponse.success(overview); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java b/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java new file mode 100644 index 0000000..8b4cd2e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.pricing.controller; + +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.service.ICouponService; +import com.ycwl.basic.pricing.service.IPriceCalculationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 价格计算控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing") +@RequiredArgsConstructor +public class PriceCalculationController { + + private final IPriceCalculationService priceCalculationService; + private final ICouponService couponService; + + /** + * 计算商品价格 + */ + @PostMapping("/calculate") + public ApiResponse calculatePrice(@RequestBody PriceCalculationRequest request) { + log.info("价格计算请求: userId={}, products={}", request.getUserId(), request.getProducts().size()); + + PriceCalculationResult result = priceCalculationService.calculatePrice(request); + + log.info("价格计算完成: originalAmount={}, finalAmount={}, usedCoupon={}", + result.getOriginalAmount(), result.getFinalAmount(), + result.getUsedCoupon() != null ? result.getUsedCoupon().getCouponId() : null); + + return ApiResponse.success(result); + } + + /** + * 使用优惠券 + * 只能通过代码处理,不能通过接口调用 + */ + @PostMapping("/coupons/use") + @Deprecated + public ApiResponse useCoupon(@RequestBody CouponUseRequest request) { +// log.info("优惠券使用请求: userId={}, couponId={}, orderId={}", +// request.getUserId(), request.getCouponId(), request.getOrderId()); +// +// CouponUseResult result = couponService.useCoupon(request); +// +// log.info("优惠券使用成功: couponId={}, discountAmount={}", +// result.getCouponId(), result.getDiscountAmount()); +// +// ApiResponse response = ApiResponse.success(result); +// response.setMsg("优惠券使用成功"); +// return response; + return null; + } + + /** + * 查询用户可用优惠券 + */ + @GetMapping("/coupons/my-coupons") + public ApiResponse> getUserCoupons(@RequestParam Long userId) { + log.info("查询用户可用优惠券: userId={}", userId); + + List coupons = couponService.getUserAvailableCoupons(userId); + + log.info("用户可用优惠券数量: {}", coupons.size()); + + return ApiResponse.success(coupons); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java new file mode 100644 index 0000000..63002a1 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java @@ -0,0 +1,300 @@ +package com.ycwl.basic.pricing.controller; + +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.service.IProductConfigService; +import com.ycwl.basic.pricing.service.IPriceBundleService; +import com.ycwl.basic.pricing.service.IPricingManagementService; +import com.ycwl.basic.pricing.service.ICouponManagementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 价格配置管理控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing/config") +@RequiredArgsConstructor +public class PricingConfigController { + + private final IProductConfigService productConfigService; + private final IPriceBundleService bundleService; + private final IPricingManagementService managementService; + private final ICouponManagementService couponManagementService; + + + // ==================== 查询API ==================== + + /** + * 获取所有商品配置 + */ + @GetMapping("/products") + public ApiResponse> getAllProductConfigs() { + log.info("获取所有商品配置"); + List configs = productConfigService.getAllProductConfigs(); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型获取阶梯配置 + */ + @GetMapping("/tiers/{productType}") + public ApiResponse> getTierConfigs(@PathVariable String productType) { + log.info("根据商品类型获取阶梯配置: {}", productType); + List configs = productConfigService.getTierConfigs(productType); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型和商品ID获取阶梯配置 + */ + @GetMapping("/tiers/{productType}/{productId}") + public ApiResponse> getTierConfigs(@PathVariable String productType, + @PathVariable String productId) { + log.info("根据商品类型和ID获取阶梯配置: {}, {}", productType, productId); + List configs = productConfigService.getTierConfigs(productType, productId); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型和商品ID获取具体配置 + */ + @GetMapping("/products/{productType}/{productId}") + public ApiResponse getProductConfig(@PathVariable String productType, + @PathVariable String productId) { + log.info("根据商品类型和ID获取商品配置: {}, {}", productType, productId); + PriceProductConfig config = productConfigService.getProductConfig(productType, productId); + return ApiResponse.success(config); + } + + /** + * 获取所有阶梯配置 + */ + @GetMapping("/tiers") + public ApiResponse> getAllTierConfigs() { + log.info("获取所有阶梯配置"); + List configs = productConfigService.getAllTierConfigs(); + return ApiResponse.success(configs); + } + + /** + * 获取所有一口价配置 + */ + @GetMapping("/bundles") + public ApiResponse> getAllBundleConfigs() { + log.info("获取所有一口价配置"); + List configs = bundleService.getAllBundles(); + return ApiResponse.success(configs); + } + + + // ==================== 配置管理API(手动处理时间) ==================== + + /** + * 创建商品配置 + */ + @PostMapping("/products") + public ApiResponse createProductConfig(@RequestBody PriceProductConfig config) { + log.info("创建商品配置: {}", config.getProductName()); + Long id = managementService.createProductConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新商品配置 + */ + @PutMapping("/products/{id}") + public ApiResponse updateProductConfig(@PathVariable Long id, @RequestBody PriceProductConfig config) { + log.info("更新商品配置: id={}, name={}", id, config.getProductName()); + config.setId(id); + boolean success = managementService.updateProductConfig(config); + return ApiResponse.success(success); + } + + /** + * 创建阶梯定价配置 + */ + @PostMapping("/tiers") + public ApiResponse createTierConfig(@RequestBody PriceTierConfig config) { + log.info("创建阶梯定价配置: productType={}, price={}", config.getProductType(), config.getPrice()); + Long id = managementService.createTierConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新阶梯定价配置 + */ + @PutMapping("/tiers/{id}") + public ApiResponse updateTierConfig(@PathVariable Long id, @RequestBody PriceTierConfig config) { + log.info("更新阶梯定价配置: id={}", id); + config.setId(id); + boolean success = managementService.updateTierConfig(config); + return ApiResponse.success(success); + } + + /** + * 创建一口价配置 + */ + @PostMapping("/bundles") + public ApiResponse createBundleConfig(@RequestBody PriceBundleConfig config) { + log.info("创建一口价配置: {}", config.getBundleName()); + Long id = managementService.createBundleConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新一口价配置 + */ + @PutMapping("/bundles/{id}") + public ApiResponse updateBundleConfig(@PathVariable Long id, @RequestBody PriceBundleConfig config) { + log.info("更新一口价配置: id={}, name={}", id, config.getBundleName()); + config.setId(id); + boolean success = managementService.updateBundleConfig(config); + return ApiResponse.success(success); + } + + // ==================== 启用/禁用API ==================== + + /** + * 启用/禁用商品配置 + */ + @PutMapping("/products/{id}/status") + public ApiResponse updateProductConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("修改商品配置状态: id={}, isActive={}", id, isActive); + boolean success = managementService.updateProductConfigStatus(id, isActive); + return ApiResponse.success(success); + } + + /** + * 启用/禁用阶梯配置 + */ + @PutMapping("/tiers/{id}/status") + public ApiResponse updateTierConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("修改阶梯配置状态: id={}, isActive={}", id, isActive); + boolean success = managementService.updateTierConfigStatus(id, isActive); + return ApiResponse.success(success); + } + + /** + * 启用/禁用一口价配置 + */ + @PutMapping("/bundles/{id}/status") + public ApiResponse updateBundleConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("修改一口价配置状态: id={}, isActive={}", id, isActive); + boolean success = managementService.updateBundleConfigStatus(id, isActive); + return ApiResponse.success(success); + } + + // ==================== 删除API ==================== + + /** + * 删除商品配置 + */ + @DeleteMapping("/products/{id}") + public ApiResponse deleteProductConfig(@PathVariable Long id) { + log.info("删除商品配置: id={}", id); + boolean success = managementService.deleteProductConfig(id); + return ApiResponse.success(success); + } + + /** + * 删除阶梯配置 + */ + @DeleteMapping("/tiers/{id}") + public ApiResponse deleteTierConfig(@PathVariable Long id) { + log.info("删除阶梯配置: id={}", id); + boolean success = managementService.deleteTierConfig(id); + return ApiResponse.success(success); + } + + /** + * 删除一口价配置 + */ + @DeleteMapping("/bundles/{id}") + public ApiResponse deleteBundleConfig(@PathVariable Long id) { + log.info("删除一口价配置: id={}", id); + boolean success = managementService.deleteBundleConfig(id); + return ApiResponse.success(success); + } + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 管理端:获取所有商品配置(包含禁用的) + */ + @GetMapping("/admin/products") + public ApiResponse> getAllProductConfigsForAdmin() { + log.info("管理端获取所有商品配置"); + List configs = productConfigService.getAllProductConfigsForAdmin(); + return ApiResponse.success(configs); + } + + /** + * 管理端:获取所有阶梯配置(包含禁用的) + */ + @GetMapping("/admin/tiers") + public ApiResponse> getAllTierConfigsForAdmin() { + log.info("管理端获取所有阶梯配置"); + List configs = productConfigService.getAllTierConfigsForAdmin(); + return ApiResponse.success(configs); + } + + /** + * 管理端:根据商品类型获取阶梯配置(包含禁用的) + */ + @GetMapping("/admin/tiers/{productType}") + public ApiResponse> getTierConfigsForAdmin(@PathVariable String productType) { + log.info("管理端根据商品类型获取阶梯配置: {}", productType); + List configs = productConfigService.getTierConfigsForAdmin(productType); + return ApiResponse.success(configs); + } + + /** + * 管理端:根据商品类型和商品ID获取阶梯配置(包含禁用的) + */ + @GetMapping("/admin/tiers/{productType}/{productId}") + public ApiResponse> getTierConfigsForAdmin(@PathVariable String productType, + @PathVariable String productId) { + log.info("管理端根据商品类型和ID获取阶梯配置: {}, {}", productType, productId); + List configs = productConfigService.getTierConfigsForAdmin(productType, productId); + return ApiResponse.success(configs); + } + + /** + * 管理端:获取所有一口价配置(包含禁用的) + */ + @GetMapping("/admin/bundles") + public ApiResponse> getAllBundleConfigsForAdmin() { + log.info("管理端获取所有一口价配置"); + List configs = bundleService.getAllBundlesForAdmin(); + return ApiResponse.success(configs); + } + + /** + * 管理端:获取所有优惠券配置(包含禁用的) + */ + @GetMapping("/admin/coupons") + public ApiResponse> getAllCouponConfigsForAdmin() { + log.info("管理端获取所有优惠券配置"); + List configs = couponManagementService.getAllCouponConfigs(); + return ApiResponse.success(configs); + } + + /** + * 管理端:获取所有优惠券领取记录 + */ + @GetMapping("/admin/coupon-records") + public ApiResponse> getAllCouponClaimRecordsForAdmin() { + log.info("管理端获取所有优惠券领取记录"); + List records = couponManagementService.getAllClaimRecords(); + return ApiResponse.success(records); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java b/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java new file mode 100644 index 0000000..573870e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java @@ -0,0 +1,96 @@ +package com.ycwl.basic.pricing.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; +import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; +import com.ycwl.basic.pricing.service.VoucherBatchService; +import com.ycwl.basic.pricing.service.VoucherCodeService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/pricing/voucher") +public class VoucherManagementController { + @Autowired + @Lazy + private VoucherBatchService voucherBatchService; + @Autowired + @Lazy + private VoucherCodeService voucherCodeService; + + @PostMapping("/batch/create") + public ApiResponse createBatch(@RequestBody VoucherBatchCreateReq req) { + Long batchId = voucherBatchService.createBatch(req); + return ApiResponse.success(batchId); + } + + @PostMapping("/batch/list") + public ApiResponse> getBatchList(@RequestBody VoucherBatchQueryReq req) { + Page page = voucherBatchService.queryBatchList(req); + return ApiResponse.success(page); + } + + @GetMapping("/batch/{id}") + public ApiResponse getBatchDetail(@PathVariable Long id) { + VoucherBatchResp batch = voucherBatchService.getBatchDetail(id); + return ApiResponse.success(batch); + } + + @GetMapping("/batch/{id}/stats") + public ApiResponse getBatchStats(@PathVariable Long id) { + VoucherBatchStatsResp stats = voucherBatchService.getBatchStats(id); + return ApiResponse.success(stats); + } + + @PutMapping("/batch/{id}/status") + public ApiResponse updateBatchStatus(@PathVariable Long id, @RequestParam Integer status) { + voucherBatchService.updateBatchStatus(id, status); + return ApiResponse.success(null); + } + + @PostMapping("/codes") + public ApiResponse> getCodeList(@RequestBody VoucherCodeQueryReq req) { + Page page = voucherCodeService.queryCodeList(req); + return ApiResponse.success(page); + } + + @PutMapping("/code/{id}/use") + public ApiResponse markCodeAsUsed(@PathVariable Long id, @RequestParam(required = false) String remark) { + voucherCodeService.markCodeAsUsed(id, remark); + return ApiResponse.success(null); + } + + @GetMapping("/scenic/{scenicId}/users") + public ApiResponse> getUsersInScenic(@PathVariable Long scenicId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + VoucherCodeQueryReq req = new VoucherCodeQueryReq(); + req.setScenicId(scenicId); + req.setPageNum(pageNum); + req.setPageSize(pageSize); + Page page = voucherCodeService.queryCodeList(req); + return ApiResponse.success(page); + } + + @PostMapping("/mobile/claim") + public ApiResponse claimVoucher(@RequestBody VoucherClaimReq req) { + VoucherCodeResp result = voucherCodeService.claimVoucher(req); + return ApiResponse.success(result); + } + + @GetMapping("/mobile/my-codes") + public ApiResponse> getMyVoucherCodes(@RequestParam Long faceId) { + List codes = voucherCodeService.getMyVoucherCodes(faceId); + return ApiResponse.success(codes); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/BundleProductItem.java b/src/main/java/com/ycwl/basic/pricing/dto/BundleProductItem.java new file mode 100644 index 0000000..fe5a9fe --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/BundleProductItem.java @@ -0,0 +1,30 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +/** + * 一口价套餐商品项DTO + */ +@Data +public class BundleProductItem { + + /** + * 商品类型 + */ + private String type; + + /** + * 商品子类型(可选) + */ + private String subType; + + /** + * 商品ID(可选) + */ + private String productId; + + /** + * 数量 + */ + private Integer quantity; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java new file mode 100644 index 0000000..6d315b7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java @@ -0,0 +1,38 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.enums.CouponType; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 优惠券信息DTO + */ +@Data +public class CouponInfo { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 优惠券名称 + */ + private String couponName; + + /** + * 优惠类型 + */ + private CouponType discountType; + + /** + * 优惠值 + */ + private BigDecimal discountValue; + + /** + * 实际优惠金额 + */ + private BigDecimal actualDiscountAmount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java new file mode 100644 index 0000000..a8231e6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java @@ -0,0 +1,42 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 优惠券使用请求DTO + */ +@Data +public class CouponUseRequest { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 订单ID + */ + private String orderId; + + /** + * 原始金额 + */ + private BigDecimal originalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 景区ID + */ + private String scenicId; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java new file mode 100644 index 0000000..a82aa00 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java @@ -0,0 +1,38 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 优惠券使用结果DTO + */ +@Data +public class CouponUseResult { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 订单ID + */ + private String orderId; + + /** + * 使用时间 + */ + private Date useTime; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountCombinationResult.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountCombinationResult.java new file mode 100644 index 0000000..7478b80 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountCombinationResult.java @@ -0,0 +1,53 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠组合结果 + */ +@Data +public class DiscountCombinationResult { + + /** + * 原始金额 + */ + private BigDecimal originalAmount; + + /** + * 最终金额 + */ + private BigDecimal finalAmount; + + /** + * 总优惠金额 + */ + private BigDecimal totalDiscountAmount; + + /** + * 应用的优惠列表(按优先级排序) + */ + private List appliedDiscounts; + + /** + * 可用但未应用的优惠列表 + */ + private List availableDiscounts; + + /** + * 优惠详情列表(用于展示) + */ + private List discountDetails; + + /** + * 计算是否成功 + */ + private Boolean success; + + /** + * 错误信息(如果success为false) + */ + private String errorMessage; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java new file mode 100644 index 0000000..e7bacc8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java @@ -0,0 +1,89 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 折扣明细DTO + */ +@Data +public class DiscountDetail { + + /** + * 折扣类型 + */ + private String discountType; + + /** + * 折扣名称 + */ + private String discountName; + + /** + * 折扣金额 + */ + private BigDecimal discountAmount; + + /** + * 折扣描述 + */ + private String description; + + /** + * 排序(数值越小越靠前) + */ + private Integer sortOrder; + + /** + * 创建限时立减折扣明细 + */ + public static DiscountDetail createLimitedTimeDiscount(BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("LIMITED_TIME"); + detail.setDiscountName("限时立减"); + detail.setDiscountAmount(discountAmount); + detail.setDescription("限时优惠,立即享受"); + detail.setSortOrder(2); // 限时立减排在券码后面 + return detail; + } + + /** + * 创建优惠券折扣明细 + */ + public static DiscountDetail createCouponDiscount(String couponName, BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("COUPON"); + detail.setDiscountName(couponName); + detail.setDiscountAmount(discountAmount); + detail.setDescription("优惠券减免"); + detail.setSortOrder(3); // 优惠券排在限时立减后面 + return detail; + } + + /** + * 创建券码折扣明细 + */ + public static DiscountDetail createVoucherDiscount(String voucherCode, String discountTypeName, BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("VOUCHER"); + detail.setDiscountName("券码优惠"); + detail.setDiscountAmount(discountAmount); + detail.setDescription(String.format("券码 %s - %s", voucherCode, discountTypeName)); + detail.setSortOrder(1); // 券码优先级最高,排在最前面 + return detail; + } + + /** + * 创建一口价折扣明细 + */ + public static DiscountDetail createBundleDiscount(BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("BUNDLE"); + detail.setDiscountName("一口价优惠"); + detail.setDiscountAmount(discountAmount); + detail.setDescription("一口价购买更优惠"); + detail.setSortOrder(4); // 一口价排在最后 + return detail; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetectionContext.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetectionContext.java new file mode 100644 index 0000000..a0a08ea --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetectionContext.java @@ -0,0 +1,53 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠检测上下文 + */ +@Data +public class DiscountDetectionContext { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户faceId + */ + private Long faceId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 商品列表 + */ + private List products; + + /** + * 当前金额 + */ + private BigDecimal currentAmount; + + /** + * 用户主动输入的券码 + */ + private String voucherCode; + + /** + * 是否自动使用优惠券 + */ + private Boolean autoUseCoupon; + + /** + * 是否自动使用券码 + */ + private Boolean autoUseVoucher; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java new file mode 100644 index 0000000..f5d59c3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java @@ -0,0 +1,82 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 优惠信息DTO + */ +@Data +public class DiscountInfo { + + /** + * 优惠ID + */ + private Long discountId; + + /** + * 优惠类型(COUPON, VOUCHER等) + */ + private String discountType; + + /** + * 优惠名称 + */ + private String discountName; + + /** + * 优惠描述 + */ + private String discountDescription; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 原始优惠值(用于百分比折扣等) + */ + private BigDecimal originalValue; + + /** + * 优惠值类型(PERCENTAGE, FIXED_AMOUNT等) + */ + private String valueType; + + /** + * 最小消费金额限制 + */ + private BigDecimal minAmount; + + /** + * 最大优惠金额限制 + */ + private BigDecimal maxDiscount; + + /** + * 优惠提供者类型 + */ + private String providerType; + + /** + * 优惠优先级 + */ + private Integer priority; + + /** + * 是否可与其他优惠叠加 + */ + private Boolean stackable; + + /** + * 券码(如果是voucher类型) + */ + private String voucherCode; + + /** + * 优惠券ID(如果是coupon类型) + */ + private Long couponId; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java new file mode 100644 index 0000000..e59c835 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java @@ -0,0 +1,43 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠应用结果 + */ +@Data +public class DiscountResult { + + /** + * 应用的优惠信息 + */ + private DiscountInfo discountInfo; + + /** + * 实际优惠金额 + */ + private BigDecimal actualDiscountAmount; + + /** + * 应用后的金额 + */ + private BigDecimal finalAmount; + + /** + * 是否成功应用 + */ + private Boolean success; + + /** + * 失败原因(如果success为false) + */ + private String failureReason; + + /** + * 影响的商品项(用于商品级别的优惠) + */ + private List affectedProducts; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java new file mode 100644 index 0000000..9563334 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java @@ -0,0 +1,52 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 价格计算请求DTO + */ +@Data +public class PriceCalculationRequest { + + /** + * 商品列表 + */ + private List products; + + /** + * 用户ID + */ + private Long userId; + + /** + * 是否自动使用优惠券 + */ + private Boolean autoUseCoupon = true; + + /** + * 用户输入的券码 + */ + private String voucherCode; + + /** + * 景区ID(用于券码验证) + */ + private Long scenicId; + + /** + * 用户faceId(用于券码领取资格验证) + */ + private Long faceId; + + /** + * 是否自动使用券码优惠 + */ + private Boolean autoUseVoucher = true; + + /** + * 是否仅预览优惠(不实际使用) + */ + private Boolean previewOnly = false; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java new file mode 100644 index 0000000..c23e8bb --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java @@ -0,0 +1,58 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 价格计算结果DTO + */ +@Data +public class PriceCalculationResult { + + /** + * 原始金额(用于前端展示的总原价) + */ + private BigDecimal originalAmount; + + /** + * 商品小计金额(按实际计算价格) + */ + private BigDecimal subtotalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 最终金额 + */ + private BigDecimal finalAmount; + + /** + * 使用的优惠券信息 + */ + private CouponInfo usedCoupon; + + /** + * 使用的券码信息 + */ + private VoucherInfo usedVoucher; + + /** + * 折扣明细列表(包含限时立减、优惠券、券码、一口价等) + */ + private List discountDetails; + + /** + * 可用但未使用的优惠列表(预览时使用) + */ + private List availableDiscounts; + + /** + * 商品明细列表 + */ + private List productDetails; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java new file mode 100644 index 0000000..c75ca33 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java @@ -0,0 +1,32 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 价格详情(内部计算用) + */ +@Data +public class PriceDetails { + + /** + * 实际计算总金额 + */ + private BigDecimal totalAmount; + + /** + * 原价总金额 + */ + private BigDecimal originalTotalAmount; + + public PriceDetails() { + this.totalAmount = BigDecimal.ZERO; + this.originalTotalAmount = BigDecimal.ZERO; + } + + public PriceDetails(BigDecimal totalAmount, BigDecimal originalTotalAmount) { + this.totalAmount = totalAmount; + this.originalTotalAmount = originalTotalAmount; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java new file mode 100644 index 0000000..c5d2160 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java @@ -0,0 +1,53 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.enums.ProductType; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 商品项DTO + */ +@Data +public class ProductItem { + + /** + * 商品类型 + */ + private ProductType productType; + + /** + * 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID + */ + private String productId; + + /** + * 数量(如原片数量、照片数量等) + */ + private Integer quantity; + + /** + * 购买数量(购买几个该商品) + */ + private Integer purchaseCount; + + /** + * 原价(用于前端展示) + */ + private BigDecimal originalPrice; + + /** + * 单价(实际计算用的价格) + */ + private BigDecimal unitPrice; + + /** + * 小计(计算后填入) + */ + private BigDecimal subtotal; + + /** + * 景区ID + */ + private String scenicId; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java new file mode 100644 index 0000000..65ae04b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 商品价格信息(内部计算用) + */ +@Data +public class ProductPriceInfo { + + /** + * 实际计算价格 + */ + private BigDecimal actualPrice; + + /** + * 原价(用于前端展示) + */ + private BigDecimal originalPrice; + + public ProductPriceInfo(BigDecimal actualPrice, BigDecimal originalPrice) { + this.actualPrice = actualPrice; + this.originalPrice = originalPrice != null ? originalPrice : actualPrice; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java new file mode 100644 index 0000000..b4e80b8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java @@ -0,0 +1,84 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 券码信息DTO + */ +@Data +public class VoucherInfo { + + /** + * 券码ID + */ + private Long voucherId; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 批次ID + */ + private Long batchId; + + /** + * 批次名称 + */ + private String batchName; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 推客ID + */ + private Long brokerId; + + /** + * 优惠类型 + */ + private VoucherDiscountType discountType; + + /** + * 优惠值 + */ + private BigDecimal discountValue; + + /** + * 实际优惠金额 + */ + private BigDecimal actualDiscountAmount; + + /** + * 状态 + */ + private Integer status; + + /** + * 领取时间 + */ + private Date claimedTime; + + /** + * 使用时间 + */ + private Date usedTime; + + /** + * 是否可用 + */ + private Boolean available; + + /** + * 不可用原因 + */ + private String unavailableReason; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReq.java new file mode 100644 index 0000000..8582fea --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReq.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.dto.req; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class VoucherBatchCreateReq { + private String batchName; + private Long scenicId; + private Long brokerId; + private Integer discountType; + private BigDecimal discountValue; + private Integer totalCount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchQueryReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchQueryReq.java new file mode 100644 index 0000000..426c947 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchQueryReq.java @@ -0,0 +1,14 @@ +package com.ycwl.basic.pricing.dto.req; + +import com.ycwl.basic.model.common.BaseQueryParameterReq; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class VoucherBatchQueryReq extends BaseQueryParameterReq { + private Long scenicId; + private Long brokerId; + private Integer status; + private String batchName; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java new file mode 100644 index 0000000..9680742 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java @@ -0,0 +1,11 @@ +package com.ycwl.basic.pricing.dto.req; + +import lombok.Data; + +@Data +public class VoucherClaimReq { + private Long scenicId; + private Long brokerId; + private Long faceId; + private String code; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherCodeQueryReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherCodeQueryReq.java new file mode 100644 index 0000000..a0065c5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherCodeQueryReq.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.dto.req; + +import com.ycwl.basic.model.common.BaseQueryParameterReq; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class VoucherCodeQueryReq extends BaseQueryParameterReq { + private Long batchId; + private Long scenicId; + private Long faceId; + private Integer status; + private String code; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java new file mode 100644 index 0000000..dcfe54e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java @@ -0,0 +1,16 @@ +package com.ycwl.basic.pricing.dto.req; + +import lombok.Data; + +/** + * 打印小票请求 + */ +@Data +public class VoucherPrintReq { + + private Long faceId; + + private Long brokerId; + + private Long scenicId; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java new file mode 100644 index 0000000..7c75781 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java @@ -0,0 +1,25 @@ +package com.ycwl.basic.pricing.dto.resp; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class VoucherBatchResp { + private Long id; + private String batchName; + private Long scenicId; + private Long brokerId; + private Integer discountType; + private String discountTypeName; + private BigDecimal discountValue; + private Integer totalCount; + private Integer usedCount; + private Integer claimedCount; + private Integer availableCount; + private Integer status; + private String statusName; + private Date createTime; + private Long createBy; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchStatsResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchStatsResp.java new file mode 100644 index 0000000..423036c --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchStatsResp.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.dto.resp; + +import lombok.Data; + +@Data +public class VoucherBatchStatsResp { + private Long batchId; + private String batchName; + private Integer totalCount; + private Integer claimedCount; + private Integer usedCount; + private Integer availableCount; + private Double claimedRate; + private Double usedRate; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java new file mode 100644 index 0000000..b7f0f8a --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.pricing.dto.resp; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class VoucherCodeResp { + private Long id; + private Long batchId; + private String batchName; + private Long scenicId; + private String code; + private Integer status; + private String statusName; + private Long faceId; + private Date claimedTime; + private Date usedTime; + private String remark; + private Date createTime; + + private Integer discountType; + private String discountTypeName; + private String discountDescription; + private BigDecimal discountValue; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java new file mode 100644 index 0000000..4a9e552 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java @@ -0,0 +1,37 @@ +package com.ycwl.basic.pricing.dto.resp; + +import lombok.Data; + +import java.util.Date; + +/** + * 打印小票响应 + */ +@Data +public class VoucherPrintResp { + + /** + * 流水号 + */ + private String code; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 打印状态:0=待打印,1=打印成功,2=打印失败 + */ + private Integer printStatus; + + /** + * 错误信息 + */ + private String errorMessage; + + /** + * 创建时间 + */ + private Date createTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java new file mode 100644 index 0000000..219a55a --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java @@ -0,0 +1,76 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ycwl.basic.pricing.dto.BundleProductItem; +import com.ycwl.basic.pricing.handler.BundleProductListTypeHandler; +import lombok.Data; +import org.apache.ibatis.type.JdbcType; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * 一口价配置实体 + */ +@Data +@TableName("price_bundle_config") +public class PriceBundleConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 套餐名称 + */ + private String bundleName; + + /** + * 景区ID + */ + private String scenicId; + + /** + * 套餐价格 + */ + private BigDecimal bundlePrice; + + /** + * 包含商品 + */ + @TableField(typeHandler = BundleProductListTypeHandler.class, jdbcType = JdbcType.VARCHAR) + private List includedProducts; + + /** + * 排除商品 + */ + @TableField(typeHandler = BundleProductListTypeHandler.class, jdbcType = JdbcType.VARCHAR) + private List excludedProducts; + + /** + * 套餐描述 + */ + private String description; + + /** + * 是否启用 + */ + private Boolean isActive; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java new file mode 100644 index 0000000..dd5c24b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java @@ -0,0 +1,66 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ycwl.basic.pricing.enums.CouponStatus; +import lombok.Data; + +import java.util.Date; + +/** + * 优惠券领用记录实体 + */ +@Data +@TableName("price_coupon_claim_record") +public class PriceCouponClaimRecord { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 领取时间 + */ + private Date claimTime; + + /** + * 使用时间 + */ + private Date useTime; + + /** + * 订单ID + */ + private String orderId; + + /** + * 状态 + */ + private CouponStatus status; + + /** + * 景区ID - 记录优惠券在哪个景区被领取/使用 + */ + private String scenicId; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java new file mode 100644 index 0000000..06be0ce --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java @@ -0,0 +1,97 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ycwl.basic.pricing.enums.CouponType; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 优惠券配置实体 + */ +@Data +@TableName("price_coupon_config") +public class PriceCouponConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 优惠券名称 + */ + private String couponName; + + /** + * 优惠券类型 + */ + private CouponType couponType; + + /** + * 优惠值 + */ + private BigDecimal discountValue; + + /** + * 最小使用金额 + */ + private BigDecimal minAmount; + + /** + * 最大优惠金额 + */ + private BigDecimal maxDiscount; + + /** + * 适用商品类型(JSON) + */ + private String applicableProducts; + + /** + * 发行总量 + */ + private Integer totalQuantity; + + /** + * 已使用数量 + */ + private Integer usedQuantity; + + /** + * 生效时间 + */ + private LocalDateTime validFrom; + + /** + * 失效时间 + */ + private LocalDateTime validUntil; + + /** + * 是否启用 + */ + private Boolean isActive; + + /** + * 景区ID - 限制优惠券只能在该景区使用 + */ + private String scenicId; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java new file mode 100644 index 0000000..4677774 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 商品价格配置实体 + */ +@Data +@TableName("price_product_config") +public class PriceProductConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 商品类型 + */ + private String productType; + + /** + * 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID + */ + private String productId; + + /** + * 景区ID:用于前端搜索和编辑时正确显示景区内容 + */ + private String scenicId; + + /** + * 商品名称 + */ + private String productName; + + /** + * 基础价格 + */ + private BigDecimal basePrice; + + /** + * 商品原价:用于前端展示优惠力度,当original_price > base_price时显示限时立减 + */ + private BigDecimal originalPrice; + + /** + * 价格单位 + */ + private String unit; + + /** + * 是否启用 + */ + private Boolean isActive; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java new file mode 100644 index 0000000..f3a3e95 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java @@ -0,0 +1,85 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 阶梯定价配置实体 + */ +@Data +@TableName("price_tier_config") +public class PriceTierConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 商品类型 + */ + private String productType; + + /** + * 具体商品ID:与price_product_config的product_id对应 + */ + private String productId; + + /** + * 景区ID:用于前端搜索和编辑时正确显示景区内容 + */ + private String scenicId; + + /** + * 最小数量 + */ + private Integer minQuantity; + + /** + * 最大数量 + */ + private Integer maxQuantity; + + /** + * 阶梯价格 + */ + private BigDecimal price; + + /** + * 阶梯原价:用于前端展示优惠力度,当original_price > price时显示限时立减 + */ + private BigDecimal originalPrice; + + /** + * 计价单位 + */ + private String unit; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 是否启用 + */ + private Boolean isActive; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java new file mode 100644 index 0000000..49e4724 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java @@ -0,0 +1,80 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 券码批次配置实体 + */ +@Data +@TableName("price_voucher_batch_config") +public class PriceVoucherBatchConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 券码批次名称 + */ + private String batchName; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 推客ID + */ + private Long brokerId; + + /** + * 优惠类型:0=全场免费,1=商品降价,2=商品打折 + */ + private Integer discountType; + + /** + * 优惠值(降价金额或折扣百分比) + */ + private BigDecimal discountValue; + + /** + * 总券码数量 + */ + private Integer totalCount; + + /** + * 已使用数量 + */ + private Integer usedCount; + + /** + * 已领取数量 + */ + private Integer claimedCount; + + /** + * 状态:0=禁用,1=启用 + */ + private Integer status; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java new file mode 100644 index 0000000..1b59dc2 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * 券码实体 + */ +@Data +@TableName("price_voucher_code") +public class PriceVoucherCode { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 批次ID + */ + private Long batchId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 券码 + */ + private String code; + + /** + * 状态:0=未领取,1=已领取未使用,2=已使用 + */ + private Integer status; + + /** + * 领取人faceId + */ + private Long faceId; + + /** + * 领取时间 + */ + private Date claimedTime; + + /** + * 使用时间 + */ + private Date usedTime; + + /** + * 使用备注 + */ + private String remark; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java new file mode 100644 index 0000000..49311c1 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * 优惠券打印记录实体 + */ +@Data +@TableName("voucher_print_record") +public class VoucherPrintRecord { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 流水号 + */ + private String code; + + /** + * 用户faceId + */ + private Long faceId; + + /** + * 经纪人ID + */ + private Long brokerId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 券码ID + */ + private Long voucherCodeId; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 打印状态:0=待打印,1=打印成功,2=打印失败 + */ + private Integer printStatus; + + /** + * 错误信息 + */ + private String errorMessage; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java b/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java new file mode 100644 index 0000000..74e511e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java @@ -0,0 +1,31 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 优惠券状态枚举 + */ +@Getter +@AllArgsConstructor +public enum CouponStatus { + + CLAIMED("claimed", "已领取"), + USED("used", "已使用"), + EXPIRED("expired", "已过期"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static CouponStatus fromCode(String code) { + for (CouponStatus status : values()) { + if (status.code.equals(code)) { + return status; + } + } + throw new IllegalArgumentException("Unknown coupon status code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java b/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java new file mode 100644 index 0000000..6908d3d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java @@ -0,0 +1,30 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 优惠券类型枚举 + */ +@Getter +@AllArgsConstructor +public enum CouponType { + + PERCENTAGE("percentage", "百分比折扣"), + FIXED_AMOUNT("fixed_amount", "固定金额减免"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static CouponType fromCode(String code) { + for (CouponType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown coupon type code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java new file mode 100644 index 0000000..bc3aa78 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java @@ -0,0 +1,33 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 商品类型枚举 + */ +@Getter +@AllArgsConstructor +public enum ProductType { + + VLOG_VIDEO("VLOG_VIDEO", "Vlog视频"), + RECORDING_SET("RECORDING_SET", "录像集"), + PHOTO_SET("PHOTO_SET", "照相集"), + PHOTO_PRINT("PHOTO_PRINT", "照片打印"), + MACHINE_PRINT("MACHINE_PRINT", "一体机打印"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static ProductType fromCode(String code) { + for (ProductType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown product type code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java b/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java new file mode 100644 index 0000000..b4651bb --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java @@ -0,0 +1,74 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 券码状态枚举 + */ +@Getter +@AllArgsConstructor +public enum VoucherCodeStatus { + + /** + * 未领取 + */ + UNCLAIMED(0, "未领取"), + + /** + * 已领取未使用 + */ + CLAIMED_UNUSED(1, "已领取未使用"), + + /** + * 已使用 + */ + USED(2, "已使用"); + + private final Integer code; + private final String name; + + /** + * 根据代码获取枚举值 + * @param code 代码 + * @return 枚举值 + */ + public static VoucherCodeStatus getByCode(Integer code) { + if (code == null) { + return null; + } + for (VoucherCodeStatus status : values()) { + if (status.getCode().equals(code)) { + return status; + } + } + return null; + } + + /** + * 检查是否为有效的状态代码 + * @param code 代码 + * @return 是否有效 + */ + public static boolean isValidCode(Integer code) { + return getByCode(code) != null; + } + + /** + * 检查是否可以使用(已领取未使用状态) + * @param code 状态代码 + * @return 是否可以使用 + */ + public static boolean canUse(Integer code) { + return CLAIMED_UNUSED.getCode().equals(code); + } + + /** + * 检查是否已使用 + * @param code 状态代码 + * @return 是否已使用 + */ + public static boolean isUsed(Integer code) { + return USED.getCode().equals(code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/VoucherDiscountType.java b/src/main/java/com/ycwl/basic/pricing/enums/VoucherDiscountType.java new file mode 100644 index 0000000..af9fdfc --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/VoucherDiscountType.java @@ -0,0 +1,57 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 券码优惠类型枚举 + */ +@Getter +@AllArgsConstructor +public enum VoucherDiscountType { + + /** + * 全场免费 - 所有商品免费 + */ + FREE_ALL(0, "全场免费", "所有商品免费"), + + /** + * 商品降价 - 每个商品减免指定金额 + */ + REDUCE_PRICE(1, "商品降价", "每个商品减免指定金额"), + + /** + * 商品打折 - 每个商品按百分比打折 + */ + DISCOUNT(2, "商品打折", "每个商品按百分比打折"); + + private final Integer code; + private final String name; + private final String description; + + /** + * 根据代码获取枚举值 + * @param code 代码 + * @return 枚举值 + */ + public static VoucherDiscountType getByCode(Integer code) { + if (code == null) { + return null; + } + for (VoucherDiscountType type : values()) { + if (type.getCode().equals(code)) { + return type; + } + } + return null; + } + + /** + * 检查是否为有效的优惠类型代码 + * @param code 代码 + * @return 是否有效 + */ + public static boolean isValidCode(Integer code) { + return getByCode(code) != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java b/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java new file mode 100644 index 0000000..73a6fa4 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 优惠券无效异常 + */ +public class CouponInvalidException extends RuntimeException { + + public CouponInvalidException(String message) { + super(message); + } + + public CouponInvalidException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/DiscountDetectionException.java b/src/main/java/com/ycwl/basic/pricing/exception/DiscountDetectionException.java new file mode 100644 index 0000000..b1b2852 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/DiscountDetectionException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 优惠检测异常 + */ +public class DiscountDetectionException extends RuntimeException { + + public DiscountDetectionException(String message) { + super(message); + } + + public DiscountDetectionException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java b/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java new file mode 100644 index 0000000..891b7ef --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 价格计算异常 + */ +public class PriceCalculationException extends RuntimeException { + + public PriceCalculationException(String message) { + super(message); + } + + public PriceCalculationException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java b/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java new file mode 100644 index 0000000..9b84eb9 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java @@ -0,0 +1,106 @@ +package com.ycwl.basic.pricing.exception; + +import com.ycwl.basic.utils.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 价格查询系统全局异常处理器 + */ +@Slf4j +@RestControllerAdvice(basePackages = "com.ycwl.basic.pricing") +public class PricingExceptionHandler { + + /** + * 处理价格计算异常 + */ + @ExceptionHandler(PriceCalculationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handlePriceCalculationException(PriceCalculationException e) { + log.error("价格计算异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理优惠券无效异常 + */ + @ExceptionHandler(CouponInvalidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleCouponInvalidException(CouponInvalidException e) { + log.error("优惠券无效异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理商品配置未找到异常 + */ + @ExceptionHandler(ProductConfigNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiResponse handleProductConfigNotFoundException(ProductConfigNotFoundException e) { + log.error("商品配置未找到异常", e); + return ApiResponse.buildResponse(404, e.getMessage()); + } + + /** + * 处理非法参数异常 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常", e); + return ApiResponse.buildResponse(400, "参数错误: " + e.getMessage()); + } + + /** + * 处理券码无效异常 + */ + @ExceptionHandler(VoucherInvalidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleVoucherInvalidException(VoucherInvalidException e) { + log.error("券码无效异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理券码已使用异常 + */ + @ExceptionHandler(VoucherAlreadyUsedException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleVoucherAlreadyUsedException(VoucherAlreadyUsedException e) { + log.error("券码已使用异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理券码不可领取异常 + */ + @ExceptionHandler(VoucherNotClaimableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleVoucherNotClaimableException(VoucherNotClaimableException e) { + log.error("券码不可领取异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理优惠检测异常 + */ + @ExceptionHandler(DiscountDetectionException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleDiscountDetectionException(DiscountDetectionException e) { + log.error("优惠检测异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理通用异常 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse handleException(Exception e) { + log.error("系统异常", e); + return ApiResponse.buildResponse(500, "系统内部错误"); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java b/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java new file mode 100644 index 0000000..9bdbe63 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 商品配置未找到异常 + */ +public class ProductConfigNotFoundException extends RuntimeException { + + public ProductConfigNotFoundException(String message) { + super(message); + } + + public ProductConfigNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/VoucherAlreadyUsedException.java b/src/main/java/com/ycwl/basic/pricing/exception/VoucherAlreadyUsedException.java new file mode 100644 index 0000000..2067de0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/VoucherAlreadyUsedException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 券码已使用异常 + */ +public class VoucherAlreadyUsedException extends RuntimeException { + + public VoucherAlreadyUsedException(String message) { + super(message); + } + + public VoucherAlreadyUsedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/VoucherInvalidException.java b/src/main/java/com/ycwl/basic/pricing/exception/VoucherInvalidException.java new file mode 100644 index 0000000..6c62288 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/VoucherInvalidException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 券码无效异常 + */ +public class VoucherInvalidException extends RuntimeException { + + public VoucherInvalidException(String message) { + super(message); + } + + public VoucherInvalidException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/VoucherNotClaimableException.java b/src/main/java/com/ycwl/basic/pricing/exception/VoucherNotClaimableException.java new file mode 100644 index 0000000..2ea2e2d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/VoucherNotClaimableException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 券码不可领取异常 + */ +public class VoucherNotClaimableException extends RuntimeException { + + public VoucherNotClaimableException(String message) { + super(message); + } + + public VoucherNotClaimableException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/handler/BundleProductListTypeHandler.java b/src/main/java/com/ycwl/basic/pricing/handler/BundleProductListTypeHandler.java new file mode 100644 index 0000000..d6d9c8b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/handler/BundleProductListTypeHandler.java @@ -0,0 +1,72 @@ +package com.ycwl.basic.pricing.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.BundleProductItem; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * 一口价商品列表类型处理器 + */ +@Slf4j +public class BundleProductListTypeHandler extends BaseTypeHandler> { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final TypeReference> typeReference = new TypeReference>() {}; + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) throws SQLException { + try { + String json = objectMapper.writeValueAsString(parameter); + ps.setString(i, json); + log.debug("序列化商品列表: {}", json); + } catch (JsonProcessingException e) { + log.error("序列化商品列表失败", e); + throw new SQLException("序列化商品列表失败", e); + } + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + String json = rs.getString(columnName); + return parseJson(json, columnName); + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String json = rs.getString(columnIndex); + return parseJson(json, "columnIndex:" + columnIndex); + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String json = cs.getString(columnIndex); + return parseJson(json, "columnIndex:" + columnIndex); + } + + private List parseJson(String json, String source) { + if (json == null || json.trim().isEmpty()) { + log.debug("从{}获取的JSON为空,返回空列表", source); + return new ArrayList<>(); + } + + try { + List result = objectMapper.readValue(json, typeReference); + log.debug("从{}反序列化商品列表成功,数量: {}", source, result != null ? result.size() : 0); + return result != null ? result : new ArrayList<>(); + } catch (JsonProcessingException e) { + log.error("从{}反序列化商品列表失败,JSON: {}", source, json, e); + return new ArrayList<>(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java new file mode 100644 index 0000000..f65e4f0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java @@ -0,0 +1,84 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +/** + * 一口价配置Mapper + */ +@Mapper +public interface PriceBundleConfigMapper extends BaseMapper { + + /** + * 查询启用的一口价配置 + */ + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + + "included_products, excluded_products, " + + "description, is_active, create_time, update_time " + + "FROM price_bundle_config WHERE is_active = 1") + @Results({ + @Result(column = "included_products", property = "includedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class), + @Result(column = "excluded_products", property = "excludedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class) + }) + List selectActiveBundles(); + + /** + * 根据ID查询启用的配置 + */ + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + + "included_products, excluded_products, " + + "description, is_active, create_time, update_time " + + "FROM price_bundle_config WHERE id = #{id} AND is_active = 1") + @Results({ + @Result(column = "included_products", property = "includedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class), + @Result(column = "excluded_products", property = "excludedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class) + }) + PriceBundleConfig selectActiveBundleById(Long id); + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 查询所有一口价配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + + "included_products, excluded_products, " + + "description, is_active, create_time, update_time " + + "FROM price_bundle_config ORDER BY is_active DESC, bundle_name ASC") + @Results({ + @Result(column = "included_products", property = "includedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class), + @Result(column = "excluded_products", property = "excludedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class) + }) + List selectAllBundlesForAdmin(); + + /** + * 插入一口价配置 + */ + @Insert("INSERT INTO price_bundle_config (bundle_name, scenic_id, bundle_price, included_products, excluded_products, " + + "description, is_active, create_time, update_time) VALUES " + + "(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " + + "#{description}, #{isActive}, NOW(), NOW())") + int insertBundleConfig(PriceBundleConfig config); + + /** + * 更新一口价配置 + */ + @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, scenic_id = #{scenicId}, bundle_price = #{bundlePrice}, " + + "included_products = #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, excluded_products = #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " + + "description = #{description}, is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") + int updateBundleConfig(PriceBundleConfig config); + + /** + * 更新一口价配置状态 + */ + @Update("UPDATE price_bundle_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") + int updateBundleConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java new file mode 100644 index 0000000..ec83ac9 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java @@ -0,0 +1,209 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.enums.CouponStatus; +import org.apache.ibatis.annotations.Insert; +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; + +/** + * 优惠券领用记录Mapper + */ +@Mapper +public interface PriceCouponClaimRecordMapper extends BaseMapper { + + /** + * 查询用户可用的优惠券记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value, c.min_amount, " + + "c.max_discount, c.applicable_products, c.valid_from, c.valid_until " + + "FROM price_coupon_claim_record r " + + "JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.user_id = #{userId} AND r.status = 'CLAIMED' " + + "AND c.is_active = 1 AND c.valid_from <= NOW() AND c.valid_until > NOW()") + List selectUserAvailableCoupons(Long userId); + + /** + * 查询用户特定优惠券记录 + */ + @Select("SELECT * FROM price_coupon_claim_record " + + "WHERE user_id = #{userId} AND coupon_id = #{couponId} AND status = 'CLAIMED'") + PriceCouponClaimRecord selectUserCouponRecord(@Param("userId") Long userId, + @Param("couponId") Long couponId); + + /** + * 更新优惠券使用状态 + */ + @Update("UPDATE price_coupon_claim_record SET status = #{status}, " + + "use_time = #{useTime}, order_id = #{orderId}, scenic_id = #{scenicId}, update_time = NOW() " + + "WHERE id = #{id}") + int updateCouponStatus(@Param("id") Long id, + @Param("status") CouponStatus status, + @Param("useTime") java.util.Date useTime, + @Param("orderId") String orderId, + @Param("scenicId") String scenicId); + + /** + * 插入优惠券领用记录 + */ + @Insert("INSERT INTO price_coupon_claim_record (coupon_id, user_id, claim_time, status, scenic_id, create_time, update_time) " + + "VALUES (#{couponId}, #{userId}, NOW(), #{status}, #{scenicId}, NOW(), NOW())") + int insertClaimRecord(PriceCouponClaimRecord record); + + /** + * 更新优惠券记录 + */ + @Update("UPDATE price_coupon_claim_record SET status = #{status}, use_time = #{useTime}, " + + "order_id = #{orderId}, update_time = NOW() WHERE id = #{id}") + int updateClaimRecord(PriceCouponClaimRecord record); + + // ==================== 管理端接口 ==================== + + /** + * 管理端:查询所有优惠券领取记录(支持分页) + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "ORDER BY r.create_time DESC") + List selectAllForAdmin(); + + /** + * 管理端:根据条件查询优惠券领取记录(支持分页) + */ + @Select("") + List selectByConditionsForAdmin(@Param("userId") Long userId, + @Param("couponId") Long couponId, + @Param("status") CouponStatus status, + @Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("scenicId") String scenicId); + + /** + * 管理端:根据用户ID查询优惠券领取记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.user_id = #{userId} " + + "ORDER BY r.create_time DESC") + List selectByUserIdForAdmin(@Param("userId") Long userId); + + /** + * 管理端:根据优惠券ID查询领取记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.coupon_id = #{couponId} " + + "ORDER BY r.create_time DESC") + List selectByCouponIdForAdmin(@Param("couponId") Long couponId); + + /** + * 管理端:根据状态查询领取记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.status = #{status} " + + "ORDER BY r.create_time DESC") + List selectByStatusForAdmin(@Param("status") CouponStatus status); + + /** + * 管理端:统计优惠券使用情况 + */ + @Select("SELECT " + + "COUNT(*) as total_claimed, " + + "COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " + + "COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available " + + "FROM price_coupon_claim_record WHERE coupon_id = #{couponId}") + java.util.Map selectCouponUsageStats(@Param("couponId") Long couponId); + + /** + * 管理端:统计优惠券详细使用情况 + */ + @Select("SELECT " + + "COUNT(*) as total_claimed, " + + "COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " + + "COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " + + "CASE WHEN COUNT(*) > 0 THEN COUNT(CASE WHEN status = 'USED' THEN 1 END) / COUNT(*) ELSE 0 END as usage_rate, " + + "CASE WHEN COUNT(CASE WHEN status = 'USED' THEN 1 END) > 0 THEN " + + "AVG(CASE WHEN status = 'USED' AND use_time IS NOT NULL AND claim_time IS NOT NULL THEN " + + "DATEDIFF(use_time, claim_time) END) ELSE 0 END as avg_days_to_use " + + "FROM price_coupon_claim_record WHERE coupon_id = #{couponId}") + java.util.Map selectCouponDetailStats(@Param("couponId") Long couponId); + + /** + * 管理端:统计时间范围内的数据 + */ + @Select("") + java.util.Map selectPeriodStats(@Param("startDate") String startDate, + @Param("endDate") String endDate, + @Param("scenicId") String scenicId); + + /** + * 根据景区ID查询优惠券领取记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.scenic_id = #{scenicId} " + + "ORDER BY r.create_time DESC") + List selectByScenicIdForAdmin(@Param("scenicId") String scenicId); + + /** + * 统计景区优惠券使用情况 + */ + @Select("SELECT " + + "COUNT(*) as total_claimed, " + + "COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " + + "COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " + + "COUNT(CASE WHEN status = 'EXPIRED' THEN 1 END) as total_expired, " + + "COUNT(DISTINCT coupon_id) as total_coupon_types, " + + "COUNT(DISTINCT user_id) as total_users " + + "FROM price_coupon_claim_record WHERE scenic_id = #{scenicId}") + java.util.Map selectScenicCouponUsageStats(@Param("scenicId") String scenicId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java new file mode 100644 index 0000000..077e470 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java @@ -0,0 +1,136 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import org.apache.ibatis.annotations.Insert; +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; + +/** + * 优惠券配置Mapper + */ +@Mapper +public interface PriceCouponConfigMapper extends BaseMapper { + + /** + * 查询有效的优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE is_active = 1 " + + "AND valid_from <= NOW() AND valid_until > NOW() " + + "AND used_quantity < total_quantity") + List selectValidCoupons(); + + /** + * 根据ID查询优惠券(包括使用数量检查) + */ + @Select("SELECT * FROM price_coupon_config WHERE id = #{couponId} " + + "AND is_active = 1 AND valid_from <= NOW() AND valid_until > NOW() " + + "AND used_quantity < total_quantity") + PriceCouponConfig selectValidCouponById(Long couponId); + + /** + * 增加优惠券使用数量 + */ + @Update("UPDATE price_coupon_config SET used_quantity = used_quantity + 1, " + + "update_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity") + int incrementUsedQuantity(Long couponId); + + /** + * 插入优惠券配置 + */ + @Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " + + "max_discount, applicable_products, total_quantity, used_quantity, valid_from, valid_until, " + + "is_active, scenic_id, create_time, update_time) VALUES " + + "(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " + + "#{applicableProducts}, #{totalQuantity}, #{usedQuantity}, #{validFrom}, #{validUntil}, " + + "#{isActive}, #{scenicId}, NOW(), NOW())") + int insertCoupon(PriceCouponConfig coupon); + + /** + * 更新优惠券配置 + */ + @Update("UPDATE price_coupon_config SET coupon_name = #{couponName}, coupon_type = #{couponType}, " + + "discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " + + "applicable_products = #{applicableProducts}, total_quantity = #{totalQuantity}, " + + "valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " + + "scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}") + int updateCoupon(PriceCouponConfig coupon); + + // ==================== 管理端接口 ==================== + + /** + * 管理端:查询所有优惠券配置(包含禁用的) + */ + @Select("SELECT * FROM price_coupon_config ORDER BY create_time DESC") + List selectAllForAdmin(); + + /** + * 管理端:根据条件查询优惠券配置(支持分页) + */ + @Select("") + List selectByConditionsForAdmin(@Param("isActive") Boolean isActive, + @Param("couponName") String couponName, + @Param("scenicId") String scenicId); + + /** + * 管理端:根据状态查询优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE is_active = #{isActive} ORDER BY create_time DESC") + List selectByStatusForAdmin(@Param("isActive") Boolean isActive); + + /** + * 管理端:更新优惠券状态 + */ + @Update("UPDATE price_coupon_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") + int updateCouponStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); + + /** + * 管理端:删除优惠券配置 + */ + @Update("UPDATE price_coupon_config SET deleted = 1, update_time = NOW() WHERE id = #{id}") + int deleteCoupon(Long id); + + /** + * 查询指定景区的有效优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE is_active = 1 " + + "AND valid_from <= NOW() AND valid_until > NOW() " + + "AND used_quantity < total_quantity " + + "AND (scenic_id IS NULL OR scenic_id = #{scenicId})") + List selectValidCouponsByScenicId(@Param("scenicId") String scenicId); + + /** + * 管理端:根据景区ID查询优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE scenic_id = #{scenicId} ORDER BY create_time DESC") + List selectByScenicIdForAdmin(@Param("scenicId") String scenicId); + + /** + * 统计景区优惠券配置数量 + */ + @Select("SELECT " + + "COUNT(*) as total_coupons, " + + "COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_coupons, " + + "SUM(total_quantity) as total_quantity, " + + "SUM(used_quantity) as used_quantity " + + "FROM price_coupon_config WHERE scenic_id = #{scenicId}") + java.util.Map selectScenicCouponConfigStats(@Param("scenicId") String scenicId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java new file mode 100644 index 0000000..e4769a0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java @@ -0,0 +1,76 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import org.apache.ibatis.annotations.Insert; +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; + +/** + * 商品价格配置Mapper + */ +@Mapper +public interface PriceProductConfigMapper extends BaseMapper { + + /** + * 查询启用的商品配置 + */ + @Select("SELECT * FROM price_product_config WHERE is_active = 1") + List selectActiveConfigs(); + + /** + * 根据商品类型查询配置 + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND is_active = 1") + List selectByProductType(String productType); + + /** + * 根据商品类型和商品ID查询配置 + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1") + PriceProductConfig selectByProductTypeAndId(String productType, String productId); + + /** + * 检查是否存在default配置(包含禁用的) + */ + @Select("SELECT COUNT(*) FROM price_product_config WHERE product_type = #{productType} AND product_id = 'default'") + int countDefaultConfigsByProductType(@Param("productType") String productType); + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 查询所有商品配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_product_config ORDER BY product_type ASC, is_active DESC, product_id ASC") + List selectAllConfigsForAdmin(); + + /** + * 根据商品类型查询所有配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} ORDER BY is_active DESC, product_id ASC") + List selectByProductTypeForAdmin(@Param("productType") String productType); + + /** + * 插入商品价格配置 + */ + @Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, create_time, update_time) " + + "VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, NOW(), NOW())") + int insertProductConfig(PriceProductConfig config); + + /** + * 更新商品价格配置 + */ + @Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " + + "original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") + int updateProductConfig(PriceProductConfig config); + + /** + * 更新商品配置状态 + */ + @Update("UPDATE price_product_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") + int updateProductConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java new file mode 100644 index 0000000..edbc30d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java @@ -0,0 +1,103 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import org.apache.ibatis.annotations.Insert; +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; + +/** + * 阶梯定价配置Mapper + */ +@Mapper +public interface PriceTierConfigMapper extends BaseMapper { + + /** + * 根据商品类型、商品ID和数量查询匹配的阶梯价格 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} " + + "AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " + + "AND is_active = 1 ORDER BY sort_order ASC LIMIT 1") + PriceTierConfig selectByProductTypeAndQuantity(@Param("productType") String productType, + @Param("productId") String productId, + @Param("quantity") Integer quantity); + + /** + * 根据商品类型查询所有阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND is_active = 1 ORDER BY sort_order ASC") + List selectByProductType(String productType); + + /** + * 根据商品类型和商品ID查询所有阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} AND is_active = 1 ORDER BY sort_order ASC") + List selectByProductTypeAndId(@Param("productType") String productType, + @Param("productId") String productId); + + + /** + * 查询所有启用的阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE is_active = 1 ORDER BY product_type ASC, sort_order ASC") + List selectAllActiveConfigs(); + + /** + * 检查是否存在default阶梯配置(包含禁用的) + */ + @Select("SELECT COUNT(*) FROM price_tier_config WHERE product_type = #{productType} AND product_id = 'default'") + int countDefaultTierConfigsByProductType(@Param("productType") String productType); + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 查询所有阶梯配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_tier_config ORDER BY product_type ASC, is_active DESC, sort_order ASC") + List selectAllConfigsForAdmin(); + + /** + * 根据商品类型查询所有阶梯配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "ORDER BY is_active DESC, sort_order ASC") + List selectByProductTypeForAdmin(@Param("productType") String productType); + + /** + * 根据商品类型和商品ID查询所有阶梯配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} ORDER BY is_active DESC, sort_order ASC") + List selectByProductTypeAndIdForAdmin(@Param("productType") String productType, + @Param("productId") String productId); + + /** + * 插入阶梯定价配置 + */ + @Insert("INSERT INTO price_tier_config (product_type, product_id, scenic_id, min_quantity, max_quantity, price, " + + "original_price, unit, sort_order, is_active, create_time, update_time) VALUES " + + "(#{productType}, #{productId}, #{scenicId}, #{minQuantity}, #{maxQuantity}, #{price}, " + + "#{originalPrice}, #{unit}, #{sortOrder}, #{isActive}, NOW(), NOW())") + int insertTierConfig(PriceTierConfig config); + + /** + * 更新阶梯定价配置 + */ + @Update("UPDATE price_tier_config SET product_id = #{productId}, scenic_id = #{scenicId}, min_quantity = #{minQuantity}, " + + "max_quantity = #{maxQuantity}, price = #{price}, original_price = #{originalPrice}, unit = #{unit}, sort_order = #{sortOrder}, " + + "is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") + int updateTierConfig(PriceTierConfig config); + + /** + * 更新阶梯配置状态 + */ + @Update("UPDATE price_tier_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") + int updateTierConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherBatchConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherBatchConfigMapper.java new file mode 100644 index 0000000..566f65d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherBatchConfigMapper.java @@ -0,0 +1,47 @@ +package com.ycwl.basic.pricing.mapper; + +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 java.util.List; + +/** + * 券码批次配置Mapper + */ +@Mapper +public interface PriceVoucherBatchConfigMapper extends BaseMapper { + + /** + * 根据景区ID和推客ID查询有效的批次列表 + * @param scenicId 景区ID + * @param brokerId 推客ID + * @return 批次列表 + */ + List selectActiveBatchesByScenicAndBroker(@Param("scenicId") Long scenicId, + @Param("brokerId") Long brokerId); + + /** + * 更新批次的已领取数量 + * @param batchId 批次ID + * @param increment 增量(可为负数) + * @return 影响行数 + */ + int updateClaimedCount(@Param("batchId") Long batchId, @Param("increment") Integer increment); + + /** + * 更新批次的已使用数量 + * @param batchId 批次ID + * @param increment 增量(可为负数) + * @return 影响行数 + */ + int updateUsedCount(@Param("batchId") Long batchId, @Param("increment") Integer increment); + + /** + * 获取批次统计信息 + * @param batchId 批次ID + * @return 统计信息 + */ + PriceVoucherBatchConfig selectBatchStats(@Param("batchId") Long batchId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java new file mode 100644 index 0000000..d7b0048 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java @@ -0,0 +1,101 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 券码Mapper + */ +@Mapper +public interface PriceVoucherCodeMapper extends BaseMapper { + + /** + * 根据券码查询券码信息 + * @param code 券码 + * @return 券码信息 + */ + PriceVoucherCode selectByCode(@Param("code") String code); + + /** + * 根据faceId和scenicId统计已领取的券码数量 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 数量 + */ + Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); + + /** + * 查询用户在指定景区的可用券码列表 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 券码列表 + */ + List selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId, + @Param("scenicId") Long scenicId); + + /** + * 根据批次ID获取可领取的券码(未领取状态) + * @param batchId 批次ID + * @param limit 限制数量 + * @return 券码列表 + */ + List selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId, + @Param("limit") Integer limit); + + /** + * 领取券码(更新状态为已领取) + * @param id 券码ID + * @param faceId 用户faceId + * @param claimedTime 领取时间 + * @return 影响行数 + */ + int claimVoucher(@Param("id") Long id, + @Param("faceId") Long faceId, + @Param("claimedTime") LocalDateTime claimedTime); + + /** + * 使用券码(更新状态为已使用) + * @param code 券码 + * @param usedTime 使用时间 + * @param remark 使用备注 + * @return 影响行数 + */ + int useVoucher(@Param("code") String code, + @Param("usedTime") LocalDateTime usedTime, + @Param("remark") String remark); + + /** + * 根据批次ID查询券码列表(支持分页) + * @param batchId 批次ID + * @return 券码列表 + */ + List selectByBatchId(@Param("batchId") Long batchId); + + /** + * 查询用户的券码列表 + * @param faceId 用户faceId + * @param scenicId 景区ID(可选) + * @return 券码列表 + */ + List selectUserVouchers(@Param("faceId") Long faceId, + @Param("scenicId") Long scenicId); + + /** + * 根据批次ID查询第一个可用的券码 + * @param batchId 批次ID + * @return 可用券码 + */ + PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId); + + /** + * 随机获取一个未被打印过的可用券码 + * @param scenicId 景区ID + * @return 可用券码 + */ + 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 new file mode 100644 index 0000000..61a6105 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java @@ -0,0 +1,40 @@ +package com.ycwl.basic.pricing.mapper; + +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; + +/** + * 优惠券打印记录Mapper + */ +@Mapper +public interface VoucherPrintRecordMapper extends BaseMapper { + + /** + * 根据faceId、brokerId、scenicId查询已存在的打印记录 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 打印记录 + */ + VoucherPrintRecord selectByFaceBrokerScenic(@Param("faceId") Long faceId, + @Param("scenicId") Long scenicId); + + /** + * 根据券码ID查询是否已被打印 + * @param voucherCodeId 券码ID + * @return 打印记录 + */ + VoucherPrintRecord selectByVoucherCodeId(@Param("voucherCodeId") Long voucherCodeId); + + /** + * 更新打印状态 + * @param id 记录ID + * @param printStatus 打印状态 + * @param errorMessage 错误信息(可为null) + * @return 影响行数 + */ + 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/java/com/ycwl/basic/pricing/service/ICouponManagementService.java b/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java new file mode 100644 index 0000000..71013cf --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java @@ -0,0 +1,112 @@ +package com.ycwl.basic.pricing.service; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; + +import java.util.List; +import java.util.Map; + +/** + * 优惠券管理服务接口(管理端) + */ +public interface ICouponManagementService { + + // ==================== 优惠券配置管理 ==================== + + /** + * 创建优惠券配置 + */ + Long createCouponConfig(PriceCouponConfig config); + + /** + * 更新优惠券配置 + */ + boolean updateCouponConfig(PriceCouponConfig config); + + /** + * 删除优惠券配置 + */ + boolean deleteCouponConfig(Long id); + + /** + * 启用/禁用优惠券配置 + */ + boolean updateCouponConfigStatus(Long id, Boolean isActive); + + /** + * 查询所有优惠券配置(包含禁用的) + */ + List getAllCouponConfigs(); + + /** + * 分页查询优惠券配置 + */ + PageInfo getCouponConfigsPage(Integer pageNum, Integer pageSize, + Boolean isActive, String couponName, String scenicId); + + /** + * 根据状态查询优惠券配置 + */ + List getCouponConfigsByStatus(Boolean isActive); + + /** + * 根据ID查询优惠券配置 + */ + PriceCouponConfig getCouponConfigById(Long id); + + // ==================== 优惠券领取记录查询 ==================== + + /** + * 查询所有优惠券领取记录 + */ + List getAllClaimRecords(); + + /** + * 分页查询优惠券领取记录 + */ + PageInfo getClaimRecordsPage(Integer pageNum, Integer pageSize, + Long userId, Long couponId, CouponStatus status, + String startTime, String endTime, String scenicId); + + /** + * 根据用户ID查询优惠券领取记录 + */ + List getClaimRecordsByUserId(Long userId); + + /** + * 根据优惠券ID查询领取记录 + */ + List getClaimRecordsByCouponId(Long couponId); + + /** + * 根据状态查询领取记录 + */ + List getClaimRecordsByStatus(CouponStatus status); + + /** + * 查询优惠券使用统计 + */ + Map getCouponUsageStats(Long couponId); + + /** + * 查询优惠券配置详细统计 + */ + Map getCouponDetailStats(Long couponId); + + /** + * 查询时间范围内的统计数据 + */ + Map getPeriodStats(String startDate, String endDate, String scenicId); + + /** + * 查询所有优惠券的使用统计概览 + */ + List> getAllCouponUsageOverview(); + + /** + * 查询景区优惠券统计 + */ + Map getScenicCouponStats(String scenicId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java b/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java new file mode 100644 index 0000000..3acd166 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java @@ -0,0 +1,62 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.CouponInfo; +import com.ycwl.basic.pricing.dto.CouponUseRequest; +import com.ycwl.basic.pricing.dto.CouponUseResult; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠券服务接口 + */ +public interface ICouponService { + + /** + * 自动选择最优优惠券 + * + * @param userId 用户ID + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 最优优惠券信息,如果没有可用优惠券则返回null + */ + CouponInfo selectBestCoupon(Long userId, List products, BigDecimal totalAmount); + + /** + * 计算优惠券优惠金额 + * + * @param coupon 优惠券配置 + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 优惠金额 + */ + BigDecimal calculateCouponDiscount(PriceCouponConfig coupon, List products, BigDecimal totalAmount); + + /** + * 验证优惠券是否可用 + * + * @param coupon 优惠券配置 + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 是否可用 + */ + boolean isCouponApplicable(PriceCouponConfig coupon, List products, BigDecimal totalAmount); + + /** + * 使用优惠券 + * + * @param request 优惠券使用请求 + * @return 使用结果 + */ + CouponUseResult useCoupon(CouponUseRequest request); + + /** + * 查询用户可用优惠券 + * + * @param userId 用户ID + * @return 可用优惠券列表 + */ + List getUserAvailableCoupons(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IDiscountDetectionService.java b/src/main/java/com/ycwl/basic/pricing/service/IDiscountDetectionService.java new file mode 100644 index 0000000..549d21b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IDiscountDetectionService.java @@ -0,0 +1,47 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.DiscountCombinationResult; +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.DiscountInfo; + +import java.util.List; + +/** + * 优惠检测服务接口 + * 负责协调所有优惠提供者,计算最优优惠组合 + */ +public interface IDiscountDetectionService { + + /** + * 检测所有可用的优惠 + * @param context 检测上下文 + * @return 所有可用的优惠列表 + */ + List detectAllAvailableDiscounts(DiscountDetectionContext context); + + /** + * 计算最优优惠组合 + * @param context 检测上下文 + * @return 最优优惠组合结果 + */ + DiscountCombinationResult calculateOptimalCombination(DiscountDetectionContext context); + + /** + * 预览优惠组合(不实际应用) + * @param context 检测上下文 + * @return 预览结果 + */ + DiscountCombinationResult previewOptimalCombination(DiscountDetectionContext context); + + /** + * 注册优惠提供者 + * @param provider 优惠提供者 + */ + void registerProvider(IDiscountProvider provider); + + /** + * 获取所有已注册的优惠提供者 + * @return 优惠提供者列表 + */ + List getAllProviders(); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java new file mode 100644 index 0000000..4d2a562 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java @@ -0,0 +1,61 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.DiscountInfo; +import com.ycwl.basic.pricing.dto.DiscountResult; + +import java.util.List; + +/** + * 优惠提供者接口 + * 所有优惠类型(coupon、voucher、促销活动等)都需要实现此接口 + */ +public interface IDiscountProvider { + + /** + * 获取提供者类型 + * @return 提供者类型标识,如 "COUPON", "VOUCHER", "FLASH_SALE" 等 + */ + String getProviderType(); + + /** + * 获取优先级 + * @return 优先级,数字越大优先级越高 + */ + int getPriority(); + + /** + * 检测可用的优惠 + * @param context 优惠检测上下文 + * @return 可用的优惠列表 + */ + List detectAvailableDiscounts(DiscountDetectionContext context); + + /** + * 应用优惠 + * @param discountInfo 要应用的优惠信息 + * @param context 优惠检测上下文 + * @return 优惠应用结果 + */ + DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context); + + /** + * 验证优惠是否可以应用 + * @param discountInfo 优惠信息 + * @param context 优惠检测上下文 + * @return 是否可以应用 + */ + default boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) { + return true; + } + + /** + * 获取优惠的最大可能折扣金额(用于排序) + * @param discountInfo 优惠信息 + * @param context 优惠检测上下文 + * @return 最大可能折扣金额 + */ + default java.math.BigDecimal getMaxPossibleDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) { + return discountInfo.getDiscountAmount() != null ? discountInfo.getDiscountAmount() : java.math.BigDecimal.ZERO; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java new file mode 100644 index 0000000..7ff94e7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java @@ -0,0 +1,52 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 一口价套餐服务接口 + */ +public interface IPriceBundleService { + + /** + * 检查商品是否适用一口价 + * + * @param products 商品列表 + * @return 是否适用 + */ + boolean isBundleApplicable(List products); + + /** + * 获取一口价价格 + * + * @param products 商品列表 + * @return 一口价价格,如果不适用则返回null + */ + BigDecimal getBundlePrice(List products); + + /** + * 获取所有启用的一口价配置 + * + * @return 一口价配置列表 + */ + List getActiveBundles(); + + /** + * 获取所有一口价配置(仅启用的) + * + * @return 一口价配置列表 + */ + List getAllBundles(); + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 获取所有一口价配置(包含禁用的)- 管理端使用 + * + * @return 一口价配置列表 + */ + List getAllBundlesForAdmin(); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java b/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java new file mode 100644 index 0000000..bffa743 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java @@ -0,0 +1,18 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.PriceCalculationRequest; +import com.ycwl.basic.pricing.dto.PriceCalculationResult; + +/** + * 价格计算服务接口 + */ +public interface IPriceCalculationService { + + /** + * 计算商品价格(支持自动优惠券应用) + * + * @param request 价格计算请求 + * @return 价格计算结果 + */ + PriceCalculationResult calculatePrice(PriceCalculationRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java new file mode 100644 index 0000000..a37de38 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java @@ -0,0 +1,93 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.entity.*; + +/** + * 价格管理服务接口(用于配置管理,手动处理时间字段) + */ +public interface IPricingManagementService { + + /** + * 创建商品价格配置 + */ + Long createProductConfig(PriceProductConfig config); + + /** + * 更新商品价格配置 + */ + boolean updateProductConfig(PriceProductConfig config); + + /** + * 创建阶梯定价配置 + */ + Long createTierConfig(PriceTierConfig config); + + /** + * 更新阶梯定价配置 + */ + boolean updateTierConfig(PriceTierConfig config); + + /** + * 创建优惠券配置 + */ + Long createCouponConfig(PriceCouponConfig config); + + /** + * 更新优惠券配置 + */ + boolean updateCouponConfig(PriceCouponConfig config); + + /** + * 创建优惠券领用记录 + */ + Long createCouponClaimRecord(PriceCouponClaimRecord record); + + /** + * 更新优惠券领用记录 + */ + boolean updateCouponClaimRecord(PriceCouponClaimRecord record); + + /** + * 创建一口价配置 + */ + Long createBundleConfig(PriceBundleConfig config); + + /** + * 更新一口价配置 + */ + boolean updateBundleConfig(PriceBundleConfig config); + + // ==================== 状态管理 ==================== + + /** + * 更新商品配置状态 + */ + boolean updateProductConfigStatus(Long id, Boolean isActive); + + /** + * 更新阶梯配置状态 + */ + boolean updateTierConfigStatus(Long id, Boolean isActive); + + /** + * 更新一口价配置状态 + */ + boolean updateBundleConfigStatus(Long id, Boolean isActive); + + // ==================== 删除操作 ==================== + + /** + * 删除商品配置 + */ + boolean deleteProductConfig(Long id); + + /** + * 删除阶梯配置 + */ + boolean deleteTierConfig(Long id); + + /** + * 删除一口价配置 + */ + boolean deleteBundleConfig(Long id); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java new file mode 100644 index 0000000..c1d27cb --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java @@ -0,0 +1,110 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; + +import java.util.List; + +/** + * 商品配置管理服务接口 + */ +public interface IProductConfigService { + + /** + * 根据商品类型获取基础配置(兼容旧接口) + * + * @param productType 商品类型 + * @return 商品配置列表 + */ + List getProductConfig(String productType); + + /** + * 根据商品类型和商品ID获取精确配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @return 商品配置 + */ + PriceProductConfig getProductConfig(String productType, String productId); + + /** + * 根据商品类型、商品ID和数量获取阶梯价格配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @param quantity 数量 + * @return 阶梯价格配置 + */ + PriceTierConfig getTierConfig(String productType, String productId, Integer quantity); + + /** + * 获取所有启用的商品配置 + * + * @return 商品配置列表 + */ + List getActiveProductConfigs(); + + /** + * 获取所有商品配置(仅启用的) + * + * @return 商品配置列表 + */ + List getAllProductConfigs(); + + /** + * 根据商品类型获取所有阶梯配置 + * + * @param productType 商品类型 + * @return 阶梯配置列表 + */ + List getTierConfigs(String productType); + + /** + * 根据商品类型和商品ID获取所有阶梯配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @return 阶梯配置列表 + */ + List getTierConfigs(String productType, String productId); + + /** + * 获取所有启用的阶梯配置 + * + * @return 阶梯配置列表 + */ + List getAllTierConfigs(); + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 获取所有商品配置(包含禁用的)- 管理端使用 + * + * @return 商品配置列表 + */ + List getAllProductConfigsForAdmin(); + + /** + * 获取所有阶梯配置(包含禁用的)- 管理端使用 + * + * @return 阶梯配置列表 + */ + List getAllTierConfigsForAdmin(); + + /** + * 根据商品类型获取所有阶梯配置(包含禁用的)- 管理端使用 + * + * @param productType 商品类型 + * @return 阶梯配置列表 + */ + List getTierConfigsForAdmin(String productType); + + /** + * 根据商品类型和商品ID获取所有阶梯配置(包含禁用的)- 管理端使用 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @return 阶梯配置列表 + */ + List getTierConfigsForAdmin(String productType, String productId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java new file mode 100644 index 0000000..ef0f4aa --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.VoucherInfo; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 券码服务接口 + */ +public interface IVoucherService { + + /** + * 验证并获取券码信息 + * @param voucherCode 券码 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 券码信息(如果有效) + */ + VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId); + + /** + * 获取用户在指定景区的可用券码列表 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 可用券码列表 + */ + List getAvailableVouchers(Long faceId, Long scenicId); + + /** + * 标记券码为已使用 + * @param voucherCode 券码 + * @param remark 使用备注 + */ + void markVoucherAsUsed(String voucherCode, String remark); + + /** + * 检查用户是否可以在指定景区领取券码 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 是否可以领取 + */ + boolean canClaimVoucher(Long faceId, Long scenicId); + + /** + * 获取该faceId在scenicId下的券码详情列表 + * @param faceId 用户面部ID + * @param scenicId 景区ID + * @return 券码详情列表,包含所有状态的券码(已领取未使用、已使用等),如果没有券码则返回空列表 + */ + List getVoucherDetails(Long faceId, Long scenicId); + + /** + * 计算券码优惠金额 + * @param voucherInfo 券码信息 + * @param context 检测上下文 + * @return 优惠金额 + */ + BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context); + + /** + * 获取最优的券码(如果用户有多个可用券码) + * @param faceId 用户faceId + * @param scenicId 景区ID + * @param context 检测上下文 + * @return 最优券码信息 + */ + VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java new file mode 100644 index 0000000..f54d9be --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.pricing.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; + +public interface VoucherBatchService { + + Long createBatch(VoucherBatchCreateReq req); + + Page queryBatchList(VoucherBatchQueryReq req); + + VoucherBatchResp getBatchDetail(Long id); + + VoucherBatchStatsResp getBatchStats(Long id); + + void updateBatchStatus(Long id, Integer status); + + void updateBatchClaimedCount(Long batchId); + + void updateBatchUsedCount(Long batchId); + + PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/VoucherCodeService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherCodeService.java new file mode 100644 index 0000000..27854f5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherCodeService.java @@ -0,0 +1,23 @@ +package com.ycwl.basic.pricing.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; +import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; + +import java.util.List; + +public interface VoucherCodeService { + + void generateVoucherCodes(Long batchId, Long scenicId, Integer count); + + VoucherCodeResp claimVoucher(VoucherClaimReq req); + + Page queryCodeList(VoucherCodeQueryReq req); + + List getMyVoucherCodes(Long faceId); + + void markCodeAsUsed(Long codeId, String remark); + + boolean canClaimVoucher(Long faceId, Long scenicId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java new file mode 100644 index 0000000..a36cdad --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java @@ -0,0 +1,19 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; +import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; + +/** + * 优惠券打印服务 + */ +public interface VoucherPrintService { + + /** + * 打印小票 + * @param request 打印请求 + * @return 打印响应 + */ + VoucherPrintResp printVoucherTicket(VoucherPrintReq request); + + VoucherPrintResp queryPrintedVoucher(Long faceId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java new file mode 100644 index 0000000..1e88cf8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java @@ -0,0 +1,106 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.service.ICouponService; +import com.ycwl.basic.pricing.service.IDiscountProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 优惠券折扣提供者 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CouponDiscountProvider implements IDiscountProvider { + + private final ICouponService couponService; + + @Override + public String getProviderType() { + return "COUPON"; + } + + @Override + public int getPriority() { + return 80; // 优惠券优先级为80,低于券码的100 + } + + @Override + public List detectAvailableDiscounts(DiscountDetectionContext context) { + List discounts = new ArrayList<>(); + + if (!Boolean.TRUE.equals(context.getAutoUseCoupon()) || context.getUserId() == null) { + return discounts; + } + + try { + CouponInfo bestCoupon = couponService.selectBestCoupon( + context.getUserId(), + context.getProducts(), + context.getCurrentAmount() + ); + + if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + DiscountInfo discountInfo = new DiscountInfo(); + discountInfo.setDiscountId(bestCoupon.getCouponId()); + discountInfo.setDiscountType("COUPON"); + discountInfo.setDiscountName(bestCoupon.getCouponName()); + discountInfo.setDiscountDescription("优惠券减免"); + discountInfo.setDiscountAmount(bestCoupon.getActualDiscountAmount()); + discountInfo.setProviderType(getProviderType()); + discountInfo.setPriority(getPriority()); + discountInfo.setStackable(true); // 优惠券可与券码叠加 + discountInfo.setCouponId(bestCoupon.getCouponId()); + + discounts.add(discountInfo); + } + } catch (Exception e) { + log.warn("检测优惠券时发生异常", e); + } + + return discounts; + } + + @Override + public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) { + DiscountResult result = new DiscountResult(); + result.setDiscountInfo(discountInfo); + + try { + // 应用优惠券逻辑 + BigDecimal actualDiscount = discountInfo.getDiscountAmount(); + BigDecimal finalAmount = context.getCurrentAmount().subtract(actualDiscount); + + if (finalAmount.compareTo(BigDecimal.ZERO) < 0) { + finalAmount = BigDecimal.ZERO; + actualDiscount = context.getCurrentAmount(); + } + + result.setActualDiscountAmount(actualDiscount); + result.setFinalAmount(finalAmount); + result.setSuccess(true); + + log.info("成功应用优惠券: {}, 优惠金额: {}", discountInfo.getDiscountName(), actualDiscount); + + } catch (Exception e) { + log.error("应用优惠券失败: " + discountInfo.getDiscountName(), e); + result.setSuccess(false); + result.setFailureReason("优惠券应用失败: " + e.getMessage()); + } + + return result; + } + + @Override + public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) { + return "COUPON".equals(discountInfo.getDiscountType()) && + Boolean.TRUE.equals(context.getAutoUseCoupon()) && + context.getUserId() != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java new file mode 100644 index 0000000..4342fdf --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java @@ -0,0 +1,253 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; +import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper; +import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper; +import com.ycwl.basic.pricing.service.ICouponManagementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 优惠券管理服务实现(管理端) + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CouponManagementServiceImpl implements ICouponManagementService { + + private final PriceCouponConfigMapper couponConfigMapper; + private final PriceCouponClaimRecordMapper claimRecordMapper; + + // ==================== 优惠券配置管理 ==================== + + @Override + @Transactional + public Long createCouponConfig(PriceCouponConfig config) { + log.info("创建优惠券配置: {}", config.getCouponName()); + + // 设置默认值 + if (config.getUsedQuantity() == null) { + config.setUsedQuantity(0); + } + if (config.getIsActive() == null) { + config.setIsActive(true); + } + + int result = couponConfigMapper.insertCoupon(config); + if (result > 0) { + log.info("优惠券配置创建成功,ID: {}", config.getId()); + return config.getId(); + } else { + log.error("优惠券配置创建失败"); + return null; + } + } + + @Override + @Transactional + public boolean updateCouponConfig(PriceCouponConfig config) { + log.info("更新优惠券配置,ID: {}", config.getId()); + + PriceCouponConfig existing = couponConfigMapper.selectById(config.getId()); + if (existing == null) { + log.error("优惠券配置不存在,ID: {}", config.getId()); + return false; + } + + int result = couponConfigMapper.updateCoupon(config); + if (result > 0) { + log.info("优惠券配置更新成功,ID: {}", config.getId()); + return true; + } else { + log.error("优惠券配置更新失败,ID: {}", config.getId()); + return false; + } + } + + @Override + @Transactional + public boolean deleteCouponConfig(Long id) { + log.info("删除优惠券配置,ID: {}", id); + + PriceCouponConfig existing = couponConfigMapper.selectById(id); + if (existing == null) { + log.error("优惠券配置不存在,ID: {}", id); + return false; + } + + int result = couponConfigMapper.deleteCoupon(id); + if (result > 0) { + log.info("优惠券配置删除成功,ID: {}", id); + return true; + } else { + log.error("优惠券配置删除失败,ID: {}", id); + return false; + } + } + + @Override + @Transactional + public boolean updateCouponConfigStatus(Long id, Boolean isActive) { + log.info("更新优惠券配置状态,ID: {}, 状态: {}", id, isActive); + + PriceCouponConfig existing = couponConfigMapper.selectById(id); + if (existing == null) { + log.error("优惠券配置不存在,ID: {}", id); + return false; + } + + int result = couponConfigMapper.updateCouponStatus(id, isActive); + if (result > 0) { + log.info("优惠券配置状态更新成功,ID: {}", id); + return true; + } else { + log.error("优惠券配置状态更新失败,ID: {}", id); + return false; + } + } + + @Override + public List getAllCouponConfigs() { + log.info("查询所有优惠券配置"); + return couponConfigMapper.selectAllForAdmin(); + } + + @Override + public PageInfo getCouponConfigsPage(Integer pageNum, Integer pageSize, + Boolean isActive, String couponName, String scenicId) { + log.info("分页查询优惠券配置,页码: {}, 页大小: {}, 状态: {}, 名称: {}, 景区ID: {}", + pageNum, pageSize, isActive, couponName, scenicId); + + PageHelper.startPage(pageNum, pageSize); + List configs = couponConfigMapper.selectByConditionsForAdmin(isActive, couponName, scenicId); + return new PageInfo<>(configs); + } + + @Override + public List getCouponConfigsByStatus(Boolean isActive) { + log.info("根据状态查询优惠券配置,状态: {}", isActive); + return couponConfigMapper.selectByStatusForAdmin(isActive); + } + + @Override + public PriceCouponConfig getCouponConfigById(Long id) { + log.info("根据ID查询优惠券配置,ID: {}", id); + return couponConfigMapper.selectById(id); + } + + // ==================== 优惠券领取记录查询 ==================== + + @Override + public List getAllClaimRecords() { + log.info("查询所有优惠券领取记录"); + return claimRecordMapper.selectAllForAdmin(); + } + + @Override + public PageInfo getClaimRecordsPage(Integer pageNum, Integer pageSize, + Long userId, Long couponId, CouponStatus status, + String startTime, String endTime, String scenicId) { + log.info("分页查询优惠券领取记录,页码: {}, 页大小: {}, 用户ID: {}, 优惠券ID: {}, 状态: {}, 开始时间: {}, 结束时间: {}, 景区ID: {}", + pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId); + + PageHelper.startPage(pageNum, pageSize); + List records = claimRecordMapper.selectByConditionsForAdmin(userId, couponId, status, startTime, endTime, scenicId); + return new PageInfo<>(records); + } + + @Override + public List getClaimRecordsByUserId(Long userId) { + log.info("根据用户ID查询优惠券领取记录,用户ID: {}", userId); + return claimRecordMapper.selectByUserIdForAdmin(userId); + } + + @Override + public List getClaimRecordsByCouponId(Long couponId) { + log.info("根据优惠券ID查询领取记录,优惠券ID: {}", couponId); + return claimRecordMapper.selectByCouponIdForAdmin(couponId); + } + + @Override + public List getClaimRecordsByStatus(CouponStatus status) { + log.info("根据状态查询领取记录,状态: {}", status); + return claimRecordMapper.selectByStatusForAdmin(status); + } + + @Override + public Map getCouponUsageStats(Long couponId) { + log.info("查询优惠券使用统计,优惠券ID: {}", couponId); + return claimRecordMapper.selectCouponUsageStats(couponId); + } + + @Override + public Map getCouponDetailStats(Long couponId) { + log.info("查询优惠券详细统计,优惠券ID: {}", couponId); + return claimRecordMapper.selectCouponDetailStats(couponId); + } + + @Override + public Map getPeriodStats(String startDate, String endDate, String scenicId) { + log.info("查询时间范围统计,开始日期: {}, 结束日期: {}, 景区ID: {}", startDate, endDate, scenicId); + return claimRecordMapper.selectPeriodStats(startDate, endDate, scenicId); + } + + @Override + public List> getAllCouponUsageOverview() { + log.info("查询所有优惠券使用统计概览"); + + List allCoupons = couponConfigMapper.selectAllForAdmin(); + List> overview = new ArrayList<>(); + + for (PriceCouponConfig coupon : allCoupons) { + Map stats = new HashMap<>(); + stats.put("couponId", coupon.getId()); + stats.put("couponName", coupon.getCouponName()); + stats.put("couponType", coupon.getCouponType()); + stats.put("totalQuantity", coupon.getTotalQuantity()); + stats.put("usedQuantity", coupon.getUsedQuantity()); + stats.put("remainingQuantity", coupon.getTotalQuantity() - coupon.getUsedQuantity()); + stats.put("isActive", coupon.getIsActive()); + stats.put("validFrom", coupon.getValidFrom()); + stats.put("validUntil", coupon.getValidUntil()); + + // 获取详细统计 + Map usageStats = claimRecordMapper.selectCouponUsageStats(coupon.getId()); + stats.putAll(usageStats); + + overview.add(stats); + } + + return overview; + } + + @Override + public Map getScenicCouponStats(String scenicId) { + log.info("查询景区优惠券统计,景区ID: {}", scenicId); + + // 获取景区优惠券配置统计 + Map configStats = couponConfigMapper.selectScenicCouponConfigStats(scenicId); + + // 获取景区优惠券使用统计 + Map usageStats = claimRecordMapper.selectScenicCouponUsageStats(scenicId); + + // 合并统计结果 + Map result = new HashMap<>(); + result.put("scenic_id", scenicId); + result.putAll(configStats); + result.putAll(usageStats); + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java new file mode 100644 index 0000000..82bbc13 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java @@ -0,0 +1,199 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; +import com.ycwl.basic.pricing.enums.CouponType; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.exception.CouponInvalidException; +import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper; +import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper; +import com.ycwl.basic.pricing.service.ICouponService; +import lombok.RequiredArgsConstructor; + +import java.util.Date; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 优惠券服务实现 + */ +@Slf4j +@Service("pricingCouponServiceImpl") +@RequiredArgsConstructor +public class CouponServiceImpl implements ICouponService { + + private final PriceCouponConfigMapper couponConfigMapper; + private final PriceCouponClaimRecordMapper couponClaimRecordMapper; + private final ObjectMapper objectMapper; + + @Override + public CouponInfo selectBestCoupon(Long userId, List products, BigDecimal totalAmount) { + List userCoupons = couponClaimRecordMapper.selectUserAvailableCoupons(userId); + if (userCoupons.isEmpty()) { + return null; + } + + CouponInfo bestCoupon = null; + BigDecimal maxDiscount = BigDecimal.ZERO; + + for (PriceCouponClaimRecord record : userCoupons) { + PriceCouponConfig coupon = couponConfigMapper.selectById(record.getCouponId()); + if (coupon == null || !isCouponApplicable(coupon, products, totalAmount)) { + continue; + } + + BigDecimal discount = calculateCouponDiscount(coupon, products, totalAmount); + if (discount.compareTo(maxDiscount) > 0) { + maxDiscount = discount; + bestCoupon = buildCouponInfo(coupon, discount); + } + } + + return bestCoupon; + } + + @Override + public BigDecimal calculateCouponDiscount(PriceCouponConfig coupon, List products, BigDecimal totalAmount) { + if (!isCouponApplicable(coupon, products, totalAmount)) { + return BigDecimal.ZERO; + } + + BigDecimal discount; + if (coupon.getCouponType() == CouponType.PERCENTAGE) { + discount = totalAmount.multiply(coupon.getDiscountValue().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)); + if (coupon.getMaxDiscount() != null && discount.compareTo(coupon.getMaxDiscount()) > 0) { + discount = coupon.getMaxDiscount(); + } + } else { + discount = coupon.getDiscountValue(); + } + + return discount.setScale(2, RoundingMode.HALF_UP); + } + + @Override + public boolean isCouponApplicable(PriceCouponConfig coupon, List products, BigDecimal totalAmount) { + // 1. 检查最小使用金额 + if (totalAmount.compareTo(coupon.getMinAmount()) < 0) { + return false; + } + + // 2. 检查景区限制 + if (coupon.getScenicId() != null && !coupon.getScenicId().isEmpty()) { + boolean hasMatchingScenicProduct = false; + for (ProductItem product : products) { + if (coupon.getScenicId().equals(product.getScenicId())) { + hasMatchingScenicProduct = true; + break; + } + } + if (!hasMatchingScenicProduct) { + log.debug("优惠券景区限制不匹配: 优惠券景区={}, 商品景区={}", + coupon.getScenicId(), + products.stream().map(ProductItem::getScenicId).distinct().toList()); + return false; + } + } + + // 3. 检查商品类型限制 + if (coupon.getApplicableProducts() == null || coupon.getApplicableProducts().isEmpty()) { + return true; + } + + try { + List applicableProductTypes = objectMapper.readValue( + coupon.getApplicableProducts(), new TypeReference>() {}); + + for (ProductItem product : products) { + if (applicableProductTypes.contains(product.getProductType().getCode())) { + return true; + } + } + return false; + } catch (Exception e) { + log.error("解析适用商品类型失败", e); + return false; + } + } + + @Override + @Transactional + public CouponUseResult useCoupon(CouponUseRequest request) { + PriceCouponClaimRecord record = couponClaimRecordMapper.selectUserCouponRecord( + request.getUserId(), request.getCouponId()); + + if (record == null) { + throw new CouponInvalidException("用户未拥有该优惠券"); + } + + if (record.getStatus() != CouponStatus.CLAIMED) { + throw new CouponInvalidException("优惠券状态无效: " + record.getStatus()); + } + + int updateCount = couponConfigMapper.incrementUsedQuantity(request.getCouponId()); + if (updateCount == 0) { + throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限"); + } + + Date useTime = new Date(); + + // 设置使用时间、订单信息和景区信息 + record.setStatus(CouponStatus.USED); + record.setUseTime(useTime); + record.setOrderId(request.getOrderId()); + record.setUpdateTime(new Date()); + + // 如果请求中包含景区ID,记录到使用记录中 + if (request.getScenicId() != null && !request.getScenicId().isEmpty()) { + record.setScenicId(request.getScenicId()); + } + + couponClaimRecordMapper.updateCouponStatus( + record.getId(), CouponStatus.USED, useTime, request.getOrderId(), request.getScenicId()); + + CouponUseResult result = new CouponUseResult(); + result.setCouponId(request.getCouponId()); + result.setUserId(request.getUserId()); + result.setOrderId(request.getOrderId()); + result.setUseTime(useTime); + result.setDiscountAmount(request.getDiscountAmount()); + + return result; + } + + @Override + public List getUserAvailableCoupons(Long userId) { + List records = couponClaimRecordMapper.selectUserAvailableCoupons(userId); + List coupons = new ArrayList<>(); + + for (PriceCouponClaimRecord record : records) { + PriceCouponConfig config = couponConfigMapper.selectById(record.getCouponId()); + if (config != null) { + coupons.add(buildCouponInfo(config, null)); + } + } + + return coupons; + } + + private CouponInfo buildCouponInfo(PriceCouponConfig coupon, BigDecimal actualDiscountAmount) { + CouponInfo info = new CouponInfo(); + info.setCouponId(coupon.getId()); + info.setCouponName(coupon.getCouponName()); + info.setDiscountType(coupon.getCouponType()); + info.setDiscountValue(coupon.getDiscountValue()); + info.setActualDiscountAmount(actualDiscountAmount); + return info; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/DiscountDetectionServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/DiscountDetectionServiceImpl.java new file mode 100644 index 0000000..111c7ef --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/DiscountDetectionServiceImpl.java @@ -0,0 +1,206 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.service.IDiscountDetectionService; +import com.ycwl.basic.pricing.service.IDiscountProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 优惠检测服务实现 + */ +@Slf4j +@Service +public class DiscountDetectionServiceImpl implements IDiscountDetectionService { + + private final List discountProviders = new ArrayList<>(); + + @Autowired + public DiscountDetectionServiceImpl(List providers) { + this.discountProviders.addAll(providers); + // 按优先级排序(优先级高的在前) + this.discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed()); + + log.info("注册了 {} 个优惠提供者: {}", + providers.size(), + providers.stream().map(IDiscountProvider::getProviderType).collect(Collectors.toList())); + } + + @Override + public List detectAllAvailableDiscounts(DiscountDetectionContext context) { + List allDiscounts = new ArrayList<>(); + + for (IDiscountProvider provider : discountProviders) { + try { + List providerDiscounts = provider.detectAvailableDiscounts(context); + if (providerDiscounts != null && !providerDiscounts.isEmpty()) { + allDiscounts.addAll(providerDiscounts); + log.debug("提供者 {} 检测到 {} 个优惠", provider.getProviderType(), providerDiscounts.size()); + } + } catch (Exception e) { + log.error("优惠提供者 {} 检测失败", provider.getProviderType(), e); + } + } + + // 按优先级排序 + allDiscounts.sort(Comparator.comparing(DiscountInfo::getPriority).reversed()); + + return allDiscounts; + } + + @Override + public DiscountCombinationResult calculateOptimalCombination(DiscountDetectionContext context) { + DiscountCombinationResult result = new DiscountCombinationResult(); + result.setOriginalAmount(context.getCurrentAmount()); + + try { + List availableDiscounts = detectAllAvailableDiscounts(context); + result.setAvailableDiscounts(availableDiscounts); + + if (availableDiscounts.isEmpty()) { + result.setFinalAmount(context.getCurrentAmount()); + result.setTotalDiscountAmount(BigDecimal.ZERO); + result.setAppliedDiscounts(new ArrayList<>()); + result.setDiscountDetails(new ArrayList<>()); + result.setSuccess(true); + return result; + } + + List appliedDiscounts = new ArrayList<>(); + List discountDetails = new ArrayList<>(); + BigDecimal currentAmount = context.getCurrentAmount(); + + // 按优先级应用优惠 + for (DiscountInfo discountInfo : availableDiscounts) { + IDiscountProvider provider = findProvider(discountInfo.getProviderType()); + if (provider == null || !provider.canApply(discountInfo, context)) { + continue; + } + + // 更新上下文中的当前金额 + context.setCurrentAmount(currentAmount); + + DiscountResult discountResult = provider.applyDiscount(discountInfo, context); + if (Boolean.TRUE.equals(discountResult.getSuccess())) { + appliedDiscounts.add(discountResult); + + // 创建显示用的优惠详情 + DiscountDetail detail = createDiscountDetail(discountResult); + if (detail != null) { + discountDetails.add(detail); + } + + // 更新当前金额 + currentAmount = discountResult.getFinalAmount(); + + log.info("成功应用优惠: {} - {}, 优惠金额: {}", + discountInfo.getProviderType(), + discountInfo.getDiscountName(), + discountResult.getActualDiscountAmount()); + + // 如果是不可叠加的优惠(如全场免费),则停止应用其他优惠 + if (!Boolean.TRUE.equals(discountInfo.getStackable())) { + log.info("遇到不可叠加优惠,停止应用其他优惠: {}", discountInfo.getDiscountName()); + break; + } + } else { + log.warn("优惠应用失败: {} - {}, 原因: {}", + discountInfo.getProviderType(), + discountInfo.getDiscountName(), + discountResult.getFailureReason()); + } + } + + // 计算总优惠金额 + BigDecimal totalDiscountAmount = appliedDiscounts.stream() + .map(DiscountResult::getActualDiscountAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 按显示顺序排序折扣详情 + discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder)); + + result.setFinalAmount(currentAmount); + result.setTotalDiscountAmount(totalDiscountAmount); + result.setAppliedDiscounts(appliedDiscounts); + result.setDiscountDetails(discountDetails); + result.setSuccess(true); + + } catch (Exception e) { + log.error("计算最优优惠组合失败", e); + result.setSuccess(false); + result.setErrorMessage("优惠计算失败: " + e.getMessage()); + result.setFinalAmount(context.getCurrentAmount()); + result.setTotalDiscountAmount(BigDecimal.ZERO); + } + + return result; + } + + @Override + public DiscountCombinationResult previewOptimalCombination(DiscountDetectionContext context) { + // 预览模式与正常计算相同,但不会实际标记优惠为已使用 + return calculateOptimalCombination(context); + } + + @Override + public void registerProvider(IDiscountProvider provider) { + if (provider != null && !discountProviders.contains(provider)) { + discountProviders.add(provider); + // 重新排序 + discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed()); + log.info("注册新的优惠提供者: {}", provider.getProviderType()); + } + } + + @Override + public List getAllProviders() { + return new ArrayList<>(discountProviders); + } + + /** + * 查找指定类型的优惠提供者 + */ + private IDiscountProvider findProvider(String providerType) { + return discountProviders.stream() + .filter(provider -> providerType.equals(provider.getProviderType())) + .findFirst() + .orElse(null); + } + + /** + * 创建显示用的优惠详情 + */ + private DiscountDetail createDiscountDetail(DiscountResult discountResult) { + DiscountInfo discountInfo = discountResult.getDiscountInfo(); + String providerType = discountInfo.getProviderType(); + + return switch (providerType) { + case "VOUCHER" -> DiscountDetail.createVoucherDiscount( + discountInfo.getVoucherCode(), + discountInfo.getDiscountDescription(), + discountResult.getActualDiscountAmount() + ); + case "COUPON" -> DiscountDetail.createCouponDiscount( + discountInfo.getDiscountName(), + discountResult.getActualDiscountAmount() + ); + default -> { + // 其他类型的优惠,创建通用的折扣详情 + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType(providerType); + detail.setDiscountName(discountInfo.getDiscountName()); + detail.setDiscountAmount(discountResult.getActualDiscountAmount()); + detail.setDescription(discountInfo.getDiscountDescription()); + detail.setSortOrder(10); // 默认排序 + yield detail; + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java new file mode 100644 index 0000000..5101323 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java @@ -0,0 +1,122 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.BundleProductItem; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.mapper.PriceBundleConfigMapper; +import com.ycwl.basic.pricing.service.IPriceBundleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +//import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 一口价套餐服务实现 + */ +@Slf4j +@Service("pricingBundleServiceImpl") +@RequiredArgsConstructor +public class PriceBundleServiceImpl implements IPriceBundleService { + + private final PriceBundleConfigMapper bundleConfigMapper; + private final ObjectMapper objectMapper; + + @Override + public boolean isBundleApplicable(List products) { + List bundles = getActiveBundles(); + if (bundles.isEmpty()) { + return false; + } + + Set productTypes = new HashSet<>(); + for (ProductItem product : products) { + productTypes.add(product.getProductType().getCode()); + } + + for (PriceBundleConfig bundle : bundles) { + if (isProductsMatchBundle(productTypes, bundle)) { + return true; + } + } + + return false; + } + + @Override + public BigDecimal getBundlePrice(List products) { + if (!isBundleApplicable(products)) { + return null; + } + + List bundles = getActiveBundles(); + Set productTypes = new HashSet<>(); + for (ProductItem product : products) { + productTypes.add(product.getProductType().getCode()); + } + + for (PriceBundleConfig bundle : bundles) { + if (isProductsMatchBundle(productTypes, bundle)) { + return bundle.getBundlePrice(); + } + } + + return null; + } + + @Override +// @Cacheable(value = "active-bundles") + public List getActiveBundles() { + return bundleConfigMapper.selectActiveBundles(); + } + + @Override +// @Cacheable(value = "all-bundles") + public List getAllBundles() { + return bundleConfigMapper.selectActiveBundles(); + } + + // ==================== 管理端接口(包含禁用的配置) ==================== + + @Override + public List getAllBundlesForAdmin() { + return bundleConfigMapper.selectAllBundlesForAdmin(); + } + + private boolean isProductsMatchBundle(Set productTypes, PriceBundleConfig bundle) { + try { + // 检查包含的商品 + if (bundle.getIncludedProducts() != null && !bundle.getIncludedProducts().isEmpty()) { + Set requiredProducts = new HashSet<>(); + for (BundleProductItem item : bundle.getIncludedProducts()) { + requiredProducts.add(item.getType()); + } + if (!productTypes.containsAll(requiredProducts)) { + return false; + } + } + + // 检查排除的商品 + if (bundle.getExcludedProducts() != null && !bundle.getExcludedProducts().isEmpty()) { + for (BundleProductItem item : bundle.getExcludedProducts()) { + if (productTypes.contains(item.getType())) { + return false; + } + } + } + + return true; + + } catch (Exception e) { + log.error("解析一口价配置失败: bundleId={}", bundle.getId(), e); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java new file mode 100644 index 0000000..4eaac53 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java @@ -0,0 +1,388 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.exception.PriceCalculationException; +import com.ycwl.basic.pricing.service.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 价格计算服务实现 + */ +@Slf4j +@Service("pricingCalculationServiceImpl") +@RequiredArgsConstructor +public class PriceCalculationServiceImpl implements IPriceCalculationService { + + private final IProductConfigService productConfigService; + private final ICouponService couponService; + private final IPriceBundleService bundleService; + private final IDiscountDetectionService discountDetectionService; + private final IVoucherService voucherService; + + @Override + public PriceCalculationResult calculatePrice(PriceCalculationRequest request) { + if (request.getProducts() == null || request.getProducts().isEmpty()) { + throw new PriceCalculationException("商品列表不能为空"); + } + + // 计算商品价格和原价 + PriceDetails priceDetails = calculateProductsPriceWithOriginal(request.getProducts()); + BigDecimal totalAmount = priceDetails.getTotalAmount(); + BigDecimal originalTotalAmount = priceDetails.getOriginalTotalAmount(); + + List discountDetails = new ArrayList<>(); + + // 添加限时立减折扣(如果原价 > 实际价格) + BigDecimal limitedTimeDiscount = originalTotalAmount.subtract(totalAmount); + if (limitedTimeDiscount.compareTo(BigDecimal.ZERO) > 0) { + discountDetails.add(DiscountDetail.createLimitedTimeDiscount(limitedTimeDiscount)); + } + + // 检查一口价优惠 + BigDecimal bundlePrice = bundleService.getBundlePrice(request.getProducts()); + if (bundlePrice != null && bundlePrice.compareTo(totalAmount) < 0) { + BigDecimal bundleDiscount = totalAmount.subtract(bundlePrice); + discountDetails.add(DiscountDetail.createBundleDiscount(bundleDiscount)); + totalAmount = bundlePrice; + log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount); + } + + // 构建价格计算结果 + PriceCalculationResult result = new PriceCalculationResult(); + result.setOriginalAmount(originalTotalAmount); // 原总价 + result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计 + result.setProductDetails(request.getProducts()); + + // 使用新的优惠检测系统处理所有优惠(券码 + 优惠券) + DiscountCombinationResult discountResult = calculateDiscounts(request, totalAmount); + + if (Boolean.TRUE.equals(discountResult.getSuccess())) { + // 合并所有优惠详情 + List allDiscountDetails = new ArrayList<>(discountDetails); + if (discountResult.getDiscountDetails() != null) { + allDiscountDetails.addAll(discountResult.getDiscountDetails()); + } + + // 重新排序 + allDiscountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder)); + + // 计算总优惠金额(包括限时立减、一口价和其他优惠) + BigDecimal totalDiscountAmount = allDiscountDetails.stream() + .map(DiscountDetail::getDiscountAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 设置结果 + result.setDiscountAmount(totalDiscountAmount); + result.setDiscountDetails(allDiscountDetails); + result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount)); + + // 设置使用的券码和优惠券信息 + setUsedDiscountInfo(result, discountResult, request); + + // 如果是预览模式,设置可用优惠列表 + if (Boolean.TRUE.equals(request.getPreviewOnly())) { + result.setAvailableDiscounts(discountResult.getAvailableDiscounts()); + } + + } else { + log.warn("优惠计算失败: {}", discountResult.getErrorMessage()); + + // 降级处理:仅使用基础优惠(限时立减、一口价) + BigDecimal totalDiscountAmount = discountDetails.stream() + .map(DiscountDetail::getDiscountAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + result.setDiscountAmount(totalDiscountAmount); + result.setDiscountDetails(discountDetails); + result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount)); + } + + // 标记使用的优惠(仅在非预览模式下) + if (!Boolean.TRUE.equals(request.getPreviewOnly())) { + markDiscountsAsUsed(result, request); + } + + return result; + } + + private BigDecimal calculateProductsPrice(List products) { + BigDecimal totalAmount = BigDecimal.ZERO; + + for (ProductItem product : products) { + BigDecimal itemPrice = calculateSingleProductPrice(product); + product.setUnitPrice(itemPrice); + + BigDecimal subtotal = itemPrice.multiply(BigDecimal.valueOf(product.getPurchaseCount())); + product.setSubtotal(subtotal); + + totalAmount = totalAmount.add(subtotal); + } + + return totalAmount.setScale(2, RoundingMode.HALF_UP); + } + + private PriceDetails calculateProductsPriceWithOriginal(List products) { + BigDecimal totalAmount = BigDecimal.ZERO; + BigDecimal originalTotalAmount = BigDecimal.ZERO; + + for (ProductItem product : products) { + // 计算实际价格和原价 + ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product); + + product.setUnitPrice(priceInfo.getActualPrice()); + product.setOriginalPrice(priceInfo.getOriginalPrice()); + + BigDecimal subtotal = priceInfo.getActualPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount())); + BigDecimal originalSubtotal = priceInfo.getOriginalPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount())); + + product.setSubtotal(subtotal); + + totalAmount = totalAmount.add(subtotal); + originalTotalAmount = originalTotalAmount.add(originalSubtotal); + } + + return new PriceDetails( + totalAmount.setScale(2, RoundingMode.HALF_UP), + originalTotalAmount.setScale(2, RoundingMode.HALF_UP) + ); + } + + private BigDecimal calculateSingleProductPrice(ProductItem product) { + ProductType productType = product.getProductType(); + String productId = product.getProductId() != null ? product.getProductId() : "default"; + + // 优先使用基于product_id的阶梯定价 + PriceTierConfig tierConfig = productConfigService.getTierConfig( + productType.getCode(), productId, product.getQuantity()); + + if (tierConfig != null) { + log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}", + productType.getCode(), productId, product.getQuantity(), tierConfig.getPrice()); + return tierConfig.getPrice(); + } + + // 使用基于product_id的基础配置 + try { + PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId); + if (baseConfig != null) { + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); + } else { + return baseConfig.getBasePrice(); + } + } + } catch (Exception e) { + log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置", + productType, productId); + } + + // 兜底:使用default配置 + try { + PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default"); + if (defaultConfig != null) { + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + return defaultConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); + } else { + return defaultConfig.getBasePrice(); + } + } + } catch (Exception e) { + log.warn("未找到default配置: productType={}", productType.getCode()); + } + + // 最后兜底:使用通用配置(向后兼容) + List configs = productConfigService.getProductConfig(productType.getCode()); + if (!configs.isEmpty()) { + PriceProductConfig baseConfig = configs.get(0); // 使用第一个配置作为默认 + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); + } else { + return baseConfig.getBasePrice(); + } + } + + throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); + } + + private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product) { + ProductType productType = product.getProductType(); + String productId = product.getProductId() != null ? product.getProductId() : "default"; + + BigDecimal actualPrice; + BigDecimal originalPrice = null; + + // 优先使用基于product_id的阶梯定价 + PriceTierConfig tierConfig = productConfigService.getTierConfig( + productType.getCode(), productId, product.getQuantity()); + + if (tierConfig != null) { + actualPrice = tierConfig.getPrice(); + originalPrice = tierConfig.getOriginalPrice(); + log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}, originalPrice={}", + productType.getCode(), productId, product.getQuantity(), actualPrice, originalPrice); + } else { + // 使用基于product_id的基础配置 + try { + PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId); + if (baseConfig != null) { + actualPrice = baseConfig.getBasePrice(); + originalPrice = baseConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } else { + throw new PriceCalculationException("无法找到具体商品配置"); + } + } catch (Exception e) { + log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置", + productType, productId); + + // 兜底:使用default配置 + try { + PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default"); + if (defaultConfig != null) { + actualPrice = defaultConfig.getBasePrice(); + originalPrice = defaultConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } else { + throw new PriceCalculationException("无法找到default配置"); + } + } catch (Exception defaultEx) { + log.warn("未找到default配置: productType={}", productType.getCode()); + + // 最后兜底:使用通用配置(向后兼容) + List configs = productConfigService.getProductConfig(productType.getCode()); + if (!configs.isEmpty()) { + PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认 + actualPrice = baseConfig.getBasePrice(); + originalPrice = baseConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } else { + throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); + } + } + } + } + + return new ProductPriceInfo(actualPrice, originalPrice); + } + + /** + * 计算优惠(券码 + 优惠券) + */ + private DiscountCombinationResult calculateDiscounts(PriceCalculationRequest request, BigDecimal currentAmount) { + try { + // 构建优惠检测上下文 + DiscountDetectionContext context = new DiscountDetectionContext(); + context.setUserId(request.getUserId()); + context.setFaceId(request.getFaceId()); + context.setScenicId(request.getScenicId()); + context.setProducts(request.getProducts()); + context.setCurrentAmount(currentAmount); + context.setVoucherCode(request.getVoucherCode()); + context.setAutoUseCoupon(request.getAutoUseCoupon()); + context.setAutoUseVoucher(request.getAutoUseVoucher()); + + // 使用优惠检测服务计算最优组合 + if (Boolean.TRUE.equals(request.getPreviewOnly())) { + return discountDetectionService.previewOptimalCombination(context); + } else { + return discountDetectionService.calculateOptimalCombination(context); + } + + } catch (Exception e) { + log.error("计算优惠时发生异常", e); + + // 返回失败结果 + DiscountCombinationResult failureResult = new DiscountCombinationResult(); + failureResult.setOriginalAmount(currentAmount); + failureResult.setFinalAmount(currentAmount); + failureResult.setTotalDiscountAmount(BigDecimal.ZERO); + failureResult.setSuccess(false); + failureResult.setErrorMessage("优惠计算失败: " + e.getMessage()); + return failureResult; + } + } + + /** + * 设置使用的优惠信息到结果中 + */ + private void setUsedDiscountInfo(PriceCalculationResult result, DiscountCombinationResult discountResult, PriceCalculationRequest request) { + if (discountResult.getAppliedDiscounts() == null) { + return; + } + + for (DiscountResult discountApplied : discountResult.getAppliedDiscounts()) { + DiscountInfo discountInfo = discountApplied.getDiscountInfo(); + + if ("COUPON".equals(discountInfo.getProviderType()) && discountInfo.getCouponId() != null) { + // 构建优惠券信息(这里可能需要重新查询完整信息) + CouponInfo couponInfo = new CouponInfo(); + couponInfo.setCouponId(discountInfo.getCouponId()); + couponInfo.setCouponName(discountInfo.getDiscountName()); + couponInfo.setActualDiscountAmount(discountApplied.getActualDiscountAmount()); + result.setUsedCoupon(couponInfo); + + } else if ("VOUCHER".equals(discountInfo.getProviderType()) && discountInfo.getVoucherCode() != null) { + // 获取券码信息 + VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo( + discountInfo.getVoucherCode(), + request.getFaceId(), + request.getScenicId() + ); + if (voucherInfo != null) { + voucherInfo.setActualDiscountAmount(discountApplied.getActualDiscountAmount()); + result.setUsedVoucher(voucherInfo); + } + } + } + } + + /** + * 标记优惠为已使用(仅在非预览模式下调用) + */ + private void markDiscountsAsUsed(PriceCalculationResult result, PriceCalculationRequest request) { + try { + // 标记券码为已使用 + if (result.getUsedVoucher() != null && result.getUsedVoucher().getVoucherCode() != null) { + String remark = String.format("价格计算使用 - 订单金额: %s", result.getFinalAmount()); + voucherService.markVoucherAsUsed(result.getUsedVoucher().getVoucherCode(), remark); + log.info("已标记券码为使用: {}", result.getUsedVoucher().getVoucherCode()); + } + + // 优惠券的使用标记由原有的CouponService处理 + // 这里不需要额外处理 + + } catch (Exception e) { + log.error("标记优惠使用状态时发生异常", e); + // 不抛出异常,避免影响主流程 + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java new file mode 100644 index 0000000..ec402f4 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java @@ -0,0 +1,168 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.entity.*; +import com.ycwl.basic.pricing.mapper.*; +import com.ycwl.basic.pricing.service.IPricingManagementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +/** + * 价格管理服务实现(用于配置管理,手动处理时间字段) + */ +@Slf4j +@Service("pricingManagementServiceImpl") +@RequiredArgsConstructor +public class PricingManagementServiceImpl implements IPricingManagementService { + + private final PriceProductConfigMapper productConfigMapper; + private final PriceTierConfigMapper tierConfigMapper; + private final PriceCouponConfigMapper couponConfigMapper; + private final PriceCouponClaimRecordMapper couponClaimRecordMapper; + private final PriceBundleConfigMapper bundleConfigMapper; + + @Override + @Transactional + public Long createProductConfig(PriceProductConfig config) { + // 校验:如果是default配置,确保该商品类型只能有一个default配置 + if ("default".equals(config.getProductId())) { + int existingCount = productConfigMapper.countDefaultConfigsByProductType(config.getProductType()); + if (existingCount > 0) { + throw new IllegalArgumentException("商品类型 " + config.getProductType() + " 的default配置已存在,每种商品类型只能有一个default配置"); + } + } + + config.setCreateTime(new Date()); + config.setUpdateTime(new Date()); + productConfigMapper.insertProductConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateProductConfig(PriceProductConfig config) { + config.setUpdateTime(new Date()); + return productConfigMapper.updateProductConfig(config) > 0; + } + + @Override + @Transactional + public Long createTierConfig(PriceTierConfig config) { + // 校验:如果是default配置,检查是否可以创建 + if ("default".equals(config.getProductId())) { + // 对于阶梯配置,可以有多个default配置(不同数量区间),不需要限制 + log.info("创建default阶梯配置: productType={}, quantity range: {}-{}", + config.getProductType(), config.getMinQuantity(), config.getMaxQuantity()); + } + + config.setCreateTime(new Date()); + config.setUpdateTime(new Date()); + tierConfigMapper.insertTierConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateTierConfig(PriceTierConfig config) { + config.setUpdateTime(new Date()); + return tierConfigMapper.updateTierConfig(config) > 0; + } + + @Override + @Transactional + public Long createCouponConfig(PriceCouponConfig config) { + config.setCreateTime(new Date()); + config.setUpdateTime(new Date()); + couponConfigMapper.insertCoupon(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateCouponConfig(PriceCouponConfig config) { + config.setUpdateTime(new Date()); + return couponConfigMapper.updateCoupon(config) > 0; + } + + @Override + @Transactional + public Long createCouponClaimRecord(PriceCouponClaimRecord record) { + record.setClaimTime(new Date()); + record.setCreateTime(new Date()); + record.setUpdateTime(new Date()); + couponClaimRecordMapper.insertClaimRecord(record); + return record.getId(); + } + + @Override + @Transactional + public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) { + record.setUpdateTime(new Date()); + return couponClaimRecordMapper.updateClaimRecord(record) > 0; + } + + @Override + @Transactional + public Long createBundleConfig(PriceBundleConfig config) { + config.setCreateTime(new Date()); + config.setUpdateTime(new Date()); + bundleConfigMapper.insertBundleConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateBundleConfig(PriceBundleConfig config) { + config.setUpdateTime(new Date()); + return bundleConfigMapper.updateBundleConfig(config) > 0; + } + + // ==================== 状态管理 ==================== + + @Override + @Transactional + public boolean updateProductConfigStatus(Long id, Boolean isActive) { + log.info("更新商品配置状态: id={}, isActive={}", id, isActive); + return productConfigMapper.updateProductConfigStatus(id, isActive) > 0; + } + + @Override + @Transactional + public boolean updateTierConfigStatus(Long id, Boolean isActive) { + log.info("更新阶梯配置状态: id={}, isActive={}", id, isActive); + return tierConfigMapper.updateTierConfigStatus(id, isActive) > 0; + } + + @Override + @Transactional + public boolean updateBundleConfigStatus(Long id, Boolean isActive) { + log.info("更新一口价配置状态: id={}, isActive={}", id, isActive); + return bundleConfigMapper.updateBundleConfigStatus(id, isActive) > 0; + } + + // ==================== 删除操作 ==================== + + @Override + @Transactional + public boolean deleteProductConfig(Long id) { + log.info("删除商品配置: id={}", id); + return productConfigMapper.deleteById(id) > 0; + } + + @Override + @Transactional + public boolean deleteTierConfig(Long id) { + log.info("删除阶梯配置: id={}", id); + return tierConfigMapper.deleteById(id) > 0; + } + + @Override + @Transactional + public boolean deleteBundleConfig(Long id) { + log.info("删除一口价配置: id={}", id); + return bundleConfigMapper.deleteById(id) > 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java new file mode 100644 index 0000000..86fb376 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java @@ -0,0 +1,117 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.exception.ProductConfigNotFoundException; +import com.ycwl.basic.pricing.mapper.PriceProductConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceTierConfigMapper; +import com.ycwl.basic.pricing.service.IProductConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +//import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 商品配置管理服务实现 + */ +@Slf4j +@Service("pricingProductConfigServiceImpl") +@RequiredArgsConstructor +public class ProductConfigServiceImpl implements IProductConfigService { + + private final PriceProductConfigMapper productConfigMapper; + private final PriceTierConfigMapper tierConfigMapper; + + @Override +// @Cacheable(value = "product-config", key = "#productType") + public List getProductConfig(String productType) { + return productConfigMapper.selectByProductType(productType); + } + + @Override +// @Cacheable(value = "product-config", key = "#productType + '_' + #productId") + public PriceProductConfig getProductConfig(String productType, String productId) { + PriceProductConfig config = productConfigMapper.selectByProductTypeAndId(productType, productId); + if (config == null) { + throw new ProductConfigNotFoundException("商品配置未找到: " + productType + ", productId: " + productId); + } + return config; + } + + @Override +// @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + #quantity") + public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity) { + PriceTierConfig config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity); + + // 如果没有找到特定商品的阶梯配置,尝试使用default配置 + if (config == null && !"default".equals(productId)) { + log.warn("阶梯定价配置未找到: productType={}, productId={}, quantity={}, 尝试使用default配置", + productType, productId, quantity); + config = tierConfigMapper.selectByProductTypeAndQuantity(productType, "default", quantity); + if (config != null) { + log.debug("使用default阶梯配置: productType={}, quantity={}, price={}", + productType, quantity, config.getPrice()); + } + } + + if (config == null) { + log.warn("阶梯定价配置未找到: productType={}, productId={}, quantity={}", + productType, productId, quantity); + } + return config; + } + + @Override +// @Cacheable(value = "active-product-configs") + public List getActiveProductConfigs() { + return productConfigMapper.selectActiveConfigs(); + } + + @Override +// @Cacheable(value = "all-product-configs") + public List getAllProductConfigs() { + return productConfigMapper.selectActiveConfigs(); + } + + @Override +// @Cacheable(value = "tier-configs", key = "#productType") + public List getTierConfigs(String productType) { + return tierConfigMapper.selectByProductType(productType); + } + + @Override +// @Cacheable(value = "tier-configs", key = "#productType + '_' + #productId") + public List getTierConfigs(String productType, String productId) { + return tierConfigMapper.selectByProductTypeAndId(productType, productId); + } + + @Override +// @Cacheable(value = "all-tier-configs") + public List getAllTierConfigs() { + return tierConfigMapper.selectAllActiveConfigs(); + } + + // ==================== 管理端接口(包含禁用的配置) ==================== + + @Override + public List getAllProductConfigsForAdmin() { + return productConfigMapper.selectAllConfigsForAdmin(); + } + + @Override + public List getAllTierConfigsForAdmin() { + return tierConfigMapper.selectAllConfigsForAdmin(); + } + + @Override + public List getTierConfigsForAdmin(String productType) { + return tierConfigMapper.selectByProductTypeForAdmin(productType); + } + + @Override + public List getTierConfigsForAdmin(String productType, String productId) { + return tierConfigMapper.selectByProductTypeAndIdForAdmin(productType, productId); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java new file mode 100644 index 0000000..805fc5b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java @@ -0,0 +1,196 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.exception.BizException; +import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper; +import com.ycwl.basic.pricing.service.VoucherBatchService; +import com.ycwl.basic.pricing.service.VoucherCodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.Date; + +@Service +public class VoucherBatchServiceImpl implements VoucherBatchService { + @Autowired + private PriceVoucherBatchConfigMapper voucherBatchMapper; + @Autowired + @Lazy + private VoucherCodeService voucherCodeService; + + @Override + @Transactional + public Long createBatch(VoucherBatchCreateReq req) { + if (req.getBatchName() == null || req.getBatchName().trim().isEmpty()) { + throw new BizException(400, "券码批次名称不能为空"); + } + if (req.getScenicId() == null) { + throw new BizException(400, "景区ID不能为空"); + } + if (req.getBrokerId() == null) { + throw new BizException(400, "推客ID不能为空"); + } + if (req.getDiscountType() == null) { + throw new BizException(400, "优惠类型不能为空"); + } + if (req.getTotalCount() == null || req.getTotalCount() < 1) { + throw new BizException(400, "券码数量必须大于0"); + } + + VoucherDiscountType discountType = VoucherDiscountType.getByCode(req.getDiscountType()); + if (discountType == null) { + throw new BizException(400, "无效的优惠类型"); + } + + if (discountType != VoucherDiscountType.FREE_ALL && req.getDiscountValue() == null) { + throw new BizException(400, "优惠金额不能为空"); + } + + PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig(); + BeanUtils.copyProperties(req, batch); + batch.setUsedCount(0); + batch.setClaimedCount(0); + batch.setStatus(1); + batch.setCreateTime(new Date()); + String userIdStr = BaseContextHandler.getUserId(); + if (userIdStr != null) { + batch.setCreateBy(Long.valueOf(userIdStr)); + } + batch.setDeleted(0); + + voucherBatchMapper.insert(batch); + + voucherCodeService.generateVoucherCodes(batch.getId(), req.getScenicId(), req.getTotalCount()); + + return batch.getId(); + } + + @Override + public Page queryBatchList(VoucherBatchQueryReq req) { + Page page = new Page<>(req.getPageNum(), req.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherBatchConfig::getDeleted, 0) + .eq(req.getScenicId() != null, PriceVoucherBatchConfig::getScenicId, req.getScenicId()) + .eq(req.getBrokerId() != null, PriceVoucherBatchConfig::getBrokerId, req.getBrokerId()) + .eq(req.getStatus() != null, PriceVoucherBatchConfig::getStatus, req.getStatus()) + .like(StringUtils.hasText(req.getBatchName()), PriceVoucherBatchConfig::getBatchName, req.getBatchName()) + .orderByDesc(PriceVoucherBatchConfig::getCreateTime); + + Page entityPage = voucherBatchMapper.selectPage(page, wrapper); + + Page respPage = new Page<>(); + BeanUtils.copyProperties(entityPage, respPage); + + respPage.setRecords(entityPage.getRecords().stream().map(this::convertToResp).toList()); + + return respPage; + } + + @Override + public VoucherBatchResp getBatchDetail(Long id) { + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id); + if (batch == null || batch.getDeleted() == 1) { + throw new BizException(404, "券码批次不存在"); + } + + return convertToResp(batch); + } + + @Override + public VoucherBatchStatsResp getBatchStats(Long id) { + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id); + if (batch == null || batch.getDeleted() == 1) { + throw new BizException(404, "券码批次不存在"); + } + + VoucherBatchStatsResp stats = new VoucherBatchStatsResp(); + stats.setBatchId(batch.getId()); + stats.setBatchName(batch.getBatchName()); + stats.setTotalCount(batch.getTotalCount()); + stats.setClaimedCount(batch.getClaimedCount()); + stats.setUsedCount(batch.getUsedCount()); + stats.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount()); + + if (batch.getTotalCount() > 0) { + stats.setClaimedRate((double) batch.getClaimedCount() / batch.getTotalCount() * 100); + stats.setUsedRate((double) batch.getUsedCount() / batch.getTotalCount() * 100); + } else { + stats.setClaimedRate(0.0); + stats.setUsedRate(0.0); + } + + return stats; + } + + @Override + public void updateBatchStatus(Long id, Integer status) { + PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig(); + batch.setId(id); + batch.setStatus(status); + + int updated = voucherBatchMapper.updateById(batch); + if (updated == 0) { + throw new BizException(404, "券码批次不存在"); + } + } + + @Override + public void updateBatchClaimedCount(Long batchId) { + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId); + if (batch != null) { + batch.setClaimedCount(batch.getClaimedCount() + 1); + voucherBatchMapper.updateById(batch); + } + } + + @Override + public void updateBatchUsedCount(Long batchId) { + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId); + if (batch != null) { + batch.setUsedCount(batch.getUsedCount() + 1); + voucherBatchMapper.updateById(batch); + } + } + + @Override + public PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherBatchConfig::getScenicId, scenicId) + .eq(PriceVoucherBatchConfig::getBrokerId, brokerId) + .eq(PriceVoucherBatchConfig::getStatus, 1) + .eq(PriceVoucherBatchConfig::getDeleted, 0) + .apply("claimed_count < total_count") + .orderByDesc(PriceVoucherBatchConfig::getCreateTime); + + return voucherBatchMapper.selectOne(wrapper); + } + + private VoucherBatchResp convertToResp(PriceVoucherBatchConfig batch) { + VoucherBatchResp resp = new VoucherBatchResp(); + BeanUtils.copyProperties(batch, resp); + + VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType()); + if (discountType != null) { + resp.setDiscountTypeName(discountType.getName()); + } + + resp.setStatusName(batch.getStatus() == 1 ? "启用" : "禁用"); + resp.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount()); + + return resp; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java new file mode 100644 index 0000000..b2cabfc --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java @@ -0,0 +1,277 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.exception.BizException; +import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; +import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import com.ycwl.basic.pricing.enums.VoucherCodeStatus; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; +import com.ycwl.basic.pricing.service.VoucherBatchService; +import com.ycwl.basic.pricing.service.VoucherCodeService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@Slf4j +@Service +public class VoucherCodeServiceImpl implements VoucherCodeService { + + // 券码生成相关常量 + private static final String SAFE_CHARS = "23456789ABCDEFGHJKMNPQRTUVWXYZ"; + private static final char[] SAFE_CHARS_ARRAY = SAFE_CHARS.toCharArray(); + private static final int CODE_LENGTH = 6; + private static final int MAX_RETRY = 10; + private static final SecureRandom RANDOM = new SecureRandom(); + + @Autowired + private PriceVoucherCodeMapper voucherCodeMapper; + @Autowired + private PriceVoucherBatchConfigMapper voucherBatchMapper; + @Autowired + @Lazy + private VoucherBatchService voucherBatchService; + + @Override + public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) { + List codes = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + PriceVoucherCode code = new PriceVoucherCode(); + code.setBatchId(batchId); + code.setScenicId(scenicId); + code.setCode(generateVoucherCode()); + code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode()); + code.setCreateTime(new Date()); + code.setDeleted(0); + codes.add(code); + } + + for (PriceVoucherCode code : codes) { + voucherCodeMapper.insert(code); + } + } + + @Override + @Transactional + public VoucherCodeResp claimVoucher(VoucherClaimReq req) { + if (req.getScenicId() == null) { + throw new BizException(400, "景区ID不能为空"); + } + if (req.getBrokerId() == null) { + throw new BizException(400, "推客ID不能为空"); + } + if (req.getFaceId() == null) { + throw new BizException(400, "用户faceId不能为空"); + } + if (!StringUtils.hasText(req.getCode())) { + throw new BizException(400, "券码不能为空"); + } + + // 验证券码是否存在且未被领取 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherCode::getCode, req.getCode()) + .eq(PriceVoucherCode::getScenicId, req.getScenicId()) + .eq(PriceVoucherCode::getDeleted, 0); + + PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper); + if (voucherCode == null) { + throw new BizException(400, "券码不存在或不属于该景区"); + } + + if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) { + throw new BizException(400, "券码已被领取或已使用"); + } + + if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) { + throw new BizException(400, "该用户在此景区已领取过券码"); + } + + // 获取券码所属批次 + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId()); + if (batch == null || batch.getDeleted() == 1) { + throw new BizException(400, "券码批次不存在"); + } + + // 验证批次是否可用于该推客 + if (!Objects.equals(batch.getBrokerId(), req.getBrokerId())) { + throw new BizException(400, "券码不属于该推客"); + } + + // 更新券码状态 + voucherCode.setFaceId(req.getFaceId()); + voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode()); + voucherCode.setClaimedTime(new Date()); + + voucherCodeMapper.updateById(voucherCode); + + voucherBatchService.updateBatchClaimedCount(batch.getId()); + + return convertToResp(voucherCode, batch); + } + + @Override + public Page queryCodeList(VoucherCodeQueryReq req) { + Page page = new Page<>(req.getPageNum(), req.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherCode::getDeleted, 0) + .eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId()) + .eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId()) + .eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId()) + .eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus()) + .like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode()) + .orderByDesc(PriceVoucherCode::getId); + + Page entityPage = voucherCodeMapper.selectPage(page, wrapper); + + Page respPage = new Page<>(); + BeanUtils.copyProperties(entityPage, respPage); + + List respList = new ArrayList<>(); + for (PriceVoucherCode code : entityPage.getRecords()) { + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId()); + respList.add(convertToResp(code, batch)); + } + respPage.setRecords(respList); + + return respPage; + } + + @Override + public List getMyVoucherCodes(Long faceId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherCode::getFaceId, faceId) + .eq(PriceVoucherCode::getDeleted, 0) + .orderByDesc(PriceVoucherCode::getClaimedTime); + + List codes = voucherCodeMapper.selectList(wrapper); + + List respList = new ArrayList<>(); + for (PriceVoucherCode code : codes) { + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId()); + respList.add(convertToResp(code, batch)); + } + + return respList; + } + + @Override + @Transactional + public void markCodeAsUsed(Long codeId, String remark) { + PriceVoucherCode code = voucherCodeMapper.selectById(codeId); + if (code == null || code.getDeleted() == 1) { + throw new BizException(404, "券码不存在"); + } + + if (!Objects.equals(code.getStatus(), VoucherCodeStatus.CLAIMED_UNUSED.getCode())) { + throw new BizException(400, "券码状态异常,无法使用"); + } + + code.setStatus(VoucherCodeStatus.USED.getCode()); + code.setUsedTime(new Date()); + code.setRemark(remark); + + voucherCodeMapper.updateById(code); + + voucherBatchService.updateBatchUsedCount(code.getBatchId()); + } + + @Override + public boolean canClaimVoucher(Long faceId, Long scenicId) { + Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId); + return count == 0; + } + + /** + * 生成6位安全券码(去除易混淆字符) + * 字符集:数字2-9 + 大写字母(去除0,1,I,L,O,S) + * + * @return 6位券码 + */ + private String generateVoucherCode() { + for (int attempt = 0; attempt < MAX_RETRY; attempt++) { + String code = generateRandomCode(); + log.debug("生成券码候选: {} (尝试第{}次)", code, attempt + 1); + + if (!isCodeExists(code)) { + log.info("成功生成券码: {} (字符集大小: {}, 理论组合数: {})", + code, SAFE_CHARS.length(), Math.pow(SAFE_CHARS.length(), CODE_LENGTH)); + return code; + } + + log.warn("券码重复,重新生成: {}", code); + } + + // 如果重试次数用完仍有重复,抛出异常 + log.error("券码生成失败:达到最大重试次数 {}", MAX_RETRY); + throw new RuntimeException("券码生成失败:达到最大重试次数,请稍后重试"); + } + + /** + * 生成随机6位字符 + * + * @return 随机6位字符 + */ + private String generateRandomCode() { + StringBuilder code = new StringBuilder(CODE_LENGTH); + for (int i = 0; i < CODE_LENGTH; i++) { + int randomIndex = RANDOM.nextInt(SAFE_CHARS_ARRAY.length); + code.append(SAFE_CHARS_ARRAY[randomIndex]); + } + return code.toString(); + } + + /** + * 检查券码是否已存在 + * + * @param code 券码 + * @return 是否存在 + */ + private boolean isCodeExists(String code) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherCode::getCode, code) + .eq(PriceVoucherCode::getDeleted, 0); + return voucherCodeMapper.selectCount(wrapper) > 0; + } + + private VoucherCodeResp convertToResp(PriceVoucherCode code, PriceVoucherBatchConfig batch) { + VoucherCodeResp resp = new VoucherCodeResp(); + BeanUtils.copyProperties(code, resp); + + if (batch != null) { + resp.setBatchName(batch.getBatchName()); + resp.setDiscountType(batch.getDiscountType()); + resp.setDiscountValue(batch.getDiscountValue()); + + VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType()); + if (discountType != null) { + resp.setDiscountTypeName(discountType.getName()); + resp.setDiscountDescription(discountType.getDescription()); + } + } + + VoucherCodeStatus status = VoucherCodeStatus.getByCode(code.getStatus()); + if (status != null) { + resp.setStatusName(status.getName()); + } + + return resp; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java new file mode 100644 index 0000000..32e5643 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java @@ -0,0 +1,175 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.service.IDiscountProvider; +import com.ycwl.basic.pricing.service.IVoucherService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 券码折扣提供者 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class VoucherDiscountProvider implements IDiscountProvider { + + private final IVoucherService voucherService; + + @Override + public String getProviderType() { + return "VOUCHER"; + } + + @Override + public int getPriority() { + return 100; // 券码优先级最高 + } + + @Override + public List detectAvailableDiscounts(DiscountDetectionContext context) { + List discounts = new ArrayList<>(); + + if (context.getFaceId() == null || context.getScenicId() == null) { + return discounts; + } + + try { + VoucherInfo voucherInfo = null; + + // 优先检查用户主动输入的券码 + if (StringUtils.hasText(context.getVoucherCode())) { + voucherInfo = voucherService.validateAndGetVoucherInfo( + context.getVoucherCode(), + context.getFaceId(), + context.getScenicId() + ); + } + // 如果没有输入券码且允许自动使用,则查找最优券码 + else if (Boolean.TRUE.equals(context.getAutoUseVoucher())) { + voucherInfo = voucherService.getBestVoucher( + context.getFaceId(), + context.getScenicId(), + context + ); + } + + if (voucherInfo != null && Boolean.TRUE.equals(voucherInfo.getAvailable())) { + // 计算券码优惠金额 + BigDecimal discountAmount = voucherService.calculateVoucherDiscount(voucherInfo, context); + + if (discountAmount.compareTo(BigDecimal.ZERO) > 0) { + DiscountInfo discountInfo = new DiscountInfo(); + discountInfo.setDiscountId(voucherInfo.getVoucherId()); + discountInfo.setDiscountType("VOUCHER"); + discountInfo.setDiscountName("券码优惠"); + discountInfo.setDiscountDescription(buildDiscountDescription(voucherInfo)); + discountInfo.setDiscountAmount(discountAmount); + discountInfo.setProviderType(getProviderType()); + discountInfo.setPriority(getPriority()); + discountInfo.setStackable(isStackable(voucherInfo)); // 只有全场免费不可叠加 + discountInfo.setVoucherCode(voucherInfo.getVoucherCode()); + + discounts.add(discountInfo); + } + } + } catch (Exception e) { + log.warn("检测券码优惠时发生异常", e); + } + + return discounts; + } + + @Override + public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) { + DiscountResult result = new DiscountResult(); + result.setDiscountInfo(discountInfo); + + try { + String voucherCode = discountInfo.getVoucherCode(); + if (!StringUtils.hasText(voucherCode)) { + result.setSuccess(false); + result.setFailureReason("券码信息丢失"); + return result; + } + + // 重新验证券码 + VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo( + voucherCode, + context.getFaceId(), + context.getScenicId() + ); + + if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable())) { + result.setSuccess(false); + result.setFailureReason("券码无效或不可用"); + return result; + } + + // 计算实际优惠金额 + BigDecimal actualDiscount = voucherService.calculateVoucherDiscount(voucherInfo, context); + BigDecimal finalAmount; + + // 对于全场免费券码,最终金额为0 + if (voucherInfo.getDiscountType() == VoucherDiscountType.FREE_ALL) { + finalAmount = BigDecimal.ZERO; + actualDiscount = context.getCurrentAmount(); + } else { + finalAmount = context.getCurrentAmount().subtract(actualDiscount); + if (finalAmount.compareTo(BigDecimal.ZERO) < 0) { + finalAmount = BigDecimal.ZERO; + actualDiscount = context.getCurrentAmount(); + } + } + + result.setActualDiscountAmount(actualDiscount); + result.setFinalAmount(finalAmount); + result.setSuccess(true); + + log.info("成功应用券码: {}, 优惠金额: {}", voucherCode, actualDiscount); + + } catch (Exception e) { + log.error("应用券码失败: " + discountInfo.getVoucherCode(), e); + result.setSuccess(false); + result.setFailureReason("券码应用失败: " + e.getMessage()); + } + + return result; + } + + @Override + public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) { + return "VOUCHER".equals(discountInfo.getDiscountType()) && + context.getFaceId() != null && + context.getScenicId() != null && + StringUtils.hasText(discountInfo.getVoucherCode()); + } + + /** + * 构建优惠描述 + */ + private String buildDiscountDescription(VoucherInfo voucherInfo) { + if (voucherInfo.getDiscountType() == null) { + return "券码优惠"; + } + + return String.format("券码 %s - %s", + voucherInfo.getVoucherCode(), + voucherInfo.getDiscountType().getName()); + } + + /** + * 判断是否可以与其他优惠叠加 + */ + private boolean isStackable(VoucherInfo voucherInfo) { + // 全场免费券码不可与其他优惠叠加 + return voucherInfo.getDiscountType() != VoucherDiscountType.FREE_ALL; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java new file mode 100644 index 0000000..e57ccf2 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java @@ -0,0 +1,205 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; +import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import com.ycwl.basic.pricing.entity.VoucherPrintRecord; +import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; +import com.ycwl.basic.pricing.mapper.VoucherPrintRecordMapper; +import com.ycwl.basic.pricing.service.VoucherPrintService; +import com.ycwl.basic.repository.FaceRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 优惠券打印服务实现 + */ +@Slf4j +@Service +public class VoucherPrintServiceImpl implements VoucherPrintService { + + @Autowired + private VoucherPrintRecordMapper voucherPrintRecordMapper; + + @Autowired + private PriceVoucherCodeMapper priceVoucherCodeMapper; + + @Autowired + private FaceRepository faceRepository; + + @Autowired + private RedisTemplate redisTemplate; + + private static final String PRINT_LOCK_KEY = "voucher_print_lock:%s:%s:%s"; // faceId:brokerId:scenicId + + // 原子计数器,确保流水号唯一性 + private static final AtomicLong counter = new AtomicLong(0); + + @Override + @Transactional(rollbackFor = Exception.class) + public VoucherPrintResp printVoucherTicket(VoucherPrintReq request) { + // 参数验证 + if (request.getFaceId() == null) { + throw new BaseException("用户faceId不能为空"); + } + if (request.getBrokerId() == null) { + throw new BaseException("经纪人ID不能为空"); + } + FaceEntity face = faceRepository.getFace(request.getFaceId()); + if (face == null) { + throw new BaseException("请上传人脸"); + } + request.setScenicId(face.getScenicId()); + + Long currentUserId = Long.valueOf(BaseContextHandler.getUserId()); + + // 验证faceId是否属于当前用户 + validateFaceOwnership(request.getFaceId(), currentUserId); + + // 使用Redis分布式锁防止重复打印 + String lockKey = String.format(PRINT_LOCK_KEY, request.getFaceId(), request.getBrokerId(), request.getScenicId()); + String lockValue = UUID.randomUUID().toString(); + + try { + // 尝试获取锁,超时时间30秒 + Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); + if (Boolean.FALSE.equals(lockAcquired)) { + throw new BaseException("请求处理中,请稍后再试"); + } + + // 检查是否已存在相同的打印记录 + VoucherPrintRecord existingRecord = voucherPrintRecordMapper.selectByFaceBrokerScenic( + request.getFaceId(), request.getScenicId()); + + if (existingRecord != null) { + log.info("找到已存在的打印记录,返回该记录: {}", existingRecord.getId()); + return buildResponse(existingRecord); + } + + // 获取一个可用的券码(未被打印过的) + PriceVoucherCode availableVoucher = getAvailableUnprintedVoucher(request.getScenicId()); + if (availableVoucher == null) { + throw new BaseException("暂无可用优惠券"); + } + + // 生成流水号 + String code = generateCode(); + + // 创建打印记录 + VoucherPrintRecord printRecord = new VoucherPrintRecord(); + printRecord.setCode(code); + printRecord.setFaceId(request.getFaceId()); + printRecord.setBrokerId(request.getBrokerId()); + printRecord.setScenicId(request.getScenicId()); + printRecord.setVoucherCodeId(availableVoucher.getId()); + printRecord.setVoucherCode(availableVoucher.getCode()); + printRecord.setPrintStatus(0); // 待打印 + printRecord.setCreateTime(new Date()); + printRecord.setUpdateTime(new Date()); + printRecord.setDeleted(0); + + voucherPrintRecordMapper.insert(printRecord); + + // TODO: 调用打印机接口打印小票 + // printTicket(printRecord); + + // 暂时标记为打印成功状态(实际应该在打印成功回调中更新) + printRecord.setPrintStatus(1); + voucherPrintRecordMapper.updatePrintStatus(printRecord.getId(), 1, null); + + log.info("成功创建打印记录: {}, 券码: {}", code, availableVoucher.getCode()); + return buildResponse(printRecord); + + } finally { + // 释放锁 + if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { + redisTemplate.delete(lockKey); + } + } + } + + @Override + public VoucherPrintResp queryPrintedVoucher(Long faceId) { + + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + throw new BaseException("请上传人脸"); + } + + Long currentUserId = Long.valueOf(BaseContextHandler.getUserId()); + + // 验证faceId是否属于当前用户 + validateFaceOwnership(faceId, currentUserId); + // 检查是否已存在相同的打印记录 + VoucherPrintRecord existingRecord = voucherPrintRecordMapper.selectByFaceBrokerScenic( + faceId, face.getScenicId()); + if (existingRecord == null) { + return null; + } + return buildResponse(existingRecord); + } + + /** + * 验证faceId是否属于当前用户 + */ + private void validateFaceOwnership(Long faceId, Long currentUserId) { + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + throw new BaseException("用户人脸信息不存在"); + } + + if (!currentUserId.equals(face.getMemberId())) { + throw new BaseException("无权限操作该人脸信息"); + } + } + + /** + * 获取可用且未被打印过的券码 + */ + private PriceVoucherCode getAvailableUnprintedVoucher(Long scenicId) { + return priceVoucherCodeMapper.findRandomUnprintedVoucher(scenicId); + } + + /** + * 生成流水号(优化版本) + * 使用原子计数器确保唯一性,解决原方法在高并发下的重复问题 + */ + private String generateCode() { + // 方案:使用毫秒级时间戳 + 原子计数器 + // 格式:5位计数器 + SSS + SimpleDateFormat sdf = new SimpleDateFormat("SSS"); + String timestamp = sdf.format(new Date()); + long count = counter.incrementAndGet() % 100000; // 5位计数,循环使用 + return String.format("%05d", count) + timestamp; + } + + /** + * 构建响应对象 + */ + private VoucherPrintResp buildResponse(VoucherPrintRecord record) { + VoucherPrintResp response = new VoucherPrintResp(); + BeanUtils.copyProperties(record, response); + return response; + } + + /** + * 调用打印机接口(待实现) + */ + private void printTicket(VoucherPrintRecord record) { + // TODO: 实现打印机接口调用逻辑 + log.info("TODO: 调用打印机打印小票,记录ID: {}, 券码: {}", record.getId(), record.getVoucherCode()); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java new file mode 100644 index 0000000..b983b39 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java @@ -0,0 +1,273 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import com.ycwl.basic.pricing.enums.VoucherCodeStatus; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; +import com.ycwl.basic.pricing.service.IVoucherService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 券码服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherServiceImpl implements IVoucherService { + + private final PriceVoucherCodeMapper voucherCodeMapper; + private final PriceVoucherBatchConfigMapper voucherBatchConfigMapper; + + @Override + public VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId) { + if (!StringUtils.hasText(voucherCode)) { + return null; + } + + // 查询券码信息 + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) { + return null; + } + + // 查询批次信息 + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId()); + if (batchConfig == null || batchConfig.getDeleted() == 1 || batchConfig.getStatus() == 0) { + return null; + } + + // 验证景区匹配 + if (scenicId != null && !scenicId.equals(voucherCodeEntity.getScenicId())) { + return null; + } + + VoucherInfo voucherInfo = buildVoucherInfo(voucherCodeEntity, batchConfig); + + // 检查券码状态和可用性 + if (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) { + // 未领取状态,检查是否可以领取 + if (faceId != null && canClaimVoucher(faceId, voucherCodeEntity.getScenicId())) { + voucherInfo.setAvailable(true); + } else { + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("您已在该景区领取过券码"); + } + } else if (VoucherCodeStatus.CLAIMED_UNUSED.getCode().equals(voucherCodeEntity.getStatus())) { + // 已领取未使用,检查是否为当前用户 + if (faceId != null && faceId.equals(voucherCodeEntity.getFaceId())) { + voucherInfo.setAvailable(true); + } else { + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码已被其他用户领取"); + } + } else { + // 已使用 + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码已使用"); + } + + return voucherInfo; + } + + @Override + public List getAvailableVouchers(Long faceId, Long scenicId) { + if (faceId == null || scenicId == null) { + return new ArrayList<>(); + } + + List voucherCodes = voucherCodeMapper.selectAvailableVouchersByFaceIdAndScenicId(faceId, scenicId); + List voucherInfos = new ArrayList<>(); + + for (PriceVoucherCode voucherCode : voucherCodes) { + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCode.getBatchId()); + if (batchConfig != null && batchConfig.getDeleted() == 0 && batchConfig.getStatus() == 1) { + VoucherInfo voucherInfo = buildVoucherInfo(voucherCode, batchConfig); + voucherInfo.setAvailable(true); + voucherInfos.add(voucherInfo); + } + } + + return voucherInfos; + } + + @Override + public void markVoucherAsUsed(String voucherCode, String remark) { + if (!StringUtils.hasText(voucherCode)) { + return; + } + + int result = voucherCodeMapper.useVoucher(voucherCode, LocalDateTime.now(), remark); + if (result > 0) { + // 更新批次统计 + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity != null) { + voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1); + } + log.info("券码已标记为使用: {}", voucherCode); + } + } + + @Override + public boolean canClaimVoucher(Long faceId, Long scenicId) { + if (faceId == null || scenicId == null) { + return false; + } + + Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId); + return count == 0; + } + + /** + * 获取该faceId在scenicId下的券码详情列表 + * @param faceId 用户面部ID + * @param scenicId 景区ID + * @return 券码详情列表,包含所有状态的券码(已领取未使用、已使用等),如果没有券码则返回空列表 + */ + public List getVoucherDetails(Long faceId, Long scenicId) { + if (faceId == null || scenicId == null) { + return new ArrayList<>(); + } + + List voucherCodes = voucherCodeMapper.selectUserVouchers(faceId, scenicId); + List voucherInfos = new ArrayList<>(); + + for (PriceVoucherCode voucherCode : voucherCodes) { + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCode.getBatchId()); + if (batchConfig != null && batchConfig.getDeleted() == 0) { + VoucherInfo voucherInfo = buildVoucherInfo(voucherCode, batchConfig); + + // 设置可用性状态 + VoucherCodeStatus statusEnum = VoucherCodeStatus.getByCode(voucherCode.getStatus()); + if (statusEnum != null) { + switch (statusEnum) { + case CLAIMED_UNUSED: + voucherInfo.setAvailable(true); + break; + case USED: + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码已使用"); + break; + case UNCLAIMED: + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码未领取"); + break; + default: + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码状态异常"); + } + } else { + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码状态未知"); + } + + voucherInfos.add(voucherInfo); + } + } + + return voucherInfos; + } + + @Override + public BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context) { + if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable()) || + context.getProducts() == null || context.getProducts().isEmpty()) { + return BigDecimal.ZERO; + } + + VoucherDiscountType discountType = voucherInfo.getDiscountType(); + BigDecimal discountValue = voucherInfo.getDiscountValue(); + + if (discountType == null) { + return BigDecimal.ZERO; + } + + return switch (discountType) { + case FREE_ALL -> { + // 全场免费,返回当前总金额 + yield context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO; + } + case REDUCE_PRICE -> { + // 商品降价,每个商品减免固定金额 + if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0) { + yield BigDecimal.ZERO; + } + BigDecimal totalDiscount = BigDecimal.ZERO; + for (ProductItem product : context.getProducts()) { + BigDecimal productDiscount = discountValue.multiply(BigDecimal.valueOf(product.getQuantity())); + totalDiscount = totalDiscount.add(productDiscount); + } + yield totalDiscount; + } + case DISCOUNT -> { + // 商品打折,按百分比计算 + if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0 || + discountValue.compareTo(BigDecimal.valueOf(100)) >= 0) { + yield BigDecimal.ZERO; + } + BigDecimal currentAmount = context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO; + BigDecimal discountRate = discountValue.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP); + yield currentAmount.multiply(discountRate).setScale(2, RoundingMode.HALF_UP); + } + }; + } + + @Override + public VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context) { + List availableVouchers = getAvailableVouchers(faceId, scenicId); + if (availableVouchers.isEmpty()) { + return null; + } + + // 计算每个券码的优惠金额,选择最优的 + VoucherInfo bestVoucher = null; + BigDecimal maxDiscount = BigDecimal.ZERO; + + for (VoucherInfo voucher : availableVouchers) { + BigDecimal discount = calculateVoucherDiscount(voucher, context); + voucher.setActualDiscountAmount(discount); + + if (discount.compareTo(maxDiscount) > 0) { + maxDiscount = discount; + bestVoucher = voucher; + } + } + + return bestVoucher; + } + + /** + * 构建券码信息DTO + */ + private VoucherInfo buildVoucherInfo(PriceVoucherCode voucherCode, PriceVoucherBatchConfig batchConfig) { + VoucherInfo voucherInfo = new VoucherInfo(); + voucherInfo.setVoucherId(voucherCode.getId()); + voucherInfo.setVoucherCode(voucherCode.getCode()); + voucherInfo.setBatchId(batchConfig.getId()); + voucherInfo.setBatchName(batchConfig.getBatchName()); + voucherInfo.setScenicId(voucherCode.getScenicId()); + voucherInfo.setBrokerId(batchConfig.getBrokerId()); + voucherInfo.setDiscountType(VoucherDiscountType.getByCode(batchConfig.getDiscountType())); + voucherInfo.setDiscountValue(batchConfig.getDiscountValue()); + voucherInfo.setStatus(voucherCode.getStatus()); + voucherInfo.setClaimedTime(voucherCode.getClaimedTime()); + voucherInfo.setUsedTime(voucherCode.getUsedTime()); + + return voucherInfo; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/repository/SourceRepository.java b/src/main/java/com/ycwl/basic/repository/SourceRepository.java index 1d1b9be..b0965dd 100644 --- a/src/main/java/com/ycwl/basic/repository/SourceRepository.java +++ b/src/main/java/com/ycwl/basic/repository/SourceRepository.java @@ -1,8 +1,13 @@ package com.ycwl.basic.repository; import com.ycwl.basic.mapper.SourceMapper; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity; +import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.service.IVoucherService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @@ -10,12 +15,17 @@ import org.springframework.stereotype.Component; import java.util.List; import java.util.Objects; +@Slf4j @Component public class SourceRepository { @Autowired private SourceMapper sourceMapper; @Autowired private RedisTemplate redisTemplate; + @Autowired + private IVoucherService iVoucherService; + @Autowired + private FaceRepository faceRepository; public void addSource(SourceEntity source) { sourceMapper.add(source); @@ -42,6 +52,19 @@ public class SourceRepository { } public boolean getUserIsBuy(Long userId, int type, Long faceId) { + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + log.info("faceId:{} is not exist", faceId); + return false; + } + // 确认人员faceId是否有券码 + List voucherDetails = iVoucherService.getVoucherDetails(faceId, face.getScenicId()); + if (voucherDetails != null && !voucherDetails.isEmpty()) { + VoucherInfo voucherInfo = voucherDetails.getFirst(); + if (voucherInfo.getDiscountType().equals(VoucherDiscountType.FREE_ALL)) { + return true; + } + } switch (type) { case 1: List videoSourceList = sourceMapper.listVideoByFaceRelation(userId, faceId); diff --git a/src/main/java/com/ycwl/basic/repository/VideoRepository.java b/src/main/java/com/ycwl/basic/repository/VideoRepository.java index 5d3c5ab..c0d4ae6 100644 --- a/src/main/java/com/ycwl/basic/repository/VideoRepository.java +++ b/src/main/java/com/ycwl/basic/repository/VideoRepository.java @@ -3,6 +3,10 @@ package com.ycwl.basic.repository; import com.ycwl.basic.biz.PriceBiz; import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO; import com.ycwl.basic.model.mobile.order.IsBuyRespVO; +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.service.IVoucherService; import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity; @@ -15,6 +19,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.util.List; import java.util.concurrent.TimeUnit; @Component @@ -29,6 +34,8 @@ public class VideoRepository { @Autowired @Lazy private PriceBiz priceBiz; + @Autowired + private IVoucherService iVoucherService; public VideoEntity getVideo(Long videoId) { if (redisTemplate.hasKey(String.format(VIDEO_CACHE_KEY, videoId))) { @@ -106,6 +113,14 @@ public class VideoRepository { if (buy.isBuy()) { return true; } + // 确认人员faceId是否有券码 + List voucherDetails = iVoucherService.getVoucherDetails(memberVideo.getFaceId(), memberVideo.getScenicId()); + if (voucherDetails != null && !voucherDetails.isEmpty()) { + VoucherInfo voucherInfo = voucherDetails.getFirst(); + if (voucherInfo.getDiscountType().equals(VoucherDiscountType.FREE_ALL)) { + isBuy = true; + } + } return isBuy; } diff --git a/src/main/resources/mapper/PriceVoucherCodeMapper.xml b/src/main/resources/mapper/PriceVoucherCodeMapper.xml new file mode 100644 index 0000000..2084c0d --- /dev/null +++ b/src/main/resources/mapper/PriceVoucherCodeMapper.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + id, batch_id, scenic_id, code, status, face_id, claimed_time, 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 + + + + UPDATE price_voucher_code + SET status = 2, + used_time = #{usedTime}, + remark = #{remark}, + update_time = NOW() + WHERE code = #{code} + AND status = 1 + 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 new file mode 100644 index 0000000..8d33bae --- /dev/null +++ b/src/main/resources/mapper/VoucherPrintRecordMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + 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