You've already forked FrameTour-BE
feat(pricing): 添加券码管理和使用功能
- 新增券码批次配置和券码实体 - 实现券码创建、领取、使用等接口 - 添加券码状态和优惠类型枚举 - 优化价格计算逻辑,支持券码优惠 - 新增优惠检测和应用相关功能
This commit is contained in:
557
src/main/java/com/ycwl/basic/pricing/CLAUDE.md
Normal file
557
src/main/java/com/ycwl/basic/pricing/CLAUDE.md
Normal file
@@ -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<List<BundleProductItem>> {
|
||||||
|
// JSON序列化/反序列化逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在实体类中使用
|
||||||
|
public class SomeEntity {
|
||||||
|
@TableField(typeHandler = BundleProductListTypeHandler.class)
|
||||||
|
private List<BundleProductItem> bundleProducts;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 异常处理模式
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 自定义异常类
|
||||||
|
public class PriceCalculationException extends RuntimeException {
|
||||||
|
public PriceCalculationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在PricingExceptionHandler中统一处理
|
||||||
|
@ExceptionHandler(PriceCalculationException.class)
|
||||||
|
public ApiResponse<String> handlePriceCalculationException(PriceCalculationException e) {
|
||||||
|
return ApiResponse.error(ErrorCode.PRICE_CALCULATION_ERROR, e.getMessage());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 分页查询实现
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 使用PageHelper实现分页
|
||||||
|
@Override
|
||||||
|
public PageInfo<CouponConfig> getCouponsByPage(int pageNum, int pageSize, String status, String name) {
|
||||||
|
PageHelper.startPage(pageNum, pageSize);
|
||||||
|
|
||||||
|
QueryWrapper<CouponConfig> queryWrapper = new QueryWrapper<>();
|
||||||
|
if (StringUtils.hasText(status)) {
|
||||||
|
queryWrapper.eq("status", status);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(name)) {
|
||||||
|
queryWrapper.like("name", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CouponConfig> list = couponConfigMapper.selectList(queryWrapper);
|
||||||
|
return new PageInfo<>(list);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试策略
|
||||||
|
|
||||||
|
### 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<DiscountInfo> 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<DiscountInfo> 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验证统计数据准确性
|
@@ -1,25 +1,25 @@
|
|||||||
package com.ycwl.basic.voucher.controller;
|
package com.ycwl.basic.pricing.controller;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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 com.ycwl.basic.utils.ApiResponse;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq;
|
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq;
|
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherClaimReq;
|
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq;
|
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp;
|
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp;
|
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp;
|
|
||||||
import com.ycwl.basic.voucher.service.VoucherBatchService;
|
|
||||||
import com.ycwl.basic.voucher.service.VoucherCodeService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/voucher")
|
@RequestMapping("/api/pricing/voucher")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VoucherController {
|
public class VoucherManagementController {
|
||||||
|
|
||||||
private final VoucherBatchService voucherBatchService;
|
private final VoucherBatchService voucherBatchService;
|
||||||
private final VoucherCodeService voucherCodeService;
|
private final VoucherCodeService voucherCodeService;
|
@@ -3,7 +3,7 @@ package com.ycwl.basic.pricing.dto;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 优惠券使用结果DTO
|
* 优惠券使用结果DTO
|
||||||
@@ -29,7 +29,7 @@ public class CouponUseResult {
|
|||||||
/**
|
/**
|
||||||
* 使用时间
|
* 使用时间
|
||||||
*/
|
*/
|
||||||
private LocalDateTime useTime;
|
private Date useTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 优惠金额
|
* 优惠金额
|
||||||
|
@@ -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<DiscountResult> appliedDiscounts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可用但未应用的优惠列表
|
||||||
|
*/
|
||||||
|
private List<DiscountInfo> availableDiscounts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优惠详情列表(用于展示)
|
||||||
|
*/
|
||||||
|
private List<DiscountDetail> discountDetails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算是否成功
|
||||||
|
*/
|
||||||
|
private Boolean success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息(如果success为false)
|
||||||
|
*/
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
@@ -44,7 +44,7 @@ public class DiscountDetail {
|
|||||||
detail.setDiscountName("限时立减");
|
detail.setDiscountName("限时立减");
|
||||||
detail.setDiscountAmount(discountAmount);
|
detail.setDiscountAmount(discountAmount);
|
||||||
detail.setDescription("限时优惠,立即享受");
|
detail.setDescription("限时优惠,立即享受");
|
||||||
detail.setSortOrder(1); // 限时立减排在最前面
|
detail.setSortOrder(2); // 限时立减排在券码后面
|
||||||
return detail;
|
return detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,20 @@ public class DiscountDetail {
|
|||||||
detail.setDiscountName(couponName);
|
detail.setDiscountName(couponName);
|
||||||
detail.setDiscountAmount(discountAmount);
|
detail.setDiscountAmount(discountAmount);
|
||||||
detail.setDescription("优惠券减免");
|
detail.setDescription("优惠券减免");
|
||||||
detail.setSortOrder(2); // 优惠券排在限时立减后面
|
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;
|
return detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +83,7 @@ public class DiscountDetail {
|
|||||||
detail.setDiscountName("一口价优惠");
|
detail.setDiscountName("一口价优惠");
|
||||||
detail.setDiscountAmount(discountAmount);
|
detail.setDiscountAmount(discountAmount);
|
||||||
detail.setDescription("一口价购买更优惠");
|
detail.setDescription("一口价购买更优惠");
|
||||||
detail.setSortOrder(3); // 一口价排在最后
|
detail.setSortOrder(4); // 一口价排在最后
|
||||||
return detail;
|
return detail;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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<ProductItem> products;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前金额
|
||||||
|
*/
|
||||||
|
private BigDecimal currentAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户主动输入的券码
|
||||||
|
*/
|
||||||
|
private String voucherCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否自动使用优惠券
|
||||||
|
*/
|
||||||
|
private Boolean autoUseCoupon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否自动使用券码
|
||||||
|
*/
|
||||||
|
private Boolean autoUseVoucher;
|
||||||
|
}
|
82
src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java
Normal file
82
src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java
Normal file
@@ -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;
|
||||||
|
}
|
43
src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java
Normal file
43
src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java
Normal file
@@ -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<ProductItem> affectedProducts;
|
||||||
|
}
|
@@ -24,4 +24,29 @@ public class PriceCalculationRequest {
|
|||||||
* 是否自动使用优惠券
|
* 是否自动使用优惠券
|
||||||
*/
|
*/
|
||||||
private Boolean autoUseCoupon = true;
|
private Boolean autoUseCoupon = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户输入的券码
|
||||||
|
*/
|
||||||
|
private String voucherCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID(用于券码验证)
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户faceId(用于券码领取资格验证)
|
||||||
|
*/
|
||||||
|
private Long faceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否自动使用券码优惠
|
||||||
|
*/
|
||||||
|
private Boolean autoUseVoucher = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否仅预览优惠(不实际使用)
|
||||||
|
*/
|
||||||
|
private Boolean previewOnly = false;
|
||||||
}
|
}
|
@@ -37,10 +37,20 @@ public class PriceCalculationResult {
|
|||||||
private CouponInfo usedCoupon;
|
private CouponInfo usedCoupon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 折扣明细列表(包含限时立减、优惠券、一口价等)
|
* 使用的券码信息
|
||||||
|
*/
|
||||||
|
private VoucherInfo usedVoucher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 折扣明细列表(包含限时立减、优惠券、券码、一口价等)
|
||||||
*/
|
*/
|
||||||
private List<DiscountDetail> discountDetails;
|
private List<DiscountDetail> discountDetails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可用但未使用的优惠列表(预览时使用)
|
||||||
|
*/
|
||||||
|
private List<DiscountInfo> availableDiscounts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 商品明细列表
|
* 商品明细列表
|
||||||
*/
|
*/
|
||||||
|
84
src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java
Normal file
84
src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java
Normal file
@@ -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;
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package com.ycwl.basic.voucher.dto.req;
|
package com.ycwl.basic.pricing.dto.req;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package com.ycwl.basic.voucher.dto.req;
|
package com.ycwl.basic.pricing.dto.req;
|
||||||
|
|
||||||
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
@@ -1,4 +1,4 @@
|
|||||||
package com.ycwl.basic.voucher.dto.req;
|
package com.ycwl.basic.pricing.dto.req;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package com.ycwl.basic.voucher.dto.req;
|
package com.ycwl.basic.pricing.dto.req;
|
||||||
|
|
||||||
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
@@ -1,4 +1,4 @@
|
|||||||
package com.ycwl.basic.voucher.dto.resp;
|
package com.ycwl.basic.pricing.dto.resp;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package com.ycwl.basic.voucher.dto.resp;
|
package com.ycwl.basic.pricing.dto.resp;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package com.ycwl.basic.voucher.dto.resp;
|
package com.ycwl.basic.pricing.dto.resp;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础实体类
|
* 基础实体类
|
||||||
@@ -15,7 +15,15 @@ public class BaseEntity {
|
|||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
private LocalDateTime createdTime;
|
private Date createdTime;
|
||||||
|
|
||||||
private LocalDateTime updatedTime;
|
private Date updatedTime;
|
||||||
|
|
||||||
|
private Long createBy;
|
||||||
|
|
||||||
|
private Long updateBy;
|
||||||
|
|
||||||
|
private Integer deleted;
|
||||||
|
|
||||||
|
private Date deletedAt;
|
||||||
}
|
}
|
@@ -5,7 +5,7 @@ import com.ycwl.basic.pricing.enums.CouponStatus;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 优惠券领用记录实体
|
* 优惠券领用记录实体
|
||||||
@@ -28,12 +28,12 @@ public class PriceCouponClaimRecord extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 领取时间
|
* 领取时间
|
||||||
*/
|
*/
|
||||||
private LocalDateTime claimTime;
|
private Date claimTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用时间
|
* 使用时间
|
||||||
*/
|
*/
|
||||||
private LocalDateTime useTime;
|
private Date useTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单ID
|
* 订单ID
|
||||||
|
@@ -0,0 +1,72 @@
|
|||||||
|
package com.ycwl.basic.pricing.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 券码批次配置实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("price_voucher_batch_config")
|
||||||
|
public class PriceVoucherBatchConfig extends BaseEntity {
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人ID
|
||||||
|
*/
|
||||||
|
private Long createBy;
|
||||||
|
}
|
@@ -0,0 +1,61 @@
|
|||||||
|
package com.ycwl.basic.pricing.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 券码实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("price_voucher_code")
|
||||||
|
public class PriceVoucherCode extends BaseEntity {
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,19 +1,39 @@
|
|||||||
package com.ycwl.basic.voucher.enums;
|
package com.ycwl.basic.pricing.enums;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 券码优惠类型枚举
|
||||||
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum VoucherDiscountType {
|
public enum VoucherDiscountType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全场免费 - 所有商品免费
|
||||||
|
*/
|
||||||
FREE_ALL(0, "全场免费", "所有商品免费"),
|
FREE_ALL(0, "全场免费", "所有商品免费"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品降价 - 每个商品减免指定金额
|
||||||
|
*/
|
||||||
REDUCE_PRICE(1, "商品降价", "每个商品减免指定金额"),
|
REDUCE_PRICE(1, "商品降价", "每个商品减免指定金额"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品打折 - 每个商品按百分比打折
|
||||||
|
*/
|
||||||
DISCOUNT(2, "商品打折", "每个商品按百分比打折");
|
DISCOUNT(2, "商品打折", "每个商品按百分比打折");
|
||||||
|
|
||||||
private final Integer code;
|
private final Integer code;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据代码获取枚举值
|
||||||
|
* @param code 代码
|
||||||
|
* @return 枚举值
|
||||||
|
*/
|
||||||
public static VoucherDiscountType getByCode(Integer code) {
|
public static VoucherDiscountType getByCode(Integer code) {
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -25,4 +45,13 @@ public enum VoucherDiscountType {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为有效的优惠类型代码
|
||||||
|
* @param code 代码
|
||||||
|
* @return 是否有效
|
||||||
|
*/
|
||||||
|
public static boolean isValidCode(Integer code) {
|
||||||
|
return getByCode(code) != null;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -54,6 +54,46 @@ public class PricingExceptionHandler {
|
|||||||
return ApiResponse.buildResponse(400, "参数错误: " + e.getMessage());
|
return ApiResponse.buildResponse(400, "参数错误: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理券码无效异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(VoucherInvalidException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public ApiResponse<Void> handleVoucherInvalidException(VoucherInvalidException e) {
|
||||||
|
log.error("券码无效异常", e);
|
||||||
|
return ApiResponse.buildResponse(400, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理券码已使用异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(VoucherAlreadyUsedException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public ApiResponse<Void> handleVoucherAlreadyUsedException(VoucherAlreadyUsedException e) {
|
||||||
|
log.error("券码已使用异常", e);
|
||||||
|
return ApiResponse.buildResponse(400, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理券码不可领取异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(VoucherNotClaimableException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public ApiResponse<Void> handleVoucherNotClaimableException(VoucherNotClaimableException e) {
|
||||||
|
log.error("券码不可领取异常", e);
|
||||||
|
return ApiResponse.buildResponse(400, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理优惠检测异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(DiscountDetectionException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public ApiResponse<Void> handleDiscountDetectionException(DiscountDetectionException e) {
|
||||||
|
log.error("优惠检测异常", e);
|
||||||
|
return ApiResponse.buildResponse(400, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理通用异常
|
* 处理通用异常
|
||||||
*/
|
*/
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -44,7 +44,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper<PriceCouponClai
|
|||||||
"WHERE id = #{id}")
|
"WHERE id = #{id}")
|
||||||
int updateCouponStatus(@Param("id") Long id,
|
int updateCouponStatus(@Param("id") Long id,
|
||||||
@Param("status") CouponStatus status,
|
@Param("status") CouponStatus status,
|
||||||
@Param("useTime") java.time.LocalDateTime useTime,
|
@Param("useTime") java.util.Date useTime,
|
||||||
@Param("orderId") String orderId,
|
@Param("orderId") String orderId,
|
||||||
@Param("scenicId") String scenicId);
|
@Param("scenicId") String scenicId);
|
||||||
|
|
||||||
|
@@ -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<PriceVoucherBatchConfig> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据景区ID和推客ID查询有效的批次列表
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @param brokerId 推客ID
|
||||||
|
* @return 批次列表
|
||||||
|
*/
|
||||||
|
List<PriceVoucherBatchConfig> 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);
|
||||||
|
}
|
@@ -0,0 +1,94 @@
|
|||||||
|
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<PriceVoucherCode> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据券码查询券码信息
|
||||||
|
* @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<PriceVoucherCode> selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId,
|
||||||
|
@Param("scenicId") Long scenicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据批次ID获取可领取的券码(未领取状态)
|
||||||
|
* @param batchId 批次ID
|
||||||
|
* @param limit 限制数量
|
||||||
|
* @return 券码列表
|
||||||
|
*/
|
||||||
|
List<PriceVoucherCode> 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<PriceVoucherCode> selectByBatchId(@Param("batchId") Long batchId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户的券码列表
|
||||||
|
* @param faceId 用户faceId
|
||||||
|
* @param scenicId 景区ID(可选)
|
||||||
|
* @return 券码列表
|
||||||
|
*/
|
||||||
|
List<PriceVoucherCode> selectUserVouchers(@Param("faceId") Long faceId,
|
||||||
|
@Param("scenicId") Long scenicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据批次ID查询第一个可用的券码
|
||||||
|
* @param batchId 批次ID
|
||||||
|
* @return 可用券码
|
||||||
|
*/
|
||||||
|
PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId);
|
||||||
|
}
|
@@ -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<DiscountInfo> 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<IDiscountProvider> getAllProviders();
|
||||||
|
}
|
@@ -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<DiscountInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,62 @@
|
|||||||
|
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<VoucherInfo> 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算券码优惠金额
|
||||||
|
* @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);
|
||||||
|
}
|
@@ -1,11 +1,11 @@
|
|||||||
package com.ycwl.basic.voucher.service;
|
package com.ycwl.basic.pricing.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
||||||
import com.ycwl.basic.voucher.entity.VoucherBatchEntity;
|
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||||
|
|
||||||
public interface VoucherBatchService {
|
public interface VoucherBatchService {
|
||||||
|
|
||||||
@@ -23,5 +23,5 @@ public interface VoucherBatchService {
|
|||||||
|
|
||||||
void updateBatchUsedCount(Long batchId);
|
void updateBatchUsedCount(Long batchId);
|
||||||
|
|
||||||
VoucherBatchEntity getAvailableBatch(Long scenicId, Long brokerId);
|
PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId);
|
||||||
}
|
}
|
@@ -1,9 +1,9 @@
|
|||||||
package com.ycwl.basic.voucher.service;
|
package com.ycwl.basic.pricing.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherClaimReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@@ -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<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
|
||||||
|
List<DiscountInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -13,6 +13,8 @@ import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper;
|
|||||||
import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper;
|
import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper;
|
||||||
import com.ycwl.basic.pricing.service.ICouponService;
|
import com.ycwl.basic.pricing.service.ICouponService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -144,13 +146,13 @@ public class CouponServiceImpl implements ICouponService {
|
|||||||
throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限");
|
throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限");
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDateTime useTime = LocalDateTime.now();
|
Date useTime = new Date();
|
||||||
|
|
||||||
// 设置使用时间、订单信息和景区信息
|
// 设置使用时间、订单信息和景区信息
|
||||||
record.setStatus(CouponStatus.USED);
|
record.setStatus(CouponStatus.USED);
|
||||||
record.setUseTime(useTime);
|
record.setUseTime(useTime);
|
||||||
record.setOrderId(request.getOrderId());
|
record.setOrderId(request.getOrderId());
|
||||||
record.setUpdatedTime(LocalDateTime.now());
|
record.setUpdatedTime(new Date());
|
||||||
|
|
||||||
// 如果请求中包含景区ID,记录到使用记录中
|
// 如果请求中包含景区ID,记录到使用记录中
|
||||||
if (request.getScenicId() != null && !request.getScenicId().isEmpty()) {
|
if (request.getScenicId() != null && !request.getScenicId().isEmpty()) {
|
||||||
|
@@ -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<IDiscountProvider> discountProviders = new ArrayList<>();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public DiscountDetectionServiceImpl(List<IDiscountProvider> 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<DiscountInfo> detectAllAvailableDiscounts(DiscountDetectionContext context) {
|
||||||
|
List<DiscountInfo> allDiscounts = new ArrayList<>();
|
||||||
|
|
||||||
|
for (IDiscountProvider provider : discountProviders) {
|
||||||
|
try {
|
||||||
|
List<DiscountInfo> 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<DiscountInfo> 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<DiscountResult> appliedDiscounts = new ArrayList<>();
|
||||||
|
List<DiscountDetail> 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<IDiscountProvider> 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -5,10 +5,7 @@ import com.ycwl.basic.pricing.entity.PriceProductConfig;
|
|||||||
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
||||||
import com.ycwl.basic.pricing.enums.ProductType;
|
import com.ycwl.basic.pricing.enums.ProductType;
|
||||||
import com.ycwl.basic.pricing.exception.PriceCalculationException;
|
import com.ycwl.basic.pricing.exception.PriceCalculationException;
|
||||||
import com.ycwl.basic.pricing.service.ICouponService;
|
import com.ycwl.basic.pricing.service.*;
|
||||||
import com.ycwl.basic.pricing.service.IPriceBundleService;
|
|
||||||
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
|
||||||
import com.ycwl.basic.pricing.service.IProductConfigService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -30,6 +27,8 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
private final IProductConfigService productConfigService;
|
private final IProductConfigService productConfigService;
|
||||||
private final ICouponService couponService;
|
private final ICouponService couponService;
|
||||||
private final IPriceBundleService bundleService;
|
private final IPriceBundleService bundleService;
|
||||||
|
private final IDiscountDetectionService discountDetectionService;
|
||||||
|
private final IVoucherService voucherService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PriceCalculationResult calculatePrice(PriceCalculationRequest request) {
|
public PriceCalculationResult calculatePrice(PriceCalculationRequest request) {
|
||||||
@@ -59,35 +58,60 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount);
|
log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建价格计算结果
|
||||||
PriceCalculationResult result = new PriceCalculationResult();
|
PriceCalculationResult result = new PriceCalculationResult();
|
||||||
result.setOriginalAmount(originalTotalAmount); // 原总价
|
result.setOriginalAmount(originalTotalAmount); // 原总价
|
||||||
result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计
|
result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计
|
||||||
result.setProductDetails(request.getProducts());
|
result.setProductDetails(request.getProducts());
|
||||||
|
|
||||||
// 处理优惠券
|
// 使用新的优惠检测系统处理所有优惠(券码 + 优惠券)
|
||||||
BigDecimal couponDiscountAmount = BigDecimal.ZERO;
|
DiscountCombinationResult discountResult = calculateDiscounts(request, totalAmount);
|
||||||
if (Boolean.TRUE.equals(request.getAutoUseCoupon()) && request.getUserId() != null) {
|
|
||||||
CouponInfo bestCoupon = couponService.selectBestCoupon(
|
|
||||||
request.getUserId(), request.getProducts(), totalAmount);
|
|
||||||
|
|
||||||
if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
|
if (Boolean.TRUE.equals(discountResult.getSuccess())) {
|
||||||
result.setUsedCoupon(bestCoupon);
|
// 合并所有优惠详情
|
||||||
couponDiscountAmount = bestCoupon.getActualDiscountAmount();
|
List<DiscountDetail> allDiscountDetails = new ArrayList<>(discountDetails);
|
||||||
discountDetails.add(DiscountDetail.createCouponDiscount(bestCoupon.getCouponName(), couponDiscountAmount));
|
if (discountResult.getDiscountDetails() != null) {
|
||||||
|
allDiscountDetails.addAll(discountResult.getDiscountDetails());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 计算总优惠金额
|
// 重新排序
|
||||||
BigDecimal totalDiscountAmount = discountDetails.stream()
|
allDiscountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
|
||||||
|
|
||||||
|
// 计算总优惠金额(包括限时立减、一口价和其他优惠)
|
||||||
|
BigDecimal totalDiscountAmount = allDiscountDetails.stream()
|
||||||
.map(DiscountDetail::getDiscountAmount)
|
.map(DiscountDetail::getDiscountAmount)
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
// 按排序排列折扣明细
|
// 设置结果
|
||||||
discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
|
result.setDiscountAmount(totalDiscountAmount);
|
||||||
|
result.setDiscountDetails(allDiscountDetails);
|
||||||
|
result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount));
|
||||||
|
|
||||||
result.setDiscountAmount(totalDiscountAmount);
|
// 设置使用的券码和优惠券信息
|
||||||
result.setDiscountDetails(discountDetails);
|
setUsedDiscountInfo(result, discountResult, request);
|
||||||
result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount));
|
|
||||||
|
// 如果是预览模式,设置可用优惠列表
|
||||||
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -269,4 +293,96 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
|
|
||||||
return new ProductPriceInfo(actualPrice, originalPrice);
|
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);
|
||||||
|
// 不抛出异常,避免影响主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -8,7 +8,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 价格管理服务实现(用于配置管理,手动处理时间字段)
|
* 价格管理服务实现(用于配置管理,手动处理时间字段)
|
||||||
@@ -35,8 +35,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.setCreatedTime(LocalDateTime.now());
|
config.setCreatedTime(new Date());
|
||||||
config.setUpdatedTime(LocalDateTime.now());
|
config.setUpdatedTime(new Date());
|
||||||
productConfigMapper.insertProductConfig(config);
|
productConfigMapper.insertProductConfig(config);
|
||||||
return config.getId();
|
return config.getId();
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean updateProductConfig(PriceProductConfig config) {
|
public boolean updateProductConfig(PriceProductConfig config) {
|
||||||
config.setUpdatedTime(LocalDateTime.now());
|
config.setUpdatedTime(new Date());
|
||||||
return productConfigMapper.updateProductConfig(config) > 0;
|
return productConfigMapper.updateProductConfig(config) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
|||||||
config.getProductType(), config.getMinQuantity(), config.getMaxQuantity());
|
config.getProductType(), config.getMinQuantity(), config.getMaxQuantity());
|
||||||
}
|
}
|
||||||
|
|
||||||
config.setCreatedTime(LocalDateTime.now());
|
config.setCreatedTime(new Date());
|
||||||
config.setUpdatedTime(LocalDateTime.now());
|
config.setUpdatedTime(new Date());
|
||||||
tierConfigMapper.insertTierConfig(config);
|
tierConfigMapper.insertTierConfig(config);
|
||||||
return config.getId();
|
return config.getId();
|
||||||
}
|
}
|
||||||
@@ -67,15 +67,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean updateTierConfig(PriceTierConfig config) {
|
public boolean updateTierConfig(PriceTierConfig config) {
|
||||||
config.setUpdatedTime(LocalDateTime.now());
|
config.setUpdatedTime(new Date());
|
||||||
return tierConfigMapper.updateTierConfig(config) > 0;
|
return tierConfigMapper.updateTierConfig(config) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public Long createCouponConfig(PriceCouponConfig config) {
|
public Long createCouponConfig(PriceCouponConfig config) {
|
||||||
config.setCreatedTime(LocalDateTime.now());
|
config.setCreatedTime(new Date());
|
||||||
config.setUpdatedTime(LocalDateTime.now());
|
config.setUpdatedTime(new Date());
|
||||||
couponConfigMapper.insertCoupon(config);
|
couponConfigMapper.insertCoupon(config);
|
||||||
return config.getId();
|
return config.getId();
|
||||||
}
|
}
|
||||||
@@ -83,16 +83,16 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean updateCouponConfig(PriceCouponConfig config) {
|
public boolean updateCouponConfig(PriceCouponConfig config) {
|
||||||
config.setUpdatedTime(LocalDateTime.now());
|
config.setUpdatedTime(new Date());
|
||||||
return couponConfigMapper.updateCoupon(config) > 0;
|
return couponConfigMapper.updateCoupon(config) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public Long createCouponClaimRecord(PriceCouponClaimRecord record) {
|
public Long createCouponClaimRecord(PriceCouponClaimRecord record) {
|
||||||
record.setClaimTime(LocalDateTime.now());
|
record.setClaimTime(new Date());
|
||||||
record.setCreatedTime(LocalDateTime.now());
|
record.setCreatedTime(new Date());
|
||||||
record.setUpdatedTime(LocalDateTime.now());
|
record.setUpdatedTime(new Date());
|
||||||
couponClaimRecordMapper.insertClaimRecord(record);
|
couponClaimRecordMapper.insertClaimRecord(record);
|
||||||
return record.getId();
|
return record.getId();
|
||||||
}
|
}
|
||||||
@@ -100,15 +100,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) {
|
public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) {
|
||||||
record.setUpdatedTime(LocalDateTime.now());
|
record.setUpdatedTime(new Date());
|
||||||
return couponClaimRecordMapper.updateClaimRecord(record) > 0;
|
return couponClaimRecordMapper.updateClaimRecord(record) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public Long createBundleConfig(PriceBundleConfig config) {
|
public Long createBundleConfig(PriceBundleConfig config) {
|
||||||
config.setCreatedTime(LocalDateTime.now());
|
config.setCreatedTime(new Date());
|
||||||
config.setUpdatedTime(LocalDateTime.now());
|
config.setUpdatedTime(new Date());
|
||||||
bundleConfigMapper.insertBundleConfig(config);
|
bundleConfigMapper.insertBundleConfig(config);
|
||||||
return config.getId();
|
return config.getId();
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean updateBundleConfig(PriceBundleConfig config) {
|
public boolean updateBundleConfig(PriceBundleConfig config) {
|
||||||
config.setUpdatedTime(LocalDateTime.now());
|
config.setUpdatedTime(new Date());
|
||||||
return bundleConfigMapper.updateBundleConfig(config) > 0;
|
return bundleConfigMapper.updateBundleConfig(config) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
package com.ycwl.basic.voucher.service.impl;
|
package com.ycwl.basic.pricing.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.ycwl.basic.constant.BaseContextHandler;
|
||||||
import com.ycwl.basic.exception.BizException;
|
import com.ycwl.basic.exception.BizException;
|
||||||
import com.ycwl.basic.interceptor.BaseContextHandler;
|
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq;
|
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp;
|
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||||
import com.ycwl.basic.voucher.entity.VoucherBatchEntity;
|
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||||
import com.ycwl.basic.voucher.enums.VoucherDiscountType;
|
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
||||||
import com.ycwl.basic.voucher.mapper.VoucherBatchMapper;
|
import com.ycwl.basic.pricing.service.VoucherBatchService;
|
||||||
import com.ycwl.basic.voucher.service.VoucherBatchService;
|
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||||
import com.ycwl.basic.voucher.service.VoucherCodeService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -25,7 +25,7 @@ import java.util.Date;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VoucherBatchServiceImpl implements VoucherBatchService {
|
public class VoucherBatchServiceImpl implements VoucherBatchService {
|
||||||
|
|
||||||
private final VoucherBatchMapper voucherBatchMapper;
|
private final PriceVoucherBatchConfigMapper voucherBatchMapper;
|
||||||
private final VoucherCodeService voucherCodeService;
|
private final VoucherCodeService voucherCodeService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -56,13 +56,16 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
throw new BizException(400, "优惠金额不能为空");
|
throw new BizException(400, "优惠金额不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
VoucherBatchEntity batch = new VoucherBatchEntity();
|
PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig();
|
||||||
BeanUtils.copyProperties(req, batch);
|
BeanUtils.copyProperties(req, batch);
|
||||||
batch.setUsedCount(0);
|
batch.setUsedCount(0);
|
||||||
batch.setClaimedCount(0);
|
batch.setClaimedCount(0);
|
||||||
batch.setStatus(1);
|
batch.setStatus(1);
|
||||||
batch.setCreateTime(new Date());
|
batch.setCreatedTime(new Date());
|
||||||
batch.setCreateBy(BaseContextHandler.getUserId());
|
String userIdStr = BaseContextHandler.getUserId();
|
||||||
|
if (userIdStr != null) {
|
||||||
|
batch.setCreateBy(Long.valueOf(userIdStr));
|
||||||
|
}
|
||||||
batch.setDeleted(0);
|
batch.setDeleted(0);
|
||||||
|
|
||||||
voucherBatchMapper.insert(batch);
|
voucherBatchMapper.insert(batch);
|
||||||
@@ -74,17 +77,17 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
|
public Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
|
||||||
Page<VoucherBatchEntity> page = new Page<>(req.getPageNum(), req.getPageSize());
|
Page<PriceVoucherBatchConfig> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||||
|
|
||||||
LambdaQueryWrapper<VoucherBatchEntity> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PriceVoucherBatchConfig> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(VoucherBatchEntity::getDeleted, 0)
|
wrapper.eq(PriceVoucherBatchConfig::getDeleted, 0)
|
||||||
.eq(req.getScenicId() != null, VoucherBatchEntity::getScenicId, req.getScenicId())
|
.eq(req.getScenicId() != null, PriceVoucherBatchConfig::getScenicId, req.getScenicId())
|
||||||
.eq(req.getBrokerId() != null, VoucherBatchEntity::getBrokerId, req.getBrokerId())
|
.eq(req.getBrokerId() != null, PriceVoucherBatchConfig::getBrokerId, req.getBrokerId())
|
||||||
.eq(req.getStatus() != null, VoucherBatchEntity::getStatus, req.getStatus())
|
.eq(req.getStatus() != null, PriceVoucherBatchConfig::getStatus, req.getStatus())
|
||||||
.like(StringUtils.hasText(req.getBatchName()), VoucherBatchEntity::getBatchName, req.getBatchName())
|
.like(StringUtils.hasText(req.getBatchName()), PriceVoucherBatchConfig::getBatchName, req.getBatchName())
|
||||||
.orderByDesc(VoucherBatchEntity::getCreateTime);
|
.orderByDesc(PriceVoucherBatchConfig::getCreatedTime);
|
||||||
|
|
||||||
Page<VoucherBatchEntity> entityPage = voucherBatchMapper.selectPage(page, wrapper);
|
Page<PriceVoucherBatchConfig> entityPage = voucherBatchMapper.selectPage(page, wrapper);
|
||||||
|
|
||||||
Page<VoucherBatchResp> respPage = new Page<>();
|
Page<VoucherBatchResp> respPage = new Page<>();
|
||||||
BeanUtils.copyProperties(entityPage, respPage);
|
BeanUtils.copyProperties(entityPage, respPage);
|
||||||
@@ -96,7 +99,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VoucherBatchResp getBatchDetail(Long id) {
|
public VoucherBatchResp getBatchDetail(Long id) {
|
||||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(id);
|
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id);
|
||||||
if (batch == null || batch.getDeleted() == 1) {
|
if (batch == null || batch.getDeleted() == 1) {
|
||||||
throw new BizException(404, "券码批次不存在");
|
throw new BizException(404, "券码批次不存在");
|
||||||
}
|
}
|
||||||
@@ -106,7 +109,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VoucherBatchStatsResp getBatchStats(Long id) {
|
public VoucherBatchStatsResp getBatchStats(Long id) {
|
||||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(id);
|
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id);
|
||||||
if (batch == null || batch.getDeleted() == 1) {
|
if (batch == null || batch.getDeleted() == 1) {
|
||||||
throw new BizException(404, "券码批次不存在");
|
throw new BizException(404, "券码批次不存在");
|
||||||
}
|
}
|
||||||
@@ -132,7 +135,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateBatchStatus(Long id, Integer status) {
|
public void updateBatchStatus(Long id, Integer status) {
|
||||||
VoucherBatchEntity batch = new VoucherBatchEntity();
|
PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig();
|
||||||
batch.setId(id);
|
batch.setId(id);
|
||||||
batch.setStatus(status);
|
batch.setStatus(status);
|
||||||
|
|
||||||
@@ -144,7 +147,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateBatchClaimedCount(Long batchId) {
|
public void updateBatchClaimedCount(Long batchId) {
|
||||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(batchId);
|
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId);
|
||||||
if (batch != null) {
|
if (batch != null) {
|
||||||
batch.setClaimedCount(batch.getClaimedCount() + 1);
|
batch.setClaimedCount(batch.getClaimedCount() + 1);
|
||||||
voucherBatchMapper.updateById(batch);
|
voucherBatchMapper.updateById(batch);
|
||||||
@@ -153,7 +156,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateBatchUsedCount(Long batchId) {
|
public void updateBatchUsedCount(Long batchId) {
|
||||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(batchId);
|
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId);
|
||||||
if (batch != null) {
|
if (batch != null) {
|
||||||
batch.setUsedCount(batch.getUsedCount() + 1);
|
batch.setUsedCount(batch.getUsedCount() + 1);
|
||||||
voucherBatchMapper.updateById(batch);
|
voucherBatchMapper.updateById(batch);
|
||||||
@@ -161,19 +164,19 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VoucherBatchEntity getAvailableBatch(Long scenicId, Long brokerId) {
|
public PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId) {
|
||||||
LambdaQueryWrapper<VoucherBatchEntity> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PriceVoucherBatchConfig> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(VoucherBatchEntity::getScenicId, scenicId)
|
wrapper.eq(PriceVoucherBatchConfig::getScenicId, scenicId)
|
||||||
.eq(VoucherBatchEntity::getBrokerId, brokerId)
|
.eq(PriceVoucherBatchConfig::getBrokerId, brokerId)
|
||||||
.eq(VoucherBatchEntity::getStatus, 1)
|
.eq(PriceVoucherBatchConfig::getStatus, 1)
|
||||||
.eq(VoucherBatchEntity::getDeleted, 0)
|
.eq(PriceVoucherBatchConfig::getDeleted, 0)
|
||||||
.lt(VoucherBatchEntity::getClaimedCount, VoucherBatchEntity::getTotalCount)
|
.apply("claimed_count < total_count")
|
||||||
.orderByDesc(VoucherBatchEntity::getCreateTime);
|
.orderByDesc(PriceVoucherBatchConfig::getCreatedTime);
|
||||||
|
|
||||||
return voucherBatchMapper.selectOne(wrapper);
|
return voucherBatchMapper.selectOne(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VoucherBatchResp convertToResp(VoucherBatchEntity batch) {
|
private VoucherBatchResp convertToResp(PriceVoucherBatchConfig batch) {
|
||||||
VoucherBatchResp resp = new VoucherBatchResp();
|
VoucherBatchResp resp = new VoucherBatchResp();
|
||||||
BeanUtils.copyProperties(batch, resp);
|
BeanUtils.copyProperties(batch, resp);
|
||||||
|
|
@@ -1,19 +1,19 @@
|
|||||||
package com.ycwl.basic.voucher.service.impl;
|
package com.ycwl.basic.pricing.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.ycwl.basic.exception.BizException;
|
import com.ycwl.basic.exception.BizException;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherClaimReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||||
import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
||||||
import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||||
import com.ycwl.basic.voucher.entity.VoucherBatchEntity;
|
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||||
import com.ycwl.basic.voucher.entity.VoucherCodeEntity;
|
import com.ycwl.basic.pricing.entity.PriceVoucherCode;
|
||||||
import com.ycwl.basic.voucher.enums.VoucherCodeStatus;
|
import com.ycwl.basic.pricing.enums.VoucherCodeStatus;
|
||||||
import com.ycwl.basic.voucher.enums.VoucherDiscountType;
|
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||||
import com.ycwl.basic.voucher.mapper.VoucherBatchMapper;
|
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
||||||
import com.ycwl.basic.voucher.mapper.VoucherCodeMapper;
|
import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper;
|
||||||
import com.ycwl.basic.voucher.service.VoucherBatchService;
|
import com.ycwl.basic.pricing.service.VoucherBatchService;
|
||||||
import com.ycwl.basic.voucher.service.VoucherCodeService;
|
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -29,26 +29,26 @@ import java.util.UUID;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VoucherCodeServiceImpl implements VoucherCodeService {
|
public class VoucherCodeServiceImpl implements VoucherCodeService {
|
||||||
|
|
||||||
private final VoucherCodeMapper voucherCodeMapper;
|
private final PriceVoucherCodeMapper voucherCodeMapper;
|
||||||
private final VoucherBatchMapper voucherBatchMapper;
|
private final PriceVoucherBatchConfigMapper voucherBatchMapper;
|
||||||
private final VoucherBatchService voucherBatchService;
|
private final VoucherBatchService voucherBatchService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
|
public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
|
||||||
List<VoucherCodeEntity> codes = new ArrayList<>();
|
List<PriceVoucherCode> codes = new ArrayList<>();
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
VoucherCodeEntity code = new VoucherCodeEntity();
|
PriceVoucherCode code = new PriceVoucherCode();
|
||||||
code.setBatchId(batchId);
|
code.setBatchId(batchId);
|
||||||
code.setScenicId(scenicId);
|
code.setScenicId(scenicId);
|
||||||
code.setCode(generateVoucherCode());
|
code.setCode(generateVoucherCode());
|
||||||
code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode());
|
code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode());
|
||||||
code.setCreateTime(new Date());
|
code.setCreatedTime(new Date());
|
||||||
code.setDeleted(0);
|
code.setDeleted(0);
|
||||||
codes.add(code);
|
codes.add(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (VoucherCodeEntity code : codes) {
|
for (PriceVoucherCode code : codes) {
|
||||||
voucherCodeMapper.insert(code);
|
voucherCodeMapper.insert(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,12 +70,12 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
|||||||
throw new BizException(400, "该用户在此景区已领取过券码");
|
throw new BizException(400, "该用户在此景区已领取过券码");
|
||||||
}
|
}
|
||||||
|
|
||||||
VoucherBatchEntity batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId());
|
PriceVoucherBatchConfig batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId());
|
||||||
if (batch == null) {
|
if (batch == null) {
|
||||||
throw new BizException(400, "暂无可用券码批次");
|
throw new BizException(400, "暂无可用券码批次");
|
||||||
}
|
}
|
||||||
|
|
||||||
VoucherCodeEntity availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId());
|
PriceVoucherCode availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId());
|
||||||
if (availableCode == null) {
|
if (availableCode == null) {
|
||||||
throw new BizException(400, "券码已领完");
|
throw new BizException(400, "券码已领完");
|
||||||
}
|
}
|
||||||
@@ -93,25 +93,25 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) {
|
public Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) {
|
||||||
Page<VoucherCodeEntity> page = new Page<>(req.getPageNum(), req.getPageSize());
|
Page<PriceVoucherCode> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||||
|
|
||||||
LambdaQueryWrapper<VoucherCodeEntity> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(VoucherCodeEntity::getDeleted, 0)
|
wrapper.eq(PriceVoucherCode::getDeleted, 0)
|
||||||
.eq(req.getBatchId() != null, VoucherCodeEntity::getBatchId, req.getBatchId())
|
.eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId())
|
||||||
.eq(req.getScenicId() != null, VoucherCodeEntity::getScenicId, req.getScenicId())
|
.eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId())
|
||||||
.eq(req.getFaceId() != null, VoucherCodeEntity::getFaceId, req.getFaceId())
|
.eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId())
|
||||||
.eq(req.getStatus() != null, VoucherCodeEntity::getStatus, req.getStatus())
|
.eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus())
|
||||||
.like(StringUtils.hasText(req.getCode()), VoucherCodeEntity::getCode, req.getCode())
|
.like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode())
|
||||||
.orderByDesc(VoucherCodeEntity::getCreateTime);
|
.orderByDesc(PriceVoucherCode::getCreatedTime);
|
||||||
|
|
||||||
Page<VoucherCodeEntity> entityPage = voucherCodeMapper.selectPage(page, wrapper);
|
Page<PriceVoucherCode> entityPage = voucherCodeMapper.selectPage(page, wrapper);
|
||||||
|
|
||||||
Page<VoucherCodeResp> respPage = new Page<>();
|
Page<VoucherCodeResp> respPage = new Page<>();
|
||||||
BeanUtils.copyProperties(entityPage, respPage);
|
BeanUtils.copyProperties(entityPage, respPage);
|
||||||
|
|
||||||
List<VoucherCodeResp> respList = new ArrayList<>();
|
List<VoucherCodeResp> respList = new ArrayList<>();
|
||||||
for (VoucherCodeEntity code : entityPage.getRecords()) {
|
for (PriceVoucherCode code : entityPage.getRecords()) {
|
||||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(code.getBatchId());
|
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId());
|
||||||
respList.add(convertToResp(code, batch));
|
respList.add(convertToResp(code, batch));
|
||||||
}
|
}
|
||||||
respPage.setRecords(respList);
|
respPage.setRecords(respList);
|
||||||
@@ -121,16 +121,16 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<VoucherCodeResp> getMyVoucherCodes(Long faceId) {
|
public List<VoucherCodeResp> getMyVoucherCodes(Long faceId) {
|
||||||
LambdaQueryWrapper<VoucherCodeEntity> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(VoucherCodeEntity::getFaceId, faceId)
|
wrapper.eq(PriceVoucherCode::getFaceId, faceId)
|
||||||
.eq(VoucherCodeEntity::getDeleted, 0)
|
.eq(PriceVoucherCode::getDeleted, 0)
|
||||||
.orderByDesc(VoucherCodeEntity::getClaimedTime);
|
.orderByDesc(PriceVoucherCode::getClaimedTime);
|
||||||
|
|
||||||
List<VoucherCodeEntity> codes = voucherCodeMapper.selectList(wrapper);
|
List<PriceVoucherCode> codes = voucherCodeMapper.selectList(wrapper);
|
||||||
|
|
||||||
List<VoucherCodeResp> respList = new ArrayList<>();
|
List<VoucherCodeResp> respList = new ArrayList<>();
|
||||||
for (VoucherCodeEntity code : codes) {
|
for (PriceVoucherCode code : codes) {
|
||||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(code.getBatchId());
|
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId());
|
||||||
respList.add(convertToResp(code, batch));
|
respList.add(convertToResp(code, batch));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void markCodeAsUsed(Long codeId, String remark) {
|
public void markCodeAsUsed(Long codeId, String remark) {
|
||||||
VoucherCodeEntity code = voucherCodeMapper.selectById(codeId);
|
PriceVoucherCode code = voucherCodeMapper.selectById(codeId);
|
||||||
if (code == null || code.getDeleted() == 1) {
|
if (code == null || code.getDeleted() == 1) {
|
||||||
throw new BizException(404, "券码不存在");
|
throw new BizException(404, "券码不存在");
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
|||||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
|
return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
private VoucherCodeResp convertToResp(VoucherCodeEntity code, VoucherBatchEntity batch) {
|
private VoucherCodeResp convertToResp(PriceVoucherCode code, PriceVoucherBatchConfig batch) {
|
||||||
VoucherCodeResp resp = new VoucherCodeResp();
|
VoucherCodeResp resp = new VoucherCodeResp();
|
||||||
BeanUtils.copyProperties(code, resp);
|
BeanUtils.copyProperties(code, resp);
|
||||||
|
|
@@ -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<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
|
||||||
|
List<DiscountInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,223 @@
|
|||||||
|
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<VoucherInfo> getAvailableVouchers(Long faceId, Long scenicId) {
|
||||||
|
if (faceId == null || scenicId == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByFaceIdAndScenicId(faceId, scenicId);
|
||||||
|
List<VoucherInfo> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<VoucherInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,179 +0,0 @@
|
|||||||
# 券码核销功能模块
|
|
||||||
|
|
||||||
本模块实现景区券码的批量创建、分发和核销管理功能。支持全场免费、商品降价、商品打折三种优惠模式,确保每个用户在每个景区只能领取一次券码。
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
|
|
||||||
- **批量创建券码**:管理员可创建券码批次,自动生成指定数量的唯一券码
|
|
||||||
- **精准分发控制**:通过景区ID、推客ID、用户faceId进行精准投放
|
|
||||||
- **三种优惠模式**:全场免费、商品降价、商品打折
|
|
||||||
- **唯一性保证**:同一用户在同一景区只能领取一次券码
|
|
||||||
- **完整管理功能**:批次管理、券码查询、使用统计、手动核销
|
|
||||||
|
|
||||||
## 数据库表结构
|
|
||||||
|
|
||||||
### 券码批次表 (voucher_batch)
|
|
||||||
```sql
|
|
||||||
CREATE TABLE voucher_batch (
|
|
||||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
batch_name VARCHAR(100) NOT NULL COMMENT '券码批次名称',
|
|
||||||
scenic_id BIGINT NOT NULL COMMENT '景区ID',
|
|
||||||
broker_id BIGINT NOT NULL COMMENT '推客ID',
|
|
||||||
discount_type TINYINT NOT NULL COMMENT '优惠类型:0=全场免费,1=商品降价,2=商品打折',
|
|
||||||
discount_value DECIMAL(10,2) COMMENT '优惠值(降价金额或折扣百分比)',
|
|
||||||
total_count INT NOT NULL COMMENT '总券码数量',
|
|
||||||
used_count INT DEFAULT 0 COMMENT '已使用数量',
|
|
||||||
claimed_count INT DEFAULT 0 COMMENT '已领取数量',
|
|
||||||
status TINYINT DEFAULT 1 COMMENT '状态:0=禁用,1=启用',
|
|
||||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
create_by BIGINT COMMENT '创建人ID',
|
|
||||||
deleted TINYINT DEFAULT 0,
|
|
||||||
deleted_at DATETIME
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 券码表 (voucher_code)
|
|
||||||
```sql
|
|
||||||
CREATE TABLE voucher_code (
|
|
||||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
batch_id BIGINT NOT NULL COMMENT '批次ID',
|
|
||||||
scenic_id BIGINT NOT NULL COMMENT '景区ID',
|
|
||||||
code VARCHAR(32) NOT NULL UNIQUE COMMENT '券码',
|
|
||||||
status TINYINT DEFAULT 0 COMMENT '状态:0=未领取,1=已领取未使用,2=已使用',
|
|
||||||
face_id BIGINT COMMENT '领取人faceId',
|
|
||||||
claimed_time DATETIME COMMENT '领取时间',
|
|
||||||
used_time DATETIME COMMENT '使用时间',
|
|
||||||
remark VARCHAR(500) COMMENT '使用备注',
|
|
||||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
deleted TINYINT DEFAULT 0,
|
|
||||||
deleted_at DATETIME
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 包结构说明
|
|
||||||
|
|
||||||
```
|
|
||||||
com.ycwl.basic.voucher/
|
|
||||||
├── controller/
|
|
||||||
│ └── VoucherController.java # 控制器:PC端管理和移动端用户接口
|
|
||||||
├── service/
|
|
||||||
│ ├── VoucherBatchService.java # 券码批次服务接口
|
|
||||||
│ ├── VoucherCodeService.java # 券码服务接口
|
|
||||||
│ └── impl/
|
|
||||||
│ ├── VoucherBatchServiceImpl.java # 券码批次服务实现
|
|
||||||
│ └── VoucherCodeServiceImpl.java # 券码服务实现
|
|
||||||
├── mapper/
|
|
||||||
│ ├── VoucherBatchMapper.java # 券码批次数据访问
|
|
||||||
│ └── VoucherCodeMapper.java # 券码数据访问
|
|
||||||
├── entity/
|
|
||||||
│ ├── VoucherBatchEntity.java # 券码批次实体
|
|
||||||
│ └── VoucherCodeEntity.java # 券码实体
|
|
||||||
├── dto/
|
|
||||||
│ ├── req/ # 请求DTO
|
|
||||||
│ │ ├── VoucherBatchCreateReq.java # 创建批次请求
|
|
||||||
│ │ ├── VoucherBatchQueryReq.java # 批次查询请求
|
|
||||||
│ │ ├── VoucherCodeQueryReq.java # 券码查询请求
|
|
||||||
│ │ └── VoucherClaimReq.java # 券码领取请求
|
|
||||||
│ └── resp/ # 响应DTO
|
|
||||||
│ ├── VoucherBatchResp.java # 批次响应
|
|
||||||
│ ├── VoucherCodeResp.java # 券码响应
|
|
||||||
│ └── VoucherBatchStatsResp.java # 批次统计响应
|
|
||||||
└── enums/
|
|
||||||
├── VoucherDiscountType.java # 优惠类型枚举
|
|
||||||
└── VoucherCodeStatus.java # 券码状态枚举
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心业务逻辑
|
|
||||||
|
|
||||||
### 1. 券码生成规则
|
|
||||||
- 使用UUID生成8位大写字母数字组合
|
|
||||||
- 确保每个券码在系统中唯一
|
|
||||||
- 券码初始状态为"未领取"
|
|
||||||
|
|
||||||
### 2. 领取验证逻辑
|
|
||||||
```java
|
|
||||||
// 核心验证:同一faceId在同一scenicId中只能领取一次
|
|
||||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
|
||||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
|
||||||
return count == 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 优惠类型说明
|
|
||||||
- **全场免费(0)**:所有商品免费,discountValue可为空
|
|
||||||
- **商品降价(1)**:每个商品减免固定金额,discountValue为减免金额
|
|
||||||
- **商品打折(2)**:每个商品按百分比打折,discountValue为折扣百分比
|
|
||||||
|
|
||||||
### 4. 状态流转
|
|
||||||
券码状态:未领取(0) → 已领取未使用(1) → 已使用(2)
|
|
||||||
|
|
||||||
## 主要接口说明
|
|
||||||
|
|
||||||
### PC端管理接口
|
|
||||||
- `POST /api/voucher/batch/create` - 创建券码批次
|
|
||||||
- `POST /api/voucher/batch/list` - 批次列表查询
|
|
||||||
- `GET /api/voucher/batch/{id}` - 批次详情
|
|
||||||
- `GET /api/voucher/batch/{id}/stats` - 批次统计
|
|
||||||
- `PUT /api/voucher/batch/{id}/status` - 启用/禁用批次
|
|
||||||
- `POST /api/voucher/codes` - 券码列表查询
|
|
||||||
- `PUT /api/voucher/code/{id}/use` - 手动标记券码已使用
|
|
||||||
- `GET /api/voucher/scenic/{scenicId}/users` - 查看景区下用户领取情况
|
|
||||||
|
|
||||||
### 移动端用户接口
|
|
||||||
- `POST /api/voucher/mobile/claim` - 领取券码(一步到位)
|
|
||||||
- `GET /api/voucher/mobile/my-codes` - 我的券码列表
|
|
||||||
|
|
||||||
## 关键技术特点
|
|
||||||
|
|
||||||
### 1. 事务管理
|
|
||||||
- 使用`@Transactional`确保券码领取操作的原子性
|
|
||||||
- 批次创建和券码生成在同一事务中完成
|
|
||||||
|
|
||||||
### 2. 并发控制
|
|
||||||
- 通过应用层验证避免数据库唯一约束冲突
|
|
||||||
- 使用数据库索引优化查询性能
|
|
||||||
|
|
||||||
### 3. 参数验证
|
|
||||||
- 在Service层进行手动参数校验
|
|
||||||
- 使用BizException统一错误处理
|
|
||||||
|
|
||||||
### 4. 数据统计
|
|
||||||
- 实时更新批次的已领取数量和已使用数量
|
|
||||||
- 提供详细的使用统计和分析数据
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 创建券码批次
|
|
||||||
```java
|
|
||||||
VoucherBatchCreateReq req = new VoucherBatchCreateReq();
|
|
||||||
req.setBatchName("春节特惠券");
|
|
||||||
req.setScenicId(1001L);
|
|
||||||
req.setBrokerId(2001L);
|
|
||||||
req.setDiscountType(1); // 商品降价
|
|
||||||
req.setDiscountValue(new BigDecimal("10.00"));
|
|
||||||
req.setTotalCount(1000);
|
|
||||||
|
|
||||||
Long batchId = voucherBatchService.createBatch(req);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 用户领取券码
|
|
||||||
```java
|
|
||||||
VoucherClaimReq req = new VoucherClaimReq();
|
|
||||||
req.setScenicId(1001L);
|
|
||||||
req.setBrokerId(2001L);
|
|
||||||
req.setFaceId(3001L);
|
|
||||||
|
|
||||||
VoucherCodeResp result = voucherCodeService.claimVoucher(req);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **唯一性限制**:同一个faceId在同一个scenicId中只能领取一次券码
|
|
||||||
2. **批次状态**:只有启用状态的批次才能领取券码
|
|
||||||
3. **券码数量**:确保批次有可用券码才能成功领取
|
|
||||||
4. **优惠值验证**:除全场免费外,其他优惠类型必须设置优惠值
|
|
||||||
5. **删除机制**:使用逻辑删除,deleted字段标记删除状态
|
|
||||||
|
|
||||||
## 扩展说明
|
|
||||||
|
|
||||||
本模块设计为独立功能模块,不依赖支付系统,为后续接入其他优惠策略预留了扩展空间。所有接口都提供了完整的错误处理和参数验证,确保系统的稳定性和可维护性。
|
|
@@ -1,30 +0,0 @@
|
|||||||
package com.ycwl.basic.voucher.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
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("voucher_batch")
|
|
||||||
public class VoucherBatchEntity {
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private String batchName;
|
|
||||||
private Long scenicId;
|
|
||||||
private Long brokerId;
|
|
||||||
private Integer discountType;
|
|
||||||
private BigDecimal discountValue;
|
|
||||||
private Integer totalCount;
|
|
||||||
private Integer usedCount;
|
|
||||||
private Integer claimedCount;
|
|
||||||
private Integer status;
|
|
||||||
private Date createTime;
|
|
||||||
private Long createBy;
|
|
||||||
private Integer deleted;
|
|
||||||
private Date deletedAt;
|
|
||||||
}
|
|
@@ -1,27 +0,0 @@
|
|||||||
package com.ycwl.basic.voucher.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("voucher_code")
|
|
||||||
public class VoucherCodeEntity {
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private Long batchId;
|
|
||||||
private Long scenicId;
|
|
||||||
private String code;
|
|
||||||
private Integer status;
|
|
||||||
private Long faceId;
|
|
||||||
private Date claimedTime;
|
|
||||||
private Date usedTime;
|
|
||||||
private String remark;
|
|
||||||
private Date createTime;
|
|
||||||
private Integer deleted;
|
|
||||||
private Date deletedAt;
|
|
||||||
}
|
|
@@ -1,27 +0,0 @@
|
|||||||
package com.ycwl.basic.voucher.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;
|
|
||||||
|
|
||||||
public static VoucherCodeStatus getByCode(Integer code) {
|
|
||||||
if (code == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (VoucherCodeStatus status : values()) {
|
|
||||||
if (status.getCode().equals(code)) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
package com.ycwl.basic.voucher.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.ycwl.basic.voucher.entity.VoucherBatchEntity;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface VoucherBatchMapper extends BaseMapper<VoucherBatchEntity> {
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
package com.ycwl.basic.voucher.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.ycwl.basic.voucher.entity.VoucherCodeEntity;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface VoucherCodeMapper extends BaseMapper<VoucherCodeEntity> {
|
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM voucher_code WHERE scenic_id = #{scenicId} AND face_id = #{faceId} AND status != 0 AND deleted = 0")
|
|
||||||
Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId);
|
|
||||||
|
|
||||||
@Select("SELECT * FROM voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1")
|
|
||||||
VoucherCodeEntity findFirstAvailableByBatchId(@Param("batchId") Long batchId);
|
|
||||||
}
|
|
Reference in New Issue
Block a user