You've already forked FrameTour-BE
Merge branch 'price_inquery'
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
package com.ycwl.basic.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfiguration {
|
||||
|
||||
@@ -13,6 +19,16 @@ public class JacksonConfiguration {
|
||||
return builder -> {
|
||||
// 把 Long 类型序列化为 String
|
||||
builder.serializerByType(Long.class, ToStringSerializer.instance);
|
||||
|
||||
// 添加 JavaTimeModule 以支持 Java 8 时间类型
|
||||
builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JavaTimeModule javaTimeModule() {
|
||||
return new JavaTimeModule();
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||
import com.ycwl.basic.pricing.service.VoucherPrintService;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/voucher/v1")
|
||||
public class AppVoucherController {
|
||||
|
||||
@Autowired
|
||||
private VoucherPrintService voucherPrintService;
|
||||
@Autowired
|
||||
private VoucherCodeService voucherCodeService;
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
/**
|
||||
* 打印小票
|
||||
* @param request 打印请求
|
||||
* @return 打印结果
|
||||
*/
|
||||
@PostMapping("/print")
|
||||
public ApiResponse<VoucherPrintResp> printVoucherTicket(@RequestBody VoucherPrintReq request) {
|
||||
log.info("收到打印小票请求: faceId={}, brokerId={}, scenicId={}",
|
||||
request.getFaceId(), request.getBrokerId(), request.getScenicId());
|
||||
|
||||
VoucherPrintResp response = voucherPrintService.printVoucherTicket(request);
|
||||
|
||||
log.info("打印小票完成: code={}, voucherCode={}, status={}",
|
||||
response.getCode(), response.getVoucherCode(), response.getPrintStatus());
|
||||
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
@GetMapping("/printed")
|
||||
public ApiResponse<VoucherPrintResp> queryPrintedVoucher(
|
||||
@RequestParam Long faceId
|
||||
) {
|
||||
return ApiResponse.success(voucherPrintService.queryPrintedVoucher(faceId));
|
||||
}
|
||||
|
||||
@PostMapping("/claim")
|
||||
public ApiResponse<VoucherCodeResp> claimVoucher(@RequestBody VoucherClaimReq req) {
|
||||
FaceEntity face = faceRepository.getFace(req.getFaceId());
|
||||
if (face == null) {
|
||||
throw new BaseException("请选择人脸");
|
||||
}
|
||||
if (!face.getMemberId().equals(Long.valueOf(BaseContextHandler.getUserId()))) {
|
||||
throw new BaseException("自动领取失败");
|
||||
}
|
||||
req.setScenicId(face.getScenicId());
|
||||
VoucherCodeResp result = voucherCodeService.claimVoucher(req);
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
}
|
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验证统计数据准确性
|
@@ -0,0 +1,233 @@
|
||||
package com.ycwl.basic.pricing.controller;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
|
||||
import com.ycwl.basic.pricing.enums.CouponStatus;
|
||||
import com.ycwl.basic.pricing.service.ICouponManagementService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 优惠券管理控制器(管理端)
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/pricing/admin/coupons")
|
||||
@RequiredArgsConstructor
|
||||
public class CouponManagementController {
|
||||
|
||||
private final ICouponManagementService couponManagementService;
|
||||
|
||||
// ==================== 优惠券配置管理 ====================
|
||||
|
||||
/**
|
||||
* 创建优惠券配置
|
||||
*/
|
||||
@PostMapping("/configs")
|
||||
public ApiResponse<Long> createCouponConfig(@RequestBody PriceCouponConfig config) {
|
||||
log.info("创建优惠券配置: {}", config.getCouponName());
|
||||
Long id = couponManagementService.createCouponConfig(config);
|
||||
return ApiResponse.success(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新优惠券配置
|
||||
*/
|
||||
@PutMapping("/configs/{id}")
|
||||
public ApiResponse<Boolean> updateCouponConfig(@PathVariable Long id, @RequestBody PriceCouponConfig config) {
|
||||
log.info("更新优惠券配置: id={}, name={}", id, config.getCouponName());
|
||||
config.setId(id);
|
||||
boolean success = couponManagementService.updateCouponConfig(config);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除优惠券配置
|
||||
*/
|
||||
@DeleteMapping("/configs/{id}")
|
||||
public ApiResponse<Boolean> deleteCouponConfig(@PathVariable Long id) {
|
||||
log.info("删除优惠券配置: id={}", id);
|
||||
boolean success = couponManagementService.deleteCouponConfig(id);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用优惠券配置
|
||||
*/
|
||||
@PutMapping("/configs/{id}/status")
|
||||
public ApiResponse<Boolean> updateCouponConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) {
|
||||
log.info("修改优惠券配置状态: id={}, isActive={}", id, isActive);
|
||||
boolean success = couponManagementService.updateCouponConfigStatus(id, isActive);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有优惠券配置(包含禁用的)
|
||||
*/
|
||||
@GetMapping("/configs")
|
||||
public ApiResponse<List<PriceCouponConfig>> getAllCouponConfigs() {
|
||||
log.info("管理端获取所有优惠券配置");
|
||||
List<PriceCouponConfig> configs = couponManagementService.getAllCouponConfigs();
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询优惠券配置
|
||||
*/
|
||||
@GetMapping("/configs/page")
|
||||
public ApiResponse<PageInfo<PriceCouponConfig>> getCouponConfigsPage(
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) Boolean isActive,
|
||||
@RequestParam(required = false) String couponName,
|
||||
@RequestParam(required = false) String scenicId) {
|
||||
log.info("分页查询优惠券配置: pageNum={}, pageSize={}, isActive={}, couponName={}, scenicId={}",
|
||||
pageNum, pageSize, isActive, couponName, scenicId);
|
||||
PageInfo<PriceCouponConfig> pageInfo = couponManagementService.getCouponConfigsPage(
|
||||
pageNum, pageSize, isActive, couponName, scenicId);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态查询优惠券配置
|
||||
*/
|
||||
@GetMapping("/configs/status/{isActive}")
|
||||
public ApiResponse<List<PriceCouponConfig>> getCouponConfigsByStatus(@PathVariable Boolean isActive) {
|
||||
log.info("根据状态查询优惠券配置: {}", isActive);
|
||||
List<PriceCouponConfig> configs = couponManagementService.getCouponConfigsByStatus(isActive);
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询优惠券配置
|
||||
*/
|
||||
@GetMapping("/configs/{id}")
|
||||
public ApiResponse<PriceCouponConfig> getCouponConfigById(@PathVariable Long id) {
|
||||
log.info("根据ID查询优惠券配置: {}", id);
|
||||
PriceCouponConfig config = couponManagementService.getCouponConfigById(id);
|
||||
return ApiResponse.success(config);
|
||||
}
|
||||
|
||||
// ==================== 优惠券领取记录查询 ====================
|
||||
|
||||
/**
|
||||
* 查询所有优惠券领取记录
|
||||
*/
|
||||
@GetMapping("/claim-records")
|
||||
public ApiResponse<List<PriceCouponClaimRecord>> getAllClaimRecords() {
|
||||
log.info("查询所有优惠券领取记录");
|
||||
List<PriceCouponClaimRecord> records = couponManagementService.getAllClaimRecords();
|
||||
return ApiResponse.success(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询优惠券领取记录
|
||||
*/
|
||||
@GetMapping("/claim-records/page")
|
||||
public ApiResponse<PageInfo<PriceCouponClaimRecord>> getClaimRecordsPage(
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) Long userId,
|
||||
@RequestParam(required = false) Long couponId,
|
||||
@RequestParam(required = false) CouponStatus status,
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime,
|
||||
@RequestParam(required = false) String scenicId) {
|
||||
log.info("分页查询优惠券领取记录: pageNum={}, pageSize={}, userId={}, couponId={}, status={}, startTime={}, endTime={}, scenicId={}",
|
||||
pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId);
|
||||
PageInfo<PriceCouponClaimRecord> pageInfo = couponManagementService.getClaimRecordsPage(
|
||||
pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId);
|
||||
return ApiResponse.success(pageInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询优惠券领取记录
|
||||
*/
|
||||
@GetMapping("/claim-records/user/{userId}")
|
||||
public ApiResponse<List<PriceCouponClaimRecord>> getClaimRecordsByUserId(@PathVariable Long userId) {
|
||||
log.info("根据用户ID查询优惠券领取记录: {}", userId);
|
||||
List<PriceCouponClaimRecord> records = couponManagementService.getClaimRecordsByUserId(userId);
|
||||
return ApiResponse.success(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据优惠券ID查询领取记录
|
||||
*/
|
||||
@GetMapping("/claim-records/coupon/{couponId}")
|
||||
public ApiResponse<List<PriceCouponClaimRecord>> getClaimRecordsByCouponId(@PathVariable Long couponId) {
|
||||
log.info("根据优惠券ID查询领取记录: {}", couponId);
|
||||
List<PriceCouponClaimRecord> records = couponManagementService.getClaimRecordsByCouponId(couponId);
|
||||
return ApiResponse.success(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态查询领取记录
|
||||
*/
|
||||
@GetMapping("/claim-records/status/{status}")
|
||||
public ApiResponse<List<PriceCouponClaimRecord>> getClaimRecordsByStatus(@PathVariable CouponStatus status) {
|
||||
log.info("根据状态查询领取记录: {}", status);
|
||||
List<PriceCouponClaimRecord> records = couponManagementService.getClaimRecordsByStatus(status);
|
||||
return ApiResponse.success(records);
|
||||
}
|
||||
|
||||
// ==================== 统计功能 ====================
|
||||
|
||||
/**
|
||||
* 查询优惠券使用统计
|
||||
*/
|
||||
@GetMapping("/stats/{couponId}")
|
||||
public ApiResponse<Map<String, Object>> getCouponUsageStats(@PathVariable Long couponId) {
|
||||
log.info("查询优惠券使用统计: {}", couponId);
|
||||
Map<String, Object> stats = couponManagementService.getCouponUsageStats(couponId);
|
||||
return ApiResponse.success(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询优惠券详细统计
|
||||
*/
|
||||
@GetMapping("/stats/{couponId}/detail")
|
||||
public ApiResponse<Map<String, Object>> getCouponDetailStats(@PathVariable Long couponId) {
|
||||
log.info("查询优惠券详细统计: {}", couponId);
|
||||
Map<String, Object> stats = couponManagementService.getCouponDetailStats(couponId);
|
||||
return ApiResponse.success(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询时间范围内的统计数据
|
||||
*/
|
||||
@GetMapping("/stats/period")
|
||||
public ApiResponse<Map<String, Object>> getPeriodStats(
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate,
|
||||
@RequestParam(required = false) String scenicId) {
|
||||
log.info("查询时间范围统计: startDate={}, endDate={}, scenicId={}", startDate, endDate, scenicId);
|
||||
Map<String, Object> stats = couponManagementService.getPeriodStats(startDate, endDate, scenicId);
|
||||
return ApiResponse.success(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询景区优惠券统计
|
||||
*/
|
||||
@GetMapping("/stats/scenic/{scenicId}")
|
||||
public ApiResponse<Map<String, Object>> getScenicCouponStats(@PathVariable String scenicId) {
|
||||
log.info("查询景区优惠券统计: scenicId={}", scenicId);
|
||||
Map<String, Object> stats = couponManagementService.getScenicCouponStats(scenicId);
|
||||
return ApiResponse.success(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有优惠券的使用统计概览
|
||||
*/
|
||||
@GetMapping("/stats/overview")
|
||||
public ApiResponse<List<Map<String, Object>>> getAllCouponUsageOverview() {
|
||||
log.info("查询所有优惠券使用统计概览");
|
||||
List<Map<String, Object>> overview = couponManagementService.getAllCouponUsageOverview();
|
||||
return ApiResponse.success(overview);
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
package com.ycwl.basic.pricing.controller;
|
||||
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.pricing.dto.*;
|
||||
import com.ycwl.basic.pricing.service.ICouponService;
|
||||
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 价格计算控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/pricing")
|
||||
@RequiredArgsConstructor
|
||||
public class PriceCalculationController {
|
||||
|
||||
private final IPriceCalculationService priceCalculationService;
|
||||
private final ICouponService couponService;
|
||||
|
||||
/**
|
||||
* 计算商品价格
|
||||
*/
|
||||
@PostMapping("/calculate")
|
||||
public ApiResponse<PriceCalculationResult> calculatePrice(@RequestBody PriceCalculationRequest request) {
|
||||
log.info("价格计算请求: userId={}, products={}", request.getUserId(), request.getProducts().size());
|
||||
|
||||
PriceCalculationResult result = priceCalculationService.calculatePrice(request);
|
||||
|
||||
log.info("价格计算完成: originalAmount={}, finalAmount={}, usedCoupon={}",
|
||||
result.getOriginalAmount(), result.getFinalAmount(),
|
||||
result.getUsedCoupon() != null ? result.getUsedCoupon().getCouponId() : null);
|
||||
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用优惠券
|
||||
* 只能通过代码处理,不能通过接口调用
|
||||
*/
|
||||
@PostMapping("/coupons/use")
|
||||
@Deprecated
|
||||
public ApiResponse<CouponUseResult> useCoupon(@RequestBody CouponUseRequest request) {
|
||||
// log.info("优惠券使用请求: userId={}, couponId={}, orderId={}",
|
||||
// request.getUserId(), request.getCouponId(), request.getOrderId());
|
||||
//
|
||||
// CouponUseResult result = couponService.useCoupon(request);
|
||||
//
|
||||
// log.info("优惠券使用成功: couponId={}, discountAmount={}",
|
||||
// result.getCouponId(), result.getDiscountAmount());
|
||||
//
|
||||
// ApiResponse<CouponUseResult> response = ApiResponse.success(result);
|
||||
// response.setMsg("优惠券使用成功");
|
||||
// return response;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户可用优惠券
|
||||
*/
|
||||
@GetMapping("/coupons/my-coupons")
|
||||
public ApiResponse<List<CouponInfo>> getUserCoupons(@RequestParam Long userId) {
|
||||
log.info("查询用户可用优惠券: userId={}", userId);
|
||||
|
||||
List<CouponInfo> coupons = couponService.getUserAvailableCoupons(userId);
|
||||
|
||||
log.info("用户可用优惠券数量: {}", coupons.size());
|
||||
|
||||
return ApiResponse.success(coupons);
|
||||
}
|
||||
}
|
@@ -0,0 +1,300 @@
|
||||
package com.ycwl.basic.pricing.controller;
|
||||
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.pricing.entity.PriceProductConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceBundleConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
||||
import com.ycwl.basic.pricing.service.IProductConfigService;
|
||||
import com.ycwl.basic.pricing.service.IPriceBundleService;
|
||||
import com.ycwl.basic.pricing.service.IPricingManagementService;
|
||||
import com.ycwl.basic.pricing.service.ICouponManagementService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 价格配置管理控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/pricing/config")
|
||||
@RequiredArgsConstructor
|
||||
public class PricingConfigController {
|
||||
|
||||
private final IProductConfigService productConfigService;
|
||||
private final IPriceBundleService bundleService;
|
||||
private final IPricingManagementService managementService;
|
||||
private final ICouponManagementService couponManagementService;
|
||||
|
||||
|
||||
// ==================== 查询API ====================
|
||||
|
||||
/**
|
||||
* 获取所有商品配置
|
||||
*/
|
||||
@GetMapping("/products")
|
||||
public ApiResponse<List<PriceProductConfig>> getAllProductConfigs() {
|
||||
log.info("获取所有商品配置");
|
||||
List<PriceProductConfig> configs = productConfigService.getAllProductConfigs();
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商品类型获取阶梯配置
|
||||
*/
|
||||
@GetMapping("/tiers/{productType}")
|
||||
public ApiResponse<List<PriceTierConfig>> getTierConfigs(@PathVariable String productType) {
|
||||
log.info("根据商品类型获取阶梯配置: {}", productType);
|
||||
List<PriceTierConfig> configs = productConfigService.getTierConfigs(productType);
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID获取阶梯配置
|
||||
*/
|
||||
@GetMapping("/tiers/{productType}/{productId}")
|
||||
public ApiResponse<List<PriceTierConfig>> getTierConfigs(@PathVariable String productType,
|
||||
@PathVariable String productId) {
|
||||
log.info("根据商品类型和ID获取阶梯配置: {}, {}", productType, productId);
|
||||
List<PriceTierConfig> configs = productConfigService.getTierConfigs(productType, productId);
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID获取具体配置
|
||||
*/
|
||||
@GetMapping("/products/{productType}/{productId}")
|
||||
public ApiResponse<PriceProductConfig> getProductConfig(@PathVariable String productType,
|
||||
@PathVariable String productId) {
|
||||
log.info("根据商品类型和ID获取商品配置: {}, {}", productType, productId);
|
||||
PriceProductConfig config = productConfigService.getProductConfig(productType, productId);
|
||||
return ApiResponse.success(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有阶梯配置
|
||||
*/
|
||||
@GetMapping("/tiers")
|
||||
public ApiResponse<List<PriceTierConfig>> getAllTierConfigs() {
|
||||
log.info("获取所有阶梯配置");
|
||||
List<PriceTierConfig> configs = productConfigService.getAllTierConfigs();
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有一口价配置
|
||||
*/
|
||||
@GetMapping("/bundles")
|
||||
public ApiResponse<List<PriceBundleConfig>> getAllBundleConfigs() {
|
||||
log.info("获取所有一口价配置");
|
||||
List<PriceBundleConfig> configs = bundleService.getAllBundles();
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
|
||||
// ==================== 配置管理API(手动处理时间) ====================
|
||||
|
||||
/**
|
||||
* 创建商品配置
|
||||
*/
|
||||
@PostMapping("/products")
|
||||
public ApiResponse<Long> createProductConfig(@RequestBody PriceProductConfig config) {
|
||||
log.info("创建商品配置: {}", config.getProductName());
|
||||
Long id = managementService.createProductConfig(config);
|
||||
return ApiResponse.success(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品配置
|
||||
*/
|
||||
@PutMapping("/products/{id}")
|
||||
public ApiResponse<Boolean> updateProductConfig(@PathVariable Long id, @RequestBody PriceProductConfig config) {
|
||||
log.info("更新商品配置: id={}, name={}", id, config.getProductName());
|
||||
config.setId(id);
|
||||
boolean success = managementService.updateProductConfig(config);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建阶梯定价配置
|
||||
*/
|
||||
@PostMapping("/tiers")
|
||||
public ApiResponse<Long> createTierConfig(@RequestBody PriceTierConfig config) {
|
||||
log.info("创建阶梯定价配置: productType={}, price={}", config.getProductType(), config.getPrice());
|
||||
Long id = managementService.createTierConfig(config);
|
||||
return ApiResponse.success(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新阶梯定价配置
|
||||
*/
|
||||
@PutMapping("/tiers/{id}")
|
||||
public ApiResponse<Boolean> updateTierConfig(@PathVariable Long id, @RequestBody PriceTierConfig config) {
|
||||
log.info("更新阶梯定价配置: id={}", id);
|
||||
config.setId(id);
|
||||
boolean success = managementService.updateTierConfig(config);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一口价配置
|
||||
*/
|
||||
@PostMapping("/bundles")
|
||||
public ApiResponse<Long> createBundleConfig(@RequestBody PriceBundleConfig config) {
|
||||
log.info("创建一口价配置: {}", config.getBundleName());
|
||||
Long id = managementService.createBundleConfig(config);
|
||||
return ApiResponse.success(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新一口价配置
|
||||
*/
|
||||
@PutMapping("/bundles/{id}")
|
||||
public ApiResponse<Boolean> updateBundleConfig(@PathVariable Long id, @RequestBody PriceBundleConfig config) {
|
||||
log.info("更新一口价配置: id={}, name={}", id, config.getBundleName());
|
||||
config.setId(id);
|
||||
boolean success = managementService.updateBundleConfig(config);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
// ==================== 启用/禁用API ====================
|
||||
|
||||
/**
|
||||
* 启用/禁用商品配置
|
||||
*/
|
||||
@PutMapping("/products/{id}/status")
|
||||
public ApiResponse<Boolean> updateProductConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) {
|
||||
log.info("修改商品配置状态: id={}, isActive={}", id, isActive);
|
||||
boolean success = managementService.updateProductConfigStatus(id, isActive);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用阶梯配置
|
||||
*/
|
||||
@PutMapping("/tiers/{id}/status")
|
||||
public ApiResponse<Boolean> updateTierConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) {
|
||||
log.info("修改阶梯配置状态: id={}, isActive={}", id, isActive);
|
||||
boolean success = managementService.updateTierConfigStatus(id, isActive);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用一口价配置
|
||||
*/
|
||||
@PutMapping("/bundles/{id}/status")
|
||||
public ApiResponse<Boolean> updateBundleConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) {
|
||||
log.info("修改一口价配置状态: id={}, isActive={}", id, isActive);
|
||||
boolean success = managementService.updateBundleConfigStatus(id, isActive);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
// ==================== 删除API ====================
|
||||
|
||||
/**
|
||||
* 删除商品配置
|
||||
*/
|
||||
@DeleteMapping("/products/{id}")
|
||||
public ApiResponse<Boolean> deleteProductConfig(@PathVariable Long id) {
|
||||
log.info("删除商品配置: id={}", id);
|
||||
boolean success = managementService.deleteProductConfig(id);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除阶梯配置
|
||||
*/
|
||||
@DeleteMapping("/tiers/{id}")
|
||||
public ApiResponse<Boolean> deleteTierConfig(@PathVariable Long id) {
|
||||
log.info("删除阶梯配置: id={}", id);
|
||||
boolean success = managementService.deleteTierConfig(id);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一口价配置
|
||||
*/
|
||||
@DeleteMapping("/bundles/{id}")
|
||||
public ApiResponse<Boolean> deleteBundleConfig(@PathVariable Long id) {
|
||||
log.info("删除一口价配置: id={}", id);
|
||||
boolean success = managementService.deleteBundleConfig(id);
|
||||
return ApiResponse.success(success);
|
||||
}
|
||||
|
||||
// ==================== 管理端接口(包含禁用的配置) ====================
|
||||
|
||||
/**
|
||||
* 管理端:获取所有商品配置(包含禁用的)
|
||||
*/
|
||||
@GetMapping("/admin/products")
|
||||
public ApiResponse<List<PriceProductConfig>> getAllProductConfigsForAdmin() {
|
||||
log.info("管理端获取所有商品配置");
|
||||
List<PriceProductConfig> configs = productConfigService.getAllProductConfigsForAdmin();
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端:获取所有阶梯配置(包含禁用的)
|
||||
*/
|
||||
@GetMapping("/admin/tiers")
|
||||
public ApiResponse<List<PriceTierConfig>> getAllTierConfigsForAdmin() {
|
||||
log.info("管理端获取所有阶梯配置");
|
||||
List<PriceTierConfig> configs = productConfigService.getAllTierConfigsForAdmin();
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端:根据商品类型获取阶梯配置(包含禁用的)
|
||||
*/
|
||||
@GetMapping("/admin/tiers/{productType}")
|
||||
public ApiResponse<List<PriceTierConfig>> getTierConfigsForAdmin(@PathVariable String productType) {
|
||||
log.info("管理端根据商品类型获取阶梯配置: {}", productType);
|
||||
List<PriceTierConfig> configs = productConfigService.getTierConfigsForAdmin(productType);
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端:根据商品类型和商品ID获取阶梯配置(包含禁用的)
|
||||
*/
|
||||
@GetMapping("/admin/tiers/{productType}/{productId}")
|
||||
public ApiResponse<List<PriceTierConfig>> getTierConfigsForAdmin(@PathVariable String productType,
|
||||
@PathVariable String productId) {
|
||||
log.info("管理端根据商品类型和ID获取阶梯配置: {}, {}", productType, productId);
|
||||
List<PriceTierConfig> configs = productConfigService.getTierConfigsForAdmin(productType, productId);
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端:获取所有一口价配置(包含禁用的)
|
||||
*/
|
||||
@GetMapping("/admin/bundles")
|
||||
public ApiResponse<List<PriceBundleConfig>> getAllBundleConfigsForAdmin() {
|
||||
log.info("管理端获取所有一口价配置");
|
||||
List<PriceBundleConfig> configs = bundleService.getAllBundlesForAdmin();
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端:获取所有优惠券配置(包含禁用的)
|
||||
*/
|
||||
@GetMapping("/admin/coupons")
|
||||
public ApiResponse<List<PriceCouponConfig>> getAllCouponConfigsForAdmin() {
|
||||
log.info("管理端获取所有优惠券配置");
|
||||
List<PriceCouponConfig> configs = couponManagementService.getAllCouponConfigs();
|
||||
return ApiResponse.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端:获取所有优惠券领取记录
|
||||
*/
|
||||
@GetMapping("/admin/coupon-records")
|
||||
public ApiResponse<List<PriceCouponClaimRecord>> getAllCouponClaimRecordsForAdmin() {
|
||||
log.info("管理端获取所有优惠券领取记录");
|
||||
List<PriceCouponClaimRecord> records = couponManagementService.getAllClaimRecords();
|
||||
return ApiResponse.success(records);
|
||||
}
|
||||
}
|
@@ -0,0 +1,96 @@
|
||||
package com.ycwl.basic.pricing.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||
import com.ycwl.basic.pricing.service.VoucherBatchService;
|
||||
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pricing/voucher")
|
||||
public class VoucherManagementController {
|
||||
@Autowired
|
||||
@Lazy
|
||||
private VoucherBatchService voucherBatchService;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private VoucherCodeService voucherCodeService;
|
||||
|
||||
@PostMapping("/batch/create")
|
||||
public ApiResponse<Long> createBatch(@RequestBody VoucherBatchCreateReq req) {
|
||||
Long batchId = voucherBatchService.createBatch(req);
|
||||
return ApiResponse.success(batchId);
|
||||
}
|
||||
|
||||
@PostMapping("/batch/list")
|
||||
public ApiResponse<Page<VoucherBatchResp>> getBatchList(@RequestBody VoucherBatchQueryReq req) {
|
||||
Page<VoucherBatchResp> page = voucherBatchService.queryBatchList(req);
|
||||
return ApiResponse.success(page);
|
||||
}
|
||||
|
||||
@GetMapping("/batch/{id}")
|
||||
public ApiResponse<VoucherBatchResp> getBatchDetail(@PathVariable Long id) {
|
||||
VoucherBatchResp batch = voucherBatchService.getBatchDetail(id);
|
||||
return ApiResponse.success(batch);
|
||||
}
|
||||
|
||||
@GetMapping("/batch/{id}/stats")
|
||||
public ApiResponse<VoucherBatchStatsResp> getBatchStats(@PathVariable Long id) {
|
||||
VoucherBatchStatsResp stats = voucherBatchService.getBatchStats(id);
|
||||
return ApiResponse.success(stats);
|
||||
}
|
||||
|
||||
@PutMapping("/batch/{id}/status")
|
||||
public ApiResponse<Void> updateBatchStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
voucherBatchService.updateBatchStatus(id, status);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/codes")
|
||||
public ApiResponse<Page<VoucherCodeResp>> getCodeList(@RequestBody VoucherCodeQueryReq req) {
|
||||
Page<VoucherCodeResp> page = voucherCodeService.queryCodeList(req);
|
||||
return ApiResponse.success(page);
|
||||
}
|
||||
|
||||
@PutMapping("/code/{id}/use")
|
||||
public ApiResponse<Void> markCodeAsUsed(@PathVariable Long id, @RequestParam(required = false) String remark) {
|
||||
voucherCodeService.markCodeAsUsed(id, remark);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/scenic/{scenicId}/users")
|
||||
public ApiResponse<Page<VoucherCodeResp>> getUsersInScenic(@PathVariable Long scenicId,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
VoucherCodeQueryReq req = new VoucherCodeQueryReq();
|
||||
req.setScenicId(scenicId);
|
||||
req.setPageNum(pageNum);
|
||||
req.setPageSize(pageSize);
|
||||
Page<VoucherCodeResp> page = voucherCodeService.queryCodeList(req);
|
||||
return ApiResponse.success(page);
|
||||
}
|
||||
|
||||
@PostMapping("/mobile/claim")
|
||||
public ApiResponse<VoucherCodeResp> claimVoucher(@RequestBody VoucherClaimReq req) {
|
||||
VoucherCodeResp result = voucherCodeService.claimVoucher(req);
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/mobile/my-codes")
|
||||
public ApiResponse<List<VoucherCodeResp>> getMyVoucherCodes(@RequestParam Long faceId) {
|
||||
List<VoucherCodeResp> codes = voucherCodeService.getMyVoucherCodes(faceId);
|
||||
return ApiResponse.success(codes);
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 一口价套餐商品项DTO
|
||||
*/
|
||||
@Data
|
||||
public class BundleProductItem {
|
||||
|
||||
/**
|
||||
* 商品类型
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 商品子类型(可选)
|
||||
*/
|
||||
private String subType;
|
||||
|
||||
/**
|
||||
* 商品ID(可选)
|
||||
*/
|
||||
private String productId;
|
||||
|
||||
/**
|
||||
* 数量
|
||||
*/
|
||||
private Integer quantity;
|
||||
}
|
38
src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java
Normal file
38
src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import com.ycwl.basic.pricing.enums.CouponType;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 优惠券信息DTO
|
||||
*/
|
||||
@Data
|
||||
public class CouponInfo {
|
||||
|
||||
/**
|
||||
* 优惠券ID
|
||||
*/
|
||||
private Long couponId;
|
||||
|
||||
/**
|
||||
* 优惠券名称
|
||||
*/
|
||||
private String couponName;
|
||||
|
||||
/**
|
||||
* 优惠类型
|
||||
*/
|
||||
private CouponType discountType;
|
||||
|
||||
/**
|
||||
* 优惠值
|
||||
*/
|
||||
private BigDecimal discountValue;
|
||||
|
||||
/**
|
||||
* 实际优惠金额
|
||||
*/
|
||||
private BigDecimal actualDiscountAmount;
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 优惠券使用请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class CouponUseRequest {
|
||||
|
||||
/**
|
||||
* 优惠券ID
|
||||
*/
|
||||
private Long couponId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 原始金额
|
||||
*/
|
||||
private BigDecimal originalAmount;
|
||||
|
||||
/**
|
||||
* 优惠金额
|
||||
*/
|
||||
private BigDecimal discountAmount;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private String scenicId;
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 优惠券使用结果DTO
|
||||
*/
|
||||
@Data
|
||||
public class CouponUseResult {
|
||||
|
||||
/**
|
||||
* 优惠券ID
|
||||
*/
|
||||
private Long couponId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 使用时间
|
||||
*/
|
||||
private Date useTime;
|
||||
|
||||
/**
|
||||
* 优惠金额
|
||||
*/
|
||||
private BigDecimal discountAmount;
|
||||
}
|
@@ -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;
|
||||
}
|
89
src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java
Normal file
89
src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 折扣明细DTO
|
||||
*/
|
||||
@Data
|
||||
public class DiscountDetail {
|
||||
|
||||
/**
|
||||
* 折扣类型
|
||||
*/
|
||||
private String discountType;
|
||||
|
||||
/**
|
||||
* 折扣名称
|
||||
*/
|
||||
private String discountName;
|
||||
|
||||
/**
|
||||
* 折扣金额
|
||||
*/
|
||||
private BigDecimal discountAmount;
|
||||
|
||||
/**
|
||||
* 折扣描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 排序(数值越小越靠前)
|
||||
*/
|
||||
private Integer sortOrder;
|
||||
|
||||
/**
|
||||
* 创建限时立减折扣明细
|
||||
*/
|
||||
public static DiscountDetail createLimitedTimeDiscount(BigDecimal discountAmount) {
|
||||
DiscountDetail detail = new DiscountDetail();
|
||||
detail.setDiscountType("LIMITED_TIME");
|
||||
detail.setDiscountName("限时立减");
|
||||
detail.setDiscountAmount(discountAmount);
|
||||
detail.setDescription("限时优惠,立即享受");
|
||||
detail.setSortOrder(2); // 限时立减排在券码后面
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建优惠券折扣明细
|
||||
*/
|
||||
public static DiscountDetail createCouponDiscount(String couponName, BigDecimal discountAmount) {
|
||||
DiscountDetail detail = new DiscountDetail();
|
||||
detail.setDiscountType("COUPON");
|
||||
detail.setDiscountName(couponName);
|
||||
detail.setDiscountAmount(discountAmount);
|
||||
detail.setDescription("优惠券减免");
|
||||
detail.setSortOrder(3); // 优惠券排在限时立减后面
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建券码折扣明细
|
||||
*/
|
||||
public static DiscountDetail createVoucherDiscount(String voucherCode, String discountTypeName, BigDecimal discountAmount) {
|
||||
DiscountDetail detail = new DiscountDetail();
|
||||
detail.setDiscountType("VOUCHER");
|
||||
detail.setDiscountName("券码优惠");
|
||||
detail.setDiscountAmount(discountAmount);
|
||||
detail.setDescription(String.format("券码 %s - %s", voucherCode, discountTypeName));
|
||||
detail.setSortOrder(1); // 券码优先级最高,排在最前面
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一口价折扣明细
|
||||
*/
|
||||
public static DiscountDetail createBundleDiscount(BigDecimal discountAmount) {
|
||||
DiscountDetail detail = new DiscountDetail();
|
||||
detail.setDiscountType("BUNDLE");
|
||||
detail.setDiscountName("一口价优惠");
|
||||
detail.setDiscountAmount(discountAmount);
|
||||
detail.setDescription("一口价购买更优惠");
|
||||
detail.setSortOrder(4); // 一口价排在最后
|
||||
return detail;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 价格计算请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class PriceCalculationRequest {
|
||||
|
||||
/**
|
||||
* 商品列表
|
||||
*/
|
||||
private List<ProductItem> products;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 是否自动使用优惠券
|
||||
*/
|
||||
private Boolean autoUseCoupon = true;
|
||||
|
||||
/**
|
||||
* 用户输入的券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 景区ID(用于券码验证)
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 用户faceId(用于券码领取资格验证)
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 是否自动使用券码优惠
|
||||
*/
|
||||
private Boolean autoUseVoucher = true;
|
||||
|
||||
/**
|
||||
* 是否仅预览优惠(不实际使用)
|
||||
*/
|
||||
private Boolean previewOnly = false;
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 价格计算结果DTO
|
||||
*/
|
||||
@Data
|
||||
public class PriceCalculationResult {
|
||||
|
||||
/**
|
||||
* 原始金额(用于前端展示的总原价)
|
||||
*/
|
||||
private BigDecimal originalAmount;
|
||||
|
||||
/**
|
||||
* 商品小计金额(按实际计算价格)
|
||||
*/
|
||||
private BigDecimal subtotalAmount;
|
||||
|
||||
/**
|
||||
* 优惠金额
|
||||
*/
|
||||
private BigDecimal discountAmount;
|
||||
|
||||
/**
|
||||
* 最终金额
|
||||
*/
|
||||
private BigDecimal finalAmount;
|
||||
|
||||
/**
|
||||
* 使用的优惠券信息
|
||||
*/
|
||||
private CouponInfo usedCoupon;
|
||||
|
||||
/**
|
||||
* 使用的券码信息
|
||||
*/
|
||||
private VoucherInfo usedVoucher;
|
||||
|
||||
/**
|
||||
* 折扣明细列表(包含限时立减、优惠券、券码、一口价等)
|
||||
*/
|
||||
private List<DiscountDetail> discountDetails;
|
||||
|
||||
/**
|
||||
* 可用但未使用的优惠列表(预览时使用)
|
||||
*/
|
||||
private List<DiscountInfo> availableDiscounts;
|
||||
|
||||
/**
|
||||
* 商品明细列表
|
||||
*/
|
||||
private List<ProductItem> productDetails;
|
||||
}
|
32
src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java
Normal file
32
src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 价格详情(内部计算用)
|
||||
*/
|
||||
@Data
|
||||
public class PriceDetails {
|
||||
|
||||
/**
|
||||
* 实际计算总金额
|
||||
*/
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
/**
|
||||
* 原价总金额
|
||||
*/
|
||||
private BigDecimal originalTotalAmount;
|
||||
|
||||
public PriceDetails() {
|
||||
this.totalAmount = BigDecimal.ZERO;
|
||||
this.originalTotalAmount = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
public PriceDetails(BigDecimal totalAmount, BigDecimal originalTotalAmount) {
|
||||
this.totalAmount = totalAmount;
|
||||
this.originalTotalAmount = originalTotalAmount;
|
||||
}
|
||||
}
|
53
src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java
Normal file
53
src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 商品项DTO
|
||||
*/
|
||||
@Data
|
||||
public class ProductItem {
|
||||
|
||||
/**
|
||||
* 商品类型
|
||||
*/
|
||||
private ProductType productType;
|
||||
|
||||
/**
|
||||
* 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID
|
||||
*/
|
||||
private String productId;
|
||||
|
||||
/**
|
||||
* 数量(如原片数量、照片数量等)
|
||||
*/
|
||||
private Integer quantity;
|
||||
|
||||
/**
|
||||
* 购买数量(购买几个该商品)
|
||||
*/
|
||||
private Integer purchaseCount;
|
||||
|
||||
/**
|
||||
* 原价(用于前端展示)
|
||||
*/
|
||||
private BigDecimal originalPrice;
|
||||
|
||||
/**
|
||||
* 单价(实际计算用的价格)
|
||||
*/
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
/**
|
||||
* 小计(计算后填入)
|
||||
*/
|
||||
private BigDecimal subtotal;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private String scenicId;
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.ycwl.basic.pricing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 商品价格信息(内部计算用)
|
||||
*/
|
||||
@Data
|
||||
public class ProductPriceInfo {
|
||||
|
||||
/**
|
||||
* 实际计算价格
|
||||
*/
|
||||
private BigDecimal actualPrice;
|
||||
|
||||
/**
|
||||
* 原价(用于前端展示)
|
||||
*/
|
||||
private BigDecimal originalPrice;
|
||||
|
||||
public ProductPriceInfo(BigDecimal actualPrice, BigDecimal originalPrice) {
|
||||
this.actualPrice = actualPrice;
|
||||
this.originalPrice = originalPrice != null ? originalPrice : actualPrice;
|
||||
}
|
||||
}
|
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;
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.pricing.dto.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class VoucherBatchCreateReq {
|
||||
private String batchName;
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Integer discountType;
|
||||
private BigDecimal discountValue;
|
||||
private Integer totalCount;
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.ycwl.basic.pricing.dto.req;
|
||||
|
||||
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class VoucherBatchQueryReq extends BaseQueryParameterReq {
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Integer status;
|
||||
private String batchName;
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.ycwl.basic.pricing.dto.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VoucherClaimReq {
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Long faceId;
|
||||
private String code;
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.pricing.dto.req;
|
||||
|
||||
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class VoucherCodeQueryReq extends BaseQueryParameterReq {
|
||||
private Long batchId;
|
||||
private Long scenicId;
|
||||
private Long faceId;
|
||||
private Integer status;
|
||||
private String code;
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package com.ycwl.basic.pricing.dto.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 打印小票请求
|
||||
*/
|
||||
@Data
|
||||
public class VoucherPrintReq {
|
||||
|
||||
private Long faceId;
|
||||
|
||||
private Long brokerId;
|
||||
|
||||
private Long scenicId;
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class VoucherBatchResp {
|
||||
private Long id;
|
||||
private String batchName;
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Integer discountType;
|
||||
private String discountTypeName;
|
||||
private BigDecimal discountValue;
|
||||
private Integer totalCount;
|
||||
private Integer usedCount;
|
||||
private Integer claimedCount;
|
||||
private Integer availableCount;
|
||||
private Integer status;
|
||||
private String statusName;
|
||||
private Date createTime;
|
||||
private Long createBy;
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VoucherBatchStatsResp {
|
||||
private Long batchId;
|
||||
private String batchName;
|
||||
private Integer totalCount;
|
||||
private Integer claimedCount;
|
||||
private Integer usedCount;
|
||||
private Integer availableCount;
|
||||
private Double claimedRate;
|
||||
private Double usedRate;
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class VoucherCodeResp {
|
||||
private Long id;
|
||||
private Long batchId;
|
||||
private String batchName;
|
||||
private Long scenicId;
|
||||
private String code;
|
||||
private Integer status;
|
||||
private String statusName;
|
||||
private Long faceId;
|
||||
private Date claimedTime;
|
||||
private Date usedTime;
|
||||
private String remark;
|
||||
private Date createTime;
|
||||
|
||||
private Integer discountType;
|
||||
private String discountTypeName;
|
||||
private String discountDescription;
|
||||
private BigDecimal discountValue;
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 打印小票响应
|
||||
*/
|
||||
@Data
|
||||
public class VoucherPrintResp {
|
||||
|
||||
/**
|
||||
* 流水号
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 打印状态:0=待打印,1=打印成功,2=打印失败
|
||||
*/
|
||||
private Integer printStatus;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ycwl.basic.pricing.dto.BundleProductItem;
|
||||
import com.ycwl.basic.pricing.handler.BundleProductListTypeHandler;
|
||||
import lombok.Data;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 一口价配置实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("price_bundle_config")
|
||||
public class PriceBundleConfig {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 套餐名称
|
||||
*/
|
||||
private String bundleName;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private String scenicId;
|
||||
|
||||
/**
|
||||
* 套餐价格
|
||||
*/
|
||||
private BigDecimal bundlePrice;
|
||||
|
||||
/**
|
||||
* 包含商品
|
||||
*/
|
||||
@TableField(typeHandler = BundleProductListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
|
||||
private List<BundleProductItem> includedProducts;
|
||||
|
||||
/**
|
||||
* 排除商品
|
||||
*/
|
||||
@TableField(typeHandler = BundleProductListTypeHandler.class, jdbcType = JdbcType.VARCHAR)
|
||||
private List<BundleProductItem> excludedProducts;
|
||||
|
||||
/**
|
||||
* 套餐描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean isActive;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Long createBy;
|
||||
|
||||
private Long updateBy;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ycwl.basic.pricing.enums.CouponStatus;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 优惠券领用记录实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("price_coupon_claim_record")
|
||||
public class PriceCouponClaimRecord {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 优惠券ID
|
||||
*/
|
||||
private Long couponId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 领取时间
|
||||
*/
|
||||
private Date claimTime;
|
||||
|
||||
/**
|
||||
* 使用时间
|
||||
*/
|
||||
private Date useTime;
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private CouponStatus status;
|
||||
|
||||
/**
|
||||
* 景区ID - 记录优惠券在哪个景区被领取/使用
|
||||
*/
|
||||
private String scenicId;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.ycwl.basic.pricing.enums.CouponType;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 优惠券配置实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("price_coupon_config")
|
||||
public class PriceCouponConfig {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 优惠券名称
|
||||
*/
|
||||
private String couponName;
|
||||
|
||||
/**
|
||||
* 优惠券类型
|
||||
*/
|
||||
private CouponType couponType;
|
||||
|
||||
/**
|
||||
* 优惠值
|
||||
*/
|
||||
private BigDecimal discountValue;
|
||||
|
||||
/**
|
||||
* 最小使用金额
|
||||
*/
|
||||
private BigDecimal minAmount;
|
||||
|
||||
/**
|
||||
* 最大优惠金额
|
||||
*/
|
||||
private BigDecimal maxDiscount;
|
||||
|
||||
/**
|
||||
* 适用商品类型(JSON)
|
||||
*/
|
||||
private String applicableProducts;
|
||||
|
||||
/**
|
||||
* 发行总量
|
||||
*/
|
||||
private Integer totalQuantity;
|
||||
|
||||
/**
|
||||
* 已使用数量
|
||||
*/
|
||||
private Integer usedQuantity;
|
||||
|
||||
/**
|
||||
* 生效时间
|
||||
*/
|
||||
private LocalDateTime validFrom;
|
||||
|
||||
/**
|
||||
* 失效时间
|
||||
*/
|
||||
private LocalDateTime validUntil;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean isActive;
|
||||
|
||||
/**
|
||||
* 景区ID - 限制优惠券只能在该景区使用
|
||||
*/
|
||||
private String scenicId;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Long createBy;
|
||||
|
||||
private Long updateBy;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 商品价格配置实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("price_product_config")
|
||||
public class PriceProductConfig {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品类型
|
||||
*/
|
||||
private String productType;
|
||||
|
||||
/**
|
||||
* 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID
|
||||
*/
|
||||
private String productId;
|
||||
|
||||
/**
|
||||
* 景区ID:用于前端搜索和编辑时正确显示景区内容
|
||||
*/
|
||||
private String scenicId;
|
||||
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
private String productName;
|
||||
|
||||
/**
|
||||
* 基础价格
|
||||
*/
|
||||
private BigDecimal basePrice;
|
||||
|
||||
/**
|
||||
* 商品原价:用于前端展示优惠力度,当original_price > base_price时显示限时立减
|
||||
*/
|
||||
private BigDecimal originalPrice;
|
||||
|
||||
/**
|
||||
* 价格单位
|
||||
*/
|
||||
private String unit;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean isActive;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Long createBy;
|
||||
|
||||
private Long updateBy;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 阶梯定价配置实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("price_tier_config")
|
||||
public class PriceTierConfig {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品类型
|
||||
*/
|
||||
private String productType;
|
||||
|
||||
/**
|
||||
* 具体商品ID:与price_product_config的product_id对应
|
||||
*/
|
||||
private String productId;
|
||||
|
||||
/**
|
||||
* 景区ID:用于前端搜索和编辑时正确显示景区内容
|
||||
*/
|
||||
private String scenicId;
|
||||
|
||||
/**
|
||||
* 最小数量
|
||||
*/
|
||||
private Integer minQuantity;
|
||||
|
||||
/**
|
||||
* 最大数量
|
||||
*/
|
||||
private Integer maxQuantity;
|
||||
|
||||
/**
|
||||
* 阶梯价格
|
||||
*/
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 阶梯原价:用于前端展示优惠力度,当original_price > price时显示限时立减
|
||||
*/
|
||||
private BigDecimal originalPrice;
|
||||
|
||||
/**
|
||||
* 计价单位
|
||||
*/
|
||||
private String unit;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sortOrder;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean isActive;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Long createBy;
|
||||
|
||||
private Long updateBy;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 券码批次配置实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("price_voucher_batch_config")
|
||||
public class PriceVoucherBatchConfig {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 券码批次名称
|
||||
*/
|
||||
private String batchName;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 推客ID
|
||||
*/
|
||||
private Long brokerId;
|
||||
|
||||
/**
|
||||
* 优惠类型:0=全场免费,1=商品降价,2=商品打折
|
||||
*/
|
||||
private Integer discountType;
|
||||
|
||||
/**
|
||||
* 优惠值(降价金额或折扣百分比)
|
||||
*/
|
||||
private BigDecimal discountValue;
|
||||
|
||||
/**
|
||||
* 总券码数量
|
||||
*/
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 已使用数量
|
||||
*/
|
||||
private Integer usedCount;
|
||||
|
||||
/**
|
||||
* 已领取数量
|
||||
*/
|
||||
private Integer claimedCount;
|
||||
|
||||
/**
|
||||
* 状态:0=禁用,1=启用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Long createBy;
|
||||
|
||||
private Long updateBy;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 券码实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("price_voucher_code")
|
||||
public class PriceVoucherCode {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 批次ID
|
||||
*/
|
||||
private Long batchId;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 状态:0=未领取,1=已领取未使用,2=已使用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 领取人faceId
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 领取时间
|
||||
*/
|
||||
private Date claimedTime;
|
||||
|
||||
/**
|
||||
* 使用时间
|
||||
*/
|
||||
private Date usedTime;
|
||||
|
||||
/**
|
||||
* 使用备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 优惠券打印记录实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("voucher_print_record")
|
||||
public class VoucherPrintRecord {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 流水号
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 用户faceId
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 经纪人ID
|
||||
*/
|
||||
private Long brokerId;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 券码ID
|
||||
*/
|
||||
private Long voucherCodeId;
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 打印状态:0=待打印,1=打印成功,2=打印失败
|
||||
*/
|
||||
private Integer printStatus;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
31
src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java
Normal file
31
src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.ycwl.basic.pricing.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 优惠券状态枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CouponStatus {
|
||||
|
||||
CLAIMED("claimed", "已领取"),
|
||||
USED("used", "已使用"),
|
||||
EXPIRED("expired", "已过期");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举
|
||||
*/
|
||||
public static CouponStatus fromCode(String code) {
|
||||
for (CouponStatus status : values()) {
|
||||
if (status.code.equals(code)) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown coupon status code: " + code);
|
||||
}
|
||||
}
|
30
src/main/java/com/ycwl/basic/pricing/enums/CouponType.java
Normal file
30
src/main/java/com/ycwl/basic/pricing/enums/CouponType.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.ycwl.basic.pricing.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 优惠券类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CouponType {
|
||||
|
||||
PERCENTAGE("percentage", "百分比折扣"),
|
||||
FIXED_AMOUNT("fixed_amount", "固定金额减免");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举
|
||||
*/
|
||||
public static CouponType fromCode(String code) {
|
||||
for (CouponType type : values()) {
|
||||
if (type.code.equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown coupon type code: " + code);
|
||||
}
|
||||
}
|
33
src/main/java/com/ycwl/basic/pricing/enums/ProductType.java
Normal file
33
src/main/java/com/ycwl/basic/pricing/enums/ProductType.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.ycwl.basic.pricing.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 商品类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProductType {
|
||||
|
||||
VLOG_VIDEO("VLOG_VIDEO", "Vlog视频"),
|
||||
RECORDING_SET("RECORDING_SET", "录像集"),
|
||||
PHOTO_SET("PHOTO_SET", "照相集"),
|
||||
PHOTO_PRINT("PHOTO_PRINT", "照片打印"),
|
||||
MACHINE_PRINT("MACHINE_PRINT", "一体机打印");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举
|
||||
*/
|
||||
public static ProductType fromCode(String code) {
|
||||
for (ProductType type : values()) {
|
||||
if (type.code.equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown product type code: " + code);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.ycwl.basic.pricing.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 券码优惠类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum VoucherDiscountType {
|
||||
|
||||
/**
|
||||
* 全场免费 - 所有商品免费
|
||||
*/
|
||||
FREE_ALL(0, "全场免费", "所有商品免费"),
|
||||
|
||||
/**
|
||||
* 商品降价 - 每个商品减免指定金额
|
||||
*/
|
||||
REDUCE_PRICE(1, "商品降价", "每个商品减免指定金额"),
|
||||
|
||||
/**
|
||||
* 商品打折 - 每个商品按百分比打折
|
||||
*/
|
||||
DISCOUNT(2, "商品打折", "每个商品按百分比打折");
|
||||
|
||||
private final Integer code;
|
||||
private final String name;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举值
|
||||
* @param code 代码
|
||||
* @return 枚举值
|
||||
*/
|
||||
public static VoucherDiscountType getByCode(Integer code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (VoucherDiscountType type : values()) {
|
||||
if (type.getCode().equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为有效的优惠类型代码
|
||||
* @param code 代码
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValidCode(Integer code) {
|
||||
return getByCode(code) != null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.pricing.exception;
|
||||
|
||||
/**
|
||||
* 优惠券无效异常
|
||||
*/
|
||||
public class CouponInvalidException extends RuntimeException {
|
||||
|
||||
public CouponInvalidException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CouponInvalidException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.pricing.exception;
|
||||
|
||||
/**
|
||||
* 价格计算异常
|
||||
*/
|
||||
public class PriceCalculationException extends RuntimeException {
|
||||
|
||||
public PriceCalculationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PriceCalculationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.ycwl.basic.pricing.exception;
|
||||
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 价格查询系统全局异常处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice(basePackages = "com.ycwl.basic.pricing")
|
||||
public class PricingExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理价格计算异常
|
||||
*/
|
||||
@ExceptionHandler(PriceCalculationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiResponse<Void> handlePriceCalculationException(PriceCalculationException e) {
|
||||
log.error("价格计算异常", e);
|
||||
return ApiResponse.buildResponse(400, e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理优惠券无效异常
|
||||
*/
|
||||
@ExceptionHandler(CouponInvalidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiResponse<Void> handleCouponInvalidException(CouponInvalidException e) {
|
||||
log.error("优惠券无效异常", e);
|
||||
return ApiResponse.buildResponse(400, e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理商品配置未找到异常
|
||||
*/
|
||||
@ExceptionHandler(ProductConfigNotFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public ApiResponse<Void> handleProductConfigNotFoundException(ProductConfigNotFoundException e) {
|
||||
log.error("商品配置未找到异常", e);
|
||||
return ApiResponse.buildResponse(404, e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理非法参数异常
|
||||
*/
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiResponse<Void> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
log.error("非法参数异常", e);
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理通用异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ApiResponse<Void> handleException(Exception e) {
|
||||
log.error("系统异常", e);
|
||||
return ApiResponse.buildResponse(500, "系统内部错误");
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.pricing.exception;
|
||||
|
||||
/**
|
||||
* 商品配置未找到异常
|
||||
*/
|
||||
public class ProductConfigNotFoundException extends RuntimeException {
|
||||
|
||||
public ProductConfigNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProductConfigNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
package com.ycwl.basic.pricing.handler;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.pricing.dto.BundleProductItem;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 一口价商品列表类型处理器
|
||||
*/
|
||||
@Slf4j
|
||||
public class BundleProductListTypeHandler extends BaseTypeHandler<List<BundleProductItem>> {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final TypeReference<List<BundleProductItem>> typeReference = new TypeReference<List<BundleProductItem>>() {};
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, List<BundleProductItem> parameter, JdbcType jdbcType) throws SQLException {
|
||||
try {
|
||||
String json = objectMapper.writeValueAsString(parameter);
|
||||
ps.setString(i, json);
|
||||
log.debug("序列化商品列表: {}", json);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("序列化商品列表失败", e);
|
||||
throw new SQLException("序列化商品列表失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BundleProductItem> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String json = rs.getString(columnName);
|
||||
return parseJson(json, columnName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BundleProductItem> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String json = rs.getString(columnIndex);
|
||||
return parseJson(json, "columnIndex:" + columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BundleProductItem> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String json = cs.getString(columnIndex);
|
||||
return parseJson(json, "columnIndex:" + columnIndex);
|
||||
}
|
||||
|
||||
private List<BundleProductItem> parseJson(String json, String source) {
|
||||
if (json == null || json.trim().isEmpty()) {
|
||||
log.debug("从{}获取的JSON为空,返回空列表", source);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
List<BundleProductItem> result = objectMapper.readValue(json, typeReference);
|
||||
log.debug("从{}反序列化商品列表成功,数量: {}", source, result != null ? result.size() : 0);
|
||||
return result != null ? result : new ArrayList<>();
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("从{}反序列化商品列表失败,JSON: {}", source, json, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
package com.ycwl.basic.pricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.pricing.entity.PriceBundleConfig;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 一口价配置Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PriceBundleConfigMapper extends BaseMapper<PriceBundleConfig> {
|
||||
|
||||
/**
|
||||
* 查询启用的一口价配置
|
||||
*/
|
||||
@Select("SELECT id, bundle_name, scenic_id, bundle_price, " +
|
||||
"included_products, excluded_products, " +
|
||||
"description, is_active, create_time, update_time " +
|
||||
"FROM price_bundle_config WHERE is_active = 1")
|
||||
@Results({
|
||||
@Result(column = "included_products", property = "includedProducts",
|
||||
typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class),
|
||||
@Result(column = "excluded_products", property = "excludedProducts",
|
||||
typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class)
|
||||
})
|
||||
List<PriceBundleConfig> selectActiveBundles();
|
||||
|
||||
/**
|
||||
* 根据ID查询启用的配置
|
||||
*/
|
||||
@Select("SELECT id, bundle_name, scenic_id, bundle_price, " +
|
||||
"included_products, excluded_products, " +
|
||||
"description, is_active, create_time, update_time " +
|
||||
"FROM price_bundle_config WHERE id = #{id} AND is_active = 1")
|
||||
@Results({
|
||||
@Result(column = "included_products", property = "includedProducts",
|
||||
typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class),
|
||||
@Result(column = "excluded_products", property = "excludedProducts",
|
||||
typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class)
|
||||
})
|
||||
PriceBundleConfig selectActiveBundleById(Long id);
|
||||
|
||||
// ==================== 管理端接口(包含禁用的配置) ====================
|
||||
|
||||
/**
|
||||
* 查询所有一口价配置(包含禁用的)- 管理端使用
|
||||
*/
|
||||
@Select("SELECT id, bundle_name, scenic_id, bundle_price, " +
|
||||
"included_products, excluded_products, " +
|
||||
"description, is_active, create_time, update_time " +
|
||||
"FROM price_bundle_config ORDER BY is_active DESC, bundle_name ASC")
|
||||
@Results({
|
||||
@Result(column = "included_products", property = "includedProducts",
|
||||
typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class),
|
||||
@Result(column = "excluded_products", property = "excludedProducts",
|
||||
typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class)
|
||||
})
|
||||
List<PriceBundleConfig> selectAllBundlesForAdmin();
|
||||
|
||||
/**
|
||||
* 插入一口价配置
|
||||
*/
|
||||
@Insert("INSERT INTO price_bundle_config (bundle_name, scenic_id, bundle_price, included_products, excluded_products, " +
|
||||
"description, is_active, create_time, update_time) VALUES " +
|
||||
"(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " +
|
||||
"#{description}, #{isActive}, NOW(), NOW())")
|
||||
int insertBundleConfig(PriceBundleConfig config);
|
||||
|
||||
/**
|
||||
* 更新一口价配置
|
||||
*/
|
||||
@Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, scenic_id = #{scenicId}, bundle_price = #{bundlePrice}, " +
|
||||
"included_products = #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, excluded_products = #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " +
|
||||
"description = #{description}, is_active = #{isActive}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateBundleConfig(PriceBundleConfig config);
|
||||
|
||||
/**
|
||||
* 更新一口价配置状态
|
||||
*/
|
||||
@Update("UPDATE price_bundle_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateBundleConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive);
|
||||
}
|
@@ -0,0 +1,209 @@
|
||||
package com.ycwl.basic.pricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
||||
import com.ycwl.basic.pricing.enums.CouponStatus;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券领用记录Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PriceCouponClaimRecordMapper extends BaseMapper<PriceCouponClaimRecord> {
|
||||
|
||||
/**
|
||||
* 查询用户可用的优惠券记录
|
||||
*/
|
||||
@Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value, c.min_amount, " +
|
||||
"c.max_discount, c.applicable_products, c.valid_from, c.valid_until " +
|
||||
"FROM price_coupon_claim_record r " +
|
||||
"JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||
"WHERE r.user_id = #{userId} AND r.status = 'CLAIMED' " +
|
||||
"AND c.is_active = 1 AND c.valid_from <= NOW() AND c.valid_until > NOW()")
|
||||
List<PriceCouponClaimRecord> selectUserAvailableCoupons(Long userId);
|
||||
|
||||
/**
|
||||
* 查询用户特定优惠券记录
|
||||
*/
|
||||
@Select("SELECT * FROM price_coupon_claim_record " +
|
||||
"WHERE user_id = #{userId} AND coupon_id = #{couponId} AND status = 'CLAIMED'")
|
||||
PriceCouponClaimRecord selectUserCouponRecord(@Param("userId") Long userId,
|
||||
@Param("couponId") Long couponId);
|
||||
|
||||
/**
|
||||
* 更新优惠券使用状态
|
||||
*/
|
||||
@Update("UPDATE price_coupon_claim_record SET status = #{status}, " +
|
||||
"use_time = #{useTime}, order_id = #{orderId}, scenic_id = #{scenicId}, update_time = NOW() " +
|
||||
"WHERE id = #{id}")
|
||||
int updateCouponStatus(@Param("id") Long id,
|
||||
@Param("status") CouponStatus status,
|
||||
@Param("useTime") java.util.Date useTime,
|
||||
@Param("orderId") String orderId,
|
||||
@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 插入优惠券领用记录
|
||||
*/
|
||||
@Insert("INSERT INTO price_coupon_claim_record (coupon_id, user_id, claim_time, status, scenic_id, create_time, update_time) " +
|
||||
"VALUES (#{couponId}, #{userId}, NOW(), #{status}, #{scenicId}, NOW(), NOW())")
|
||||
int insertClaimRecord(PriceCouponClaimRecord record);
|
||||
|
||||
/**
|
||||
* 更新优惠券记录
|
||||
*/
|
||||
@Update("UPDATE price_coupon_claim_record SET status = #{status}, use_time = #{useTime}, " +
|
||||
"order_id = #{orderId}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateClaimRecord(PriceCouponClaimRecord record);
|
||||
|
||||
// ==================== 管理端接口 ====================
|
||||
|
||||
/**
|
||||
* 管理端:查询所有优惠券领取记录(支持分页)
|
||||
*/
|
||||
@Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " +
|
||||
"FROM price_coupon_claim_record r " +
|
||||
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||
"ORDER BY r.create_time DESC")
|
||||
List<PriceCouponClaimRecord> selectAllForAdmin();
|
||||
|
||||
/**
|
||||
* 管理端:根据条件查询优惠券领取记录(支持分页)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " +
|
||||
"FROM price_coupon_claim_record r " +
|
||||
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||
"<where>" +
|
||||
"<if test='userId != null'>" +
|
||||
"AND r.user_id = #{userId}" +
|
||||
"</if>" +
|
||||
"<if test='couponId != null'>" +
|
||||
"AND r.coupon_id = #{couponId}" +
|
||||
"</if>" +
|
||||
"<if test='status != null'>" +
|
||||
"AND r.status = #{status}" +
|
||||
"</if>" +
|
||||
"<if test='startTime != null and startTime != \"\"'>" +
|
||||
"AND r.claim_time >= #{startTime}" +
|
||||
"</if>" +
|
||||
"<if test='endTime != null and endTime != \"\"'>" +
|
||||
"AND r.claim_time <= #{endTime}" +
|
||||
"</if>" +
|
||||
"<if test='scenicId != null and scenicId != \"\"'>" +
|
||||
"AND r.scenic_id = #{scenicId}" +
|
||||
"</if>" +
|
||||
"</where>" +
|
||||
"ORDER BY r.create_time DESC" +
|
||||
"</script>")
|
||||
List<PriceCouponClaimRecord> selectByConditionsForAdmin(@Param("userId") Long userId,
|
||||
@Param("couponId") Long couponId,
|
||||
@Param("status") CouponStatus status,
|
||||
@Param("startTime") String startTime,
|
||||
@Param("endTime") String endTime,
|
||||
@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 管理端:根据用户ID查询优惠券领取记录
|
||||
*/
|
||||
@Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " +
|
||||
"FROM price_coupon_claim_record r " +
|
||||
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||
"WHERE r.user_id = #{userId} " +
|
||||
"ORDER BY r.create_time DESC")
|
||||
List<PriceCouponClaimRecord> selectByUserIdForAdmin(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 管理端:根据优惠券ID查询领取记录
|
||||
*/
|
||||
@Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " +
|
||||
"FROM price_coupon_claim_record r " +
|
||||
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||
"WHERE r.coupon_id = #{couponId} " +
|
||||
"ORDER BY r.create_time DESC")
|
||||
List<PriceCouponClaimRecord> selectByCouponIdForAdmin(@Param("couponId") Long couponId);
|
||||
|
||||
/**
|
||||
* 管理端:根据状态查询领取记录
|
||||
*/
|
||||
@Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " +
|
||||
"FROM price_coupon_claim_record r " +
|
||||
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||
"WHERE r.status = #{status} " +
|
||||
"ORDER BY r.create_time DESC")
|
||||
List<PriceCouponClaimRecord> selectByStatusForAdmin(@Param("status") CouponStatus status);
|
||||
|
||||
/**
|
||||
* 管理端:统计优惠券使用情况
|
||||
*/
|
||||
@Select("SELECT " +
|
||||
"COUNT(*) as total_claimed, " +
|
||||
"COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " +
|
||||
"COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available " +
|
||||
"FROM price_coupon_claim_record WHERE coupon_id = #{couponId}")
|
||||
java.util.Map<String, Object> selectCouponUsageStats(@Param("couponId") Long couponId);
|
||||
|
||||
/**
|
||||
* 管理端:统计优惠券详细使用情况
|
||||
*/
|
||||
@Select("SELECT " +
|
||||
"COUNT(*) as total_claimed, " +
|
||||
"COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " +
|
||||
"COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " +
|
||||
"CASE WHEN COUNT(*) > 0 THEN COUNT(CASE WHEN status = 'USED' THEN 1 END) / COUNT(*) ELSE 0 END as usage_rate, " +
|
||||
"CASE WHEN COUNT(CASE WHEN status = 'USED' THEN 1 END) > 0 THEN " +
|
||||
"AVG(CASE WHEN status = 'USED' AND use_time IS NOT NULL AND claim_time IS NOT NULL THEN " +
|
||||
"DATEDIFF(use_time, claim_time) END) ELSE 0 END as avg_days_to_use " +
|
||||
"FROM price_coupon_claim_record WHERE coupon_id = #{couponId}")
|
||||
java.util.Map<String, Object> selectCouponDetailStats(@Param("couponId") Long couponId);
|
||||
|
||||
/**
|
||||
* 管理端:统计时间范围内的数据
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT " +
|
||||
"COUNT(*) as total_claimed, " +
|
||||
"COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " +
|
||||
"COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " +
|
||||
"COUNT(CASE WHEN status = 'EXPIRED' THEN 1 END) as total_expired, " +
|
||||
"COUNT(DISTINCT coupon_id) as total_coupon_types, " +
|
||||
"COUNT(DISTINCT user_id) as total_users " +
|
||||
"FROM price_coupon_claim_record " +
|
||||
"WHERE claim_time >= #{startDate} AND claim_time <= #{endDate} " +
|
||||
"<if test='scenicId != null and scenicId != \"\"'>" +
|
||||
"AND scenic_id = #{scenicId} " +
|
||||
"</if>" +
|
||||
"</script>")
|
||||
java.util.Map<String, Object> selectPeriodStats(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate,
|
||||
@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 根据景区ID查询优惠券领取记录
|
||||
*/
|
||||
@Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " +
|
||||
"FROM price_coupon_claim_record r " +
|
||||
"LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " +
|
||||
"WHERE r.scenic_id = #{scenicId} " +
|
||||
"ORDER BY r.create_time DESC")
|
||||
List<PriceCouponClaimRecord> selectByScenicIdForAdmin(@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 统计景区优惠券使用情况
|
||||
*/
|
||||
@Select("SELECT " +
|
||||
"COUNT(*) as total_claimed, " +
|
||||
"COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " +
|
||||
"COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " +
|
||||
"COUNT(CASE WHEN status = 'EXPIRED' THEN 1 END) as total_expired, " +
|
||||
"COUNT(DISTINCT coupon_id) as total_coupon_types, " +
|
||||
"COUNT(DISTINCT user_id) as total_users " +
|
||||
"FROM price_coupon_claim_record WHERE scenic_id = #{scenicId}")
|
||||
java.util.Map<String, Object> selectScenicCouponUsageStats(@Param("scenicId") String scenicId);
|
||||
}
|
@@ -0,0 +1,136 @@
|
||||
package com.ycwl.basic.pricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券配置Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
|
||||
|
||||
/**
|
||||
* 查询有效的优惠券配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_coupon_config WHERE is_active = 1 " +
|
||||
"AND valid_from <= NOW() AND valid_until > NOW() " +
|
||||
"AND used_quantity < total_quantity")
|
||||
List<PriceCouponConfig> selectValidCoupons();
|
||||
|
||||
/**
|
||||
* 根据ID查询优惠券(包括使用数量检查)
|
||||
*/
|
||||
@Select("SELECT * FROM price_coupon_config WHERE id = #{couponId} " +
|
||||
"AND is_active = 1 AND valid_from <= NOW() AND valid_until > NOW() " +
|
||||
"AND used_quantity < total_quantity")
|
||||
PriceCouponConfig selectValidCouponById(Long couponId);
|
||||
|
||||
/**
|
||||
* 增加优惠券使用数量
|
||||
*/
|
||||
@Update("UPDATE price_coupon_config SET used_quantity = used_quantity + 1, " +
|
||||
"update_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity")
|
||||
int incrementUsedQuantity(Long couponId);
|
||||
|
||||
/**
|
||||
* 插入优惠券配置
|
||||
*/
|
||||
@Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " +
|
||||
"max_discount, applicable_products, total_quantity, used_quantity, valid_from, valid_until, " +
|
||||
"is_active, scenic_id, create_time, update_time) VALUES " +
|
||||
"(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " +
|
||||
"#{applicableProducts}, #{totalQuantity}, #{usedQuantity}, #{validFrom}, #{validUntil}, " +
|
||||
"#{isActive}, #{scenicId}, NOW(), NOW())")
|
||||
int insertCoupon(PriceCouponConfig coupon);
|
||||
|
||||
/**
|
||||
* 更新优惠券配置
|
||||
*/
|
||||
@Update("UPDATE price_coupon_config SET coupon_name = #{couponName}, coupon_type = #{couponType}, " +
|
||||
"discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " +
|
||||
"applicable_products = #{applicableProducts}, total_quantity = #{totalQuantity}, " +
|
||||
"valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " +
|
||||
"scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateCoupon(PriceCouponConfig coupon);
|
||||
|
||||
// ==================== 管理端接口 ====================
|
||||
|
||||
/**
|
||||
* 管理端:查询所有优惠券配置(包含禁用的)
|
||||
*/
|
||||
@Select("SELECT * FROM price_coupon_config ORDER BY create_time DESC")
|
||||
List<PriceCouponConfig> selectAllForAdmin();
|
||||
|
||||
/**
|
||||
* 管理端:根据条件查询优惠券配置(支持分页)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT * FROM price_coupon_config " +
|
||||
"<where>" +
|
||||
"<if test='isActive != null'>" +
|
||||
"AND is_active = #{isActive}" +
|
||||
"</if>" +
|
||||
"<if test='couponName != null and couponName != \"\"'>" +
|
||||
"AND coupon_name LIKE CONCAT('%', #{couponName}, '%')" +
|
||||
"</if>" +
|
||||
"<if test='scenicId != null and scenicId != \"\"'>" +
|
||||
"AND scenic_id = #{scenicId}" +
|
||||
"</if>" +
|
||||
"</where>" +
|
||||
"ORDER BY create_time DESC" +
|
||||
"</script>")
|
||||
List<PriceCouponConfig> selectByConditionsForAdmin(@Param("isActive") Boolean isActive,
|
||||
@Param("couponName") String couponName,
|
||||
@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 管理端:根据状态查询优惠券配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_coupon_config WHERE is_active = #{isActive} ORDER BY create_time DESC")
|
||||
List<PriceCouponConfig> selectByStatusForAdmin(@Param("isActive") Boolean isActive);
|
||||
|
||||
/**
|
||||
* 管理端:更新优惠券状态
|
||||
*/
|
||||
@Update("UPDATE price_coupon_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateCouponStatus(@Param("id") Long id, @Param("isActive") Boolean isActive);
|
||||
|
||||
/**
|
||||
* 管理端:删除优惠券配置
|
||||
*/
|
||||
@Update("UPDATE price_coupon_config SET deleted = 1, update_time = NOW() WHERE id = #{id}")
|
||||
int deleteCoupon(Long id);
|
||||
|
||||
/**
|
||||
* 查询指定景区的有效优惠券配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_coupon_config WHERE is_active = 1 " +
|
||||
"AND valid_from <= NOW() AND valid_until > NOW() " +
|
||||
"AND used_quantity < total_quantity " +
|
||||
"AND (scenic_id IS NULL OR scenic_id = #{scenicId})")
|
||||
List<PriceCouponConfig> selectValidCouponsByScenicId(@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 管理端:根据景区ID查询优惠券配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_coupon_config WHERE scenic_id = #{scenicId} ORDER BY create_time DESC")
|
||||
List<PriceCouponConfig> selectByScenicIdForAdmin(@Param("scenicId") String scenicId);
|
||||
|
||||
/**
|
||||
* 统计景区优惠券配置数量
|
||||
*/
|
||||
@Select("SELECT " +
|
||||
"COUNT(*) as total_coupons, " +
|
||||
"COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_coupons, " +
|
||||
"SUM(total_quantity) as total_quantity, " +
|
||||
"SUM(used_quantity) as used_quantity " +
|
||||
"FROM price_coupon_config WHERE scenic_id = #{scenicId}")
|
||||
java.util.Map<String, Object> selectScenicCouponConfigStats(@Param("scenicId") String scenicId);
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package com.ycwl.basic.pricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.pricing.entity.PriceProductConfig;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品价格配置Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig> {
|
||||
|
||||
/**
|
||||
* 查询启用的商品配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_product_config WHERE is_active = 1")
|
||||
List<PriceProductConfig> selectActiveConfigs();
|
||||
|
||||
/**
|
||||
* 根据商品类型查询配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND is_active = 1")
|
||||
List<PriceProductConfig> selectByProductType(String productType);
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID查询配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1")
|
||||
PriceProductConfig selectByProductTypeAndId(String productType, String productId);
|
||||
|
||||
/**
|
||||
* 检查是否存在default配置(包含禁用的)
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM price_product_config WHERE product_type = #{productType} AND product_id = 'default'")
|
||||
int countDefaultConfigsByProductType(@Param("productType") String productType);
|
||||
|
||||
// ==================== 管理端接口(包含禁用的配置) ====================
|
||||
|
||||
/**
|
||||
* 查询所有商品配置(包含禁用的)- 管理端使用
|
||||
*/
|
||||
@Select("SELECT * FROM price_product_config ORDER BY product_type ASC, is_active DESC, product_id ASC")
|
||||
List<PriceProductConfig> selectAllConfigsForAdmin();
|
||||
|
||||
/**
|
||||
* 根据商品类型查询所有配置(包含禁用的)- 管理端使用
|
||||
*/
|
||||
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} ORDER BY is_active DESC, product_id ASC")
|
||||
List<PriceProductConfig> selectByProductTypeForAdmin(@Param("productType") String productType);
|
||||
|
||||
/**
|
||||
* 插入商品价格配置
|
||||
*/
|
||||
@Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, create_time, update_time) " +
|
||||
"VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, NOW(), NOW())")
|
||||
int insertProductConfig(PriceProductConfig config);
|
||||
|
||||
/**
|
||||
* 更新商品价格配置
|
||||
*/
|
||||
@Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " +
|
||||
"original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateProductConfig(PriceProductConfig config);
|
||||
|
||||
/**
|
||||
* 更新商品配置状态
|
||||
*/
|
||||
@Update("UPDATE price_product_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateProductConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive);
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
package com.ycwl.basic.pricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 阶梯定价配置Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PriceTierConfigMapper extends BaseMapper<PriceTierConfig> {
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID和数量查询匹配的阶梯价格
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
|
||||
"AND product_id = #{productId} " +
|
||||
"AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " +
|
||||
"AND is_active = 1 ORDER BY sort_order ASC LIMIT 1")
|
||||
PriceTierConfig selectByProductTypeAndQuantity(@Param("productType") String productType,
|
||||
@Param("productId") String productId,
|
||||
@Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 根据商品类型查询所有阶梯配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
|
||||
"AND is_active = 1 ORDER BY sort_order ASC")
|
||||
List<PriceTierConfig> selectByProductType(String productType);
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID查询所有阶梯配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
|
||||
"AND product_id = #{productId} AND is_active = 1 ORDER BY sort_order ASC")
|
||||
List<PriceTierConfig> selectByProductTypeAndId(@Param("productType") String productType,
|
||||
@Param("productId") String productId);
|
||||
|
||||
|
||||
/**
|
||||
* 查询所有启用的阶梯配置
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config WHERE is_active = 1 ORDER BY product_type ASC, sort_order ASC")
|
||||
List<PriceTierConfig> selectAllActiveConfigs();
|
||||
|
||||
/**
|
||||
* 检查是否存在default阶梯配置(包含禁用的)
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM price_tier_config WHERE product_type = #{productType} AND product_id = 'default'")
|
||||
int countDefaultTierConfigsByProductType(@Param("productType") String productType);
|
||||
|
||||
// ==================== 管理端接口(包含禁用的配置) ====================
|
||||
|
||||
/**
|
||||
* 查询所有阶梯配置(包含禁用的)- 管理端使用
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config ORDER BY product_type ASC, is_active DESC, sort_order ASC")
|
||||
List<PriceTierConfig> selectAllConfigsForAdmin();
|
||||
|
||||
/**
|
||||
* 根据商品类型查询所有阶梯配置(包含禁用的)- 管理端使用
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
|
||||
"ORDER BY is_active DESC, sort_order ASC")
|
||||
List<PriceTierConfig> selectByProductTypeForAdmin(@Param("productType") String productType);
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID查询所有阶梯配置(包含禁用的)- 管理端使用
|
||||
*/
|
||||
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
|
||||
"AND product_id = #{productId} ORDER BY is_active DESC, sort_order ASC")
|
||||
List<PriceTierConfig> selectByProductTypeAndIdForAdmin(@Param("productType") String productType,
|
||||
@Param("productId") String productId);
|
||||
|
||||
/**
|
||||
* 插入阶梯定价配置
|
||||
*/
|
||||
@Insert("INSERT INTO price_tier_config (product_type, product_id, scenic_id, min_quantity, max_quantity, price, " +
|
||||
"original_price, unit, sort_order, is_active, create_time, update_time) VALUES " +
|
||||
"(#{productType}, #{productId}, #{scenicId}, #{minQuantity}, #{maxQuantity}, #{price}, " +
|
||||
"#{originalPrice}, #{unit}, #{sortOrder}, #{isActive}, NOW(), NOW())")
|
||||
int insertTierConfig(PriceTierConfig config);
|
||||
|
||||
/**
|
||||
* 更新阶梯定价配置
|
||||
*/
|
||||
@Update("UPDATE price_tier_config SET product_id = #{productId}, scenic_id = #{scenicId}, min_quantity = #{minQuantity}, " +
|
||||
"max_quantity = #{maxQuantity}, price = #{price}, original_price = #{originalPrice}, unit = #{unit}, sort_order = #{sortOrder}, " +
|
||||
"is_active = #{isActive}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateTierConfig(PriceTierConfig config);
|
||||
|
||||
/**
|
||||
* 更新阶梯配置状态
|
||||
*/
|
||||
@Update("UPDATE price_tier_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateTierConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive);
|
||||
}
|
@@ -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,101 @@
|
||||
package com.ycwl.basic.pricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherCode;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 券码Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PriceVoucherCodeMapper extends BaseMapper<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);
|
||||
|
||||
/**
|
||||
* 随机获取一个未被打印过的可用券码
|
||||
* @param scenicId 景区ID
|
||||
* @return 可用券码
|
||||
*/
|
||||
PriceVoucherCode findRandomUnprintedVoucher(@Param("scenicId") Long scenicId);
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package com.ycwl.basic.pricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.pricing.entity.VoucherPrintRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 优惠券打印记录Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface VoucherPrintRecordMapper extends BaseMapper<VoucherPrintRecord> {
|
||||
|
||||
/**
|
||||
* 根据faceId、brokerId、scenicId查询已存在的打印记录
|
||||
* @param faceId 用户faceId
|
||||
* @param scenicId 景区ID
|
||||
* @return 打印记录
|
||||
*/
|
||||
VoucherPrintRecord selectByFaceBrokerScenic(@Param("faceId") Long faceId,
|
||||
@Param("scenicId") Long scenicId);
|
||||
|
||||
/**
|
||||
* 根据券码ID查询是否已被打印
|
||||
* @param voucherCodeId 券码ID
|
||||
* @return 打印记录
|
||||
*/
|
||||
VoucherPrintRecord selectByVoucherCodeId(@Param("voucherCodeId") Long voucherCodeId);
|
||||
|
||||
/**
|
||||
* 更新打印状态
|
||||
* @param id 记录ID
|
||||
* @param printStatus 打印状态
|
||||
* @param errorMessage 错误信息(可为null)
|
||||
* @return 影响行数
|
||||
*/
|
||||
int updatePrintStatus(@Param("id") Long id,
|
||||
@Param("printStatus") Integer printStatus,
|
||||
@Param("errorMessage") String errorMessage);
|
||||
}
|
@@ -0,0 +1,112 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
|
||||
import com.ycwl.basic.pricing.enums.CouponStatus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 优惠券管理服务接口(管理端)
|
||||
*/
|
||||
public interface ICouponManagementService {
|
||||
|
||||
// ==================== 优惠券配置管理 ====================
|
||||
|
||||
/**
|
||||
* 创建优惠券配置
|
||||
*/
|
||||
Long createCouponConfig(PriceCouponConfig config);
|
||||
|
||||
/**
|
||||
* 更新优惠券配置
|
||||
*/
|
||||
boolean updateCouponConfig(PriceCouponConfig config);
|
||||
|
||||
/**
|
||||
* 删除优惠券配置
|
||||
*/
|
||||
boolean deleteCouponConfig(Long id);
|
||||
|
||||
/**
|
||||
* 启用/禁用优惠券配置
|
||||
*/
|
||||
boolean updateCouponConfigStatus(Long id, Boolean isActive);
|
||||
|
||||
/**
|
||||
* 查询所有优惠券配置(包含禁用的)
|
||||
*/
|
||||
List<PriceCouponConfig> getAllCouponConfigs();
|
||||
|
||||
/**
|
||||
* 分页查询优惠券配置
|
||||
*/
|
||||
PageInfo<PriceCouponConfig> getCouponConfigsPage(Integer pageNum, Integer pageSize,
|
||||
Boolean isActive, String couponName, String scenicId);
|
||||
|
||||
/**
|
||||
* 根据状态查询优惠券配置
|
||||
*/
|
||||
List<PriceCouponConfig> getCouponConfigsByStatus(Boolean isActive);
|
||||
|
||||
/**
|
||||
* 根据ID查询优惠券配置
|
||||
*/
|
||||
PriceCouponConfig getCouponConfigById(Long id);
|
||||
|
||||
// ==================== 优惠券领取记录查询 ====================
|
||||
|
||||
/**
|
||||
* 查询所有优惠券领取记录
|
||||
*/
|
||||
List<PriceCouponClaimRecord> getAllClaimRecords();
|
||||
|
||||
/**
|
||||
* 分页查询优惠券领取记录
|
||||
*/
|
||||
PageInfo<PriceCouponClaimRecord> getClaimRecordsPage(Integer pageNum, Integer pageSize,
|
||||
Long userId, Long couponId, CouponStatus status,
|
||||
String startTime, String endTime, String scenicId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询优惠券领取记录
|
||||
*/
|
||||
List<PriceCouponClaimRecord> getClaimRecordsByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 根据优惠券ID查询领取记录
|
||||
*/
|
||||
List<PriceCouponClaimRecord> getClaimRecordsByCouponId(Long couponId);
|
||||
|
||||
/**
|
||||
* 根据状态查询领取记录
|
||||
*/
|
||||
List<PriceCouponClaimRecord> getClaimRecordsByStatus(CouponStatus status);
|
||||
|
||||
/**
|
||||
* 查询优惠券使用统计
|
||||
*/
|
||||
Map<String, Object> getCouponUsageStats(Long couponId);
|
||||
|
||||
/**
|
||||
* 查询优惠券配置详细统计
|
||||
*/
|
||||
Map<String, Object> getCouponDetailStats(Long couponId);
|
||||
|
||||
/**
|
||||
* 查询时间范围内的统计数据
|
||||
*/
|
||||
Map<String, Object> getPeriodStats(String startDate, String endDate, String scenicId);
|
||||
|
||||
/**
|
||||
* 查询所有优惠券的使用统计概览
|
||||
*/
|
||||
List<Map<String, Object>> getAllCouponUsageOverview();
|
||||
|
||||
/**
|
||||
* 查询景区优惠券统计
|
||||
*/
|
||||
Map<String, Object> getScenicCouponStats(String scenicId);
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.CouponInfo;
|
||||
import com.ycwl.basic.pricing.dto.CouponUseRequest;
|
||||
import com.ycwl.basic.pricing.dto.CouponUseResult;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券服务接口
|
||||
*/
|
||||
public interface ICouponService {
|
||||
|
||||
/**
|
||||
* 自动选择最优优惠券
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param products 商品列表
|
||||
* @param totalAmount 总金额
|
||||
* @return 最优优惠券信息,如果没有可用优惠券则返回null
|
||||
*/
|
||||
CouponInfo selectBestCoupon(Long userId, List<ProductItem> products, BigDecimal totalAmount);
|
||||
|
||||
/**
|
||||
* 计算优惠券优惠金额
|
||||
*
|
||||
* @param coupon 优惠券配置
|
||||
* @param products 商品列表
|
||||
* @param totalAmount 总金额
|
||||
* @return 优惠金额
|
||||
*/
|
||||
BigDecimal calculateCouponDiscount(PriceCouponConfig coupon, List<ProductItem> products, BigDecimal totalAmount);
|
||||
|
||||
/**
|
||||
* 验证优惠券是否可用
|
||||
*
|
||||
* @param coupon 优惠券配置
|
||||
* @param products 商品列表
|
||||
* @param totalAmount 总金额
|
||||
* @return 是否可用
|
||||
*/
|
||||
boolean isCouponApplicable(PriceCouponConfig coupon, List<ProductItem> products, BigDecimal totalAmount);
|
||||
|
||||
/**
|
||||
* 使用优惠券
|
||||
*
|
||||
* @param request 优惠券使用请求
|
||||
* @return 使用结果
|
||||
*/
|
||||
CouponUseResult useCoupon(CouponUseRequest request);
|
||||
|
||||
/**
|
||||
* 查询用户可用优惠券
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 可用优惠券列表
|
||||
*/
|
||||
List<CouponInfo> getUserAvailableCoupons(Long userId);
|
||||
}
|
@@ -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,52 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import com.ycwl.basic.pricing.entity.PriceBundleConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 一口价套餐服务接口
|
||||
*/
|
||||
public interface IPriceBundleService {
|
||||
|
||||
/**
|
||||
* 检查商品是否适用一口价
|
||||
*
|
||||
* @param products 商品列表
|
||||
* @return 是否适用
|
||||
*/
|
||||
boolean isBundleApplicable(List<ProductItem> products);
|
||||
|
||||
/**
|
||||
* 获取一口价价格
|
||||
*
|
||||
* @param products 商品列表
|
||||
* @return 一口价价格,如果不适用则返回null
|
||||
*/
|
||||
BigDecimal getBundlePrice(List<ProductItem> products);
|
||||
|
||||
/**
|
||||
* 获取所有启用的一口价配置
|
||||
*
|
||||
* @return 一口价配置列表
|
||||
*/
|
||||
List<PriceBundleConfig> getActiveBundles();
|
||||
|
||||
/**
|
||||
* 获取所有一口价配置(仅启用的)
|
||||
*
|
||||
* @return 一口价配置列表
|
||||
*/
|
||||
List<PriceBundleConfig> getAllBundles();
|
||||
|
||||
// ==================== 管理端接口(包含禁用的配置) ====================
|
||||
|
||||
/**
|
||||
* 获取所有一口价配置(包含禁用的)- 管理端使用
|
||||
*
|
||||
* @return 一口价配置列表
|
||||
*/
|
||||
List<PriceBundleConfig> getAllBundlesForAdmin();
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
|
||||
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
||||
|
||||
/**
|
||||
* 价格计算服务接口
|
||||
*/
|
||||
public interface IPriceCalculationService {
|
||||
|
||||
/**
|
||||
* 计算商品价格(支持自动优惠券应用)
|
||||
*
|
||||
* @param request 价格计算请求
|
||||
* @return 价格计算结果
|
||||
*/
|
||||
PriceCalculationResult calculatePrice(PriceCalculationRequest request);
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.entity.*;
|
||||
|
||||
/**
|
||||
* 价格管理服务接口(用于配置管理,手动处理时间字段)
|
||||
*/
|
||||
public interface IPricingManagementService {
|
||||
|
||||
/**
|
||||
* 创建商品价格配置
|
||||
*/
|
||||
Long createProductConfig(PriceProductConfig config);
|
||||
|
||||
/**
|
||||
* 更新商品价格配置
|
||||
*/
|
||||
boolean updateProductConfig(PriceProductConfig config);
|
||||
|
||||
/**
|
||||
* 创建阶梯定价配置
|
||||
*/
|
||||
Long createTierConfig(PriceTierConfig config);
|
||||
|
||||
/**
|
||||
* 更新阶梯定价配置
|
||||
*/
|
||||
boolean updateTierConfig(PriceTierConfig config);
|
||||
|
||||
/**
|
||||
* 创建优惠券配置
|
||||
*/
|
||||
Long createCouponConfig(PriceCouponConfig config);
|
||||
|
||||
/**
|
||||
* 更新优惠券配置
|
||||
*/
|
||||
boolean updateCouponConfig(PriceCouponConfig config);
|
||||
|
||||
/**
|
||||
* 创建优惠券领用记录
|
||||
*/
|
||||
Long createCouponClaimRecord(PriceCouponClaimRecord record);
|
||||
|
||||
/**
|
||||
* 更新优惠券领用记录
|
||||
*/
|
||||
boolean updateCouponClaimRecord(PriceCouponClaimRecord record);
|
||||
|
||||
/**
|
||||
* 创建一口价配置
|
||||
*/
|
||||
Long createBundleConfig(PriceBundleConfig config);
|
||||
|
||||
/**
|
||||
* 更新一口价配置
|
||||
*/
|
||||
boolean updateBundleConfig(PriceBundleConfig config);
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
|
||||
/**
|
||||
* 更新商品配置状态
|
||||
*/
|
||||
boolean updateProductConfigStatus(Long id, Boolean isActive);
|
||||
|
||||
/**
|
||||
* 更新阶梯配置状态
|
||||
*/
|
||||
boolean updateTierConfigStatus(Long id, Boolean isActive);
|
||||
|
||||
/**
|
||||
* 更新一口价配置状态
|
||||
*/
|
||||
boolean updateBundleConfigStatus(Long id, Boolean isActive);
|
||||
|
||||
// ==================== 删除操作 ====================
|
||||
|
||||
/**
|
||||
* 删除商品配置
|
||||
*/
|
||||
boolean deleteProductConfig(Long id);
|
||||
|
||||
/**
|
||||
* 删除阶梯配置
|
||||
*/
|
||||
boolean deleteTierConfig(Long id);
|
||||
|
||||
/**
|
||||
* 删除一口价配置
|
||||
*/
|
||||
boolean deleteBundleConfig(Long id);
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.entity.PriceProductConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品配置管理服务接口
|
||||
*/
|
||||
public interface IProductConfigService {
|
||||
|
||||
/**
|
||||
* 根据商品类型获取基础配置(兼容旧接口)
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @return 商品配置列表
|
||||
*/
|
||||
List<PriceProductConfig> getProductConfig(String productType);
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID获取精确配置
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @param productId 具体商品ID
|
||||
* @return 商品配置
|
||||
*/
|
||||
PriceProductConfig getProductConfig(String productType, String productId);
|
||||
|
||||
/**
|
||||
* 根据商品类型、商品ID和数量获取阶梯价格配置
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @param productId 具体商品ID
|
||||
* @param quantity 数量
|
||||
* @return 阶梯价格配置
|
||||
*/
|
||||
PriceTierConfig getTierConfig(String productType, String productId, Integer quantity);
|
||||
|
||||
/**
|
||||
* 获取所有启用的商品配置
|
||||
*
|
||||
* @return 商品配置列表
|
||||
*/
|
||||
List<PriceProductConfig> getActiveProductConfigs();
|
||||
|
||||
/**
|
||||
* 获取所有商品配置(仅启用的)
|
||||
*
|
||||
* @return 商品配置列表
|
||||
*/
|
||||
List<PriceProductConfig> getAllProductConfigs();
|
||||
|
||||
/**
|
||||
* 根据商品类型获取所有阶梯配置
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @return 阶梯配置列表
|
||||
*/
|
||||
List<PriceTierConfig> getTierConfigs(String productType);
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID获取所有阶梯配置
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @param productId 具体商品ID
|
||||
* @return 阶梯配置列表
|
||||
*/
|
||||
List<PriceTierConfig> getTierConfigs(String productType, String productId);
|
||||
|
||||
/**
|
||||
* 获取所有启用的阶梯配置
|
||||
*
|
||||
* @return 阶梯配置列表
|
||||
*/
|
||||
List<PriceTierConfig> getAllTierConfigs();
|
||||
|
||||
// ==================== 管理端接口(包含禁用的配置) ====================
|
||||
|
||||
/**
|
||||
* 获取所有商品配置(包含禁用的)- 管理端使用
|
||||
*
|
||||
* @return 商品配置列表
|
||||
*/
|
||||
List<PriceProductConfig> getAllProductConfigsForAdmin();
|
||||
|
||||
/**
|
||||
* 获取所有阶梯配置(包含禁用的)- 管理端使用
|
||||
*
|
||||
* @return 阶梯配置列表
|
||||
*/
|
||||
List<PriceTierConfig> getAllTierConfigsForAdmin();
|
||||
|
||||
/**
|
||||
* 根据商品类型获取所有阶梯配置(包含禁用的)- 管理端使用
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @return 阶梯配置列表
|
||||
*/
|
||||
List<PriceTierConfig> getTierConfigsForAdmin(String productType);
|
||||
|
||||
/**
|
||||
* 根据商品类型和商品ID获取所有阶梯配置(包含禁用的)- 管理端使用
|
||||
*
|
||||
* @param productType 商品类型
|
||||
* @param productId 具体商品ID
|
||||
* @return 阶梯配置列表
|
||||
*/
|
||||
List<PriceTierConfig> getTierConfigsForAdmin(String productType, String productId);
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 券码服务接口
|
||||
*/
|
||||
public interface IVoucherService {
|
||||
|
||||
/**
|
||||
* 验证并获取券码信息
|
||||
* @param voucherCode 券码
|
||||
* @param faceId 用户faceId
|
||||
* @param scenicId 景区ID
|
||||
* @return 券码信息(如果有效)
|
||||
*/
|
||||
VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId);
|
||||
|
||||
/**
|
||||
* 获取用户在指定景区的可用券码列表
|
||||
* @param faceId 用户faceId
|
||||
* @param scenicId 景区ID
|
||||
* @return 可用券码列表
|
||||
*/
|
||||
List<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);
|
||||
|
||||
/**
|
||||
* 获取该faceId在scenicId下的券码详情列表
|
||||
* @param faceId 用户面部ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 券码详情列表,包含所有状态的券码(已领取未使用、已使用等),如果没有券码则返回空列表
|
||||
*/
|
||||
List<VoucherInfo> getVoucherDetails(Long faceId, Long scenicId);
|
||||
|
||||
/**
|
||||
* 计算券码优惠金额
|
||||
* @param voucherInfo 券码信息
|
||||
* @param context 检测上下文
|
||||
* @return 优惠金额
|
||||
*/
|
||||
BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context);
|
||||
|
||||
/**
|
||||
* 获取最优的券码(如果用户有多个可用券码)
|
||||
* @param faceId 用户faceId
|
||||
* @param scenicId 景区ID
|
||||
* @param context 检测上下文
|
||||
* @return 最优券码信息
|
||||
*/
|
||||
VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context);
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||
|
||||
public interface VoucherBatchService {
|
||||
|
||||
Long createBatch(VoucherBatchCreateReq req);
|
||||
|
||||
Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req);
|
||||
|
||||
VoucherBatchResp getBatchDetail(Long id);
|
||||
|
||||
VoucherBatchStatsResp getBatchStats(Long id);
|
||||
|
||||
void updateBatchStatus(Long id, Integer status);
|
||||
|
||||
void updateBatchClaimedCount(Long batchId);
|
||||
|
||||
void updateBatchUsedCount(Long batchId);
|
||||
|
||||
PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId);
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface VoucherCodeService {
|
||||
|
||||
void generateVoucherCodes(Long batchId, Long scenicId, Integer count);
|
||||
|
||||
VoucherCodeResp claimVoucher(VoucherClaimReq req);
|
||||
|
||||
Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req);
|
||||
|
||||
List<VoucherCodeResp> getMyVoucherCodes(Long faceId);
|
||||
|
||||
void markCodeAsUsed(Long codeId, String remark);
|
||||
|
||||
boolean canClaimVoucher(Long faceId, Long scenicId);
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
|
||||
/**
|
||||
* 优惠券打印服务
|
||||
*/
|
||||
public interface VoucherPrintService {
|
||||
|
||||
/**
|
||||
* 打印小票
|
||||
* @param request 打印请求
|
||||
* @return 打印响应
|
||||
*/
|
||||
VoucherPrintResp printVoucherTicket(VoucherPrintReq request);
|
||||
|
||||
VoucherPrintResp queryPrintedVoucher(Long faceId);
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,253 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
|
||||
import com.ycwl.basic.pricing.enums.CouponStatus;
|
||||
import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper;
|
||||
import com.ycwl.basic.pricing.service.ICouponManagementService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 优惠券管理服务实现(管理端)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CouponManagementServiceImpl implements ICouponManagementService {
|
||||
|
||||
private final PriceCouponConfigMapper couponConfigMapper;
|
||||
private final PriceCouponClaimRecordMapper claimRecordMapper;
|
||||
|
||||
// ==================== 优惠券配置管理 ====================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createCouponConfig(PriceCouponConfig config) {
|
||||
log.info("创建优惠券配置: {}", config.getCouponName());
|
||||
|
||||
// 设置默认值
|
||||
if (config.getUsedQuantity() == null) {
|
||||
config.setUsedQuantity(0);
|
||||
}
|
||||
if (config.getIsActive() == null) {
|
||||
config.setIsActive(true);
|
||||
}
|
||||
|
||||
int result = couponConfigMapper.insertCoupon(config);
|
||||
if (result > 0) {
|
||||
log.info("优惠券配置创建成功,ID: {}", config.getId());
|
||||
return config.getId();
|
||||
} else {
|
||||
log.error("优惠券配置创建失败");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateCouponConfig(PriceCouponConfig config) {
|
||||
log.info("更新优惠券配置,ID: {}", config.getId());
|
||||
|
||||
PriceCouponConfig existing = couponConfigMapper.selectById(config.getId());
|
||||
if (existing == null) {
|
||||
log.error("优惠券配置不存在,ID: {}", config.getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = couponConfigMapper.updateCoupon(config);
|
||||
if (result > 0) {
|
||||
log.info("优惠券配置更新成功,ID: {}", config.getId());
|
||||
return true;
|
||||
} else {
|
||||
log.error("优惠券配置更新失败,ID: {}", config.getId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deleteCouponConfig(Long id) {
|
||||
log.info("删除优惠券配置,ID: {}", id);
|
||||
|
||||
PriceCouponConfig existing = couponConfigMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
log.error("优惠券配置不存在,ID: {}", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = couponConfigMapper.deleteCoupon(id);
|
||||
if (result > 0) {
|
||||
log.info("优惠券配置删除成功,ID: {}", id);
|
||||
return true;
|
||||
} else {
|
||||
log.error("优惠券配置删除失败,ID: {}", id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateCouponConfigStatus(Long id, Boolean isActive) {
|
||||
log.info("更新优惠券配置状态,ID: {}, 状态: {}", id, isActive);
|
||||
|
||||
PriceCouponConfig existing = couponConfigMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
log.error("优惠券配置不存在,ID: {}", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = couponConfigMapper.updateCouponStatus(id, isActive);
|
||||
if (result > 0) {
|
||||
log.info("优惠券配置状态更新成功,ID: {}", id);
|
||||
return true;
|
||||
} else {
|
||||
log.error("优惠券配置状态更新失败,ID: {}", id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceCouponConfig> getAllCouponConfigs() {
|
||||
log.info("查询所有优惠券配置");
|
||||
return couponConfigMapper.selectAllForAdmin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageInfo<PriceCouponConfig> getCouponConfigsPage(Integer pageNum, Integer pageSize,
|
||||
Boolean isActive, String couponName, String scenicId) {
|
||||
log.info("分页查询优惠券配置,页码: {}, 页大小: {}, 状态: {}, 名称: {}, 景区ID: {}",
|
||||
pageNum, pageSize, isActive, couponName, scenicId);
|
||||
|
||||
PageHelper.startPage(pageNum, pageSize);
|
||||
List<PriceCouponConfig> configs = couponConfigMapper.selectByConditionsForAdmin(isActive, couponName, scenicId);
|
||||
return new PageInfo<>(configs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceCouponConfig> getCouponConfigsByStatus(Boolean isActive) {
|
||||
log.info("根据状态查询优惠券配置,状态: {}", isActive);
|
||||
return couponConfigMapper.selectByStatusForAdmin(isActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriceCouponConfig getCouponConfigById(Long id) {
|
||||
log.info("根据ID查询优惠券配置,ID: {}", id);
|
||||
return couponConfigMapper.selectById(id);
|
||||
}
|
||||
|
||||
// ==================== 优惠券领取记录查询 ====================
|
||||
|
||||
@Override
|
||||
public List<PriceCouponClaimRecord> getAllClaimRecords() {
|
||||
log.info("查询所有优惠券领取记录");
|
||||
return claimRecordMapper.selectAllForAdmin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageInfo<PriceCouponClaimRecord> getClaimRecordsPage(Integer pageNum, Integer pageSize,
|
||||
Long userId, Long couponId, CouponStatus status,
|
||||
String startTime, String endTime, String scenicId) {
|
||||
log.info("分页查询优惠券领取记录,页码: {}, 页大小: {}, 用户ID: {}, 优惠券ID: {}, 状态: {}, 开始时间: {}, 结束时间: {}, 景区ID: {}",
|
||||
pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId);
|
||||
|
||||
PageHelper.startPage(pageNum, pageSize);
|
||||
List<PriceCouponClaimRecord> records = claimRecordMapper.selectByConditionsForAdmin(userId, couponId, status, startTime, endTime, scenicId);
|
||||
return new PageInfo<>(records);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceCouponClaimRecord> getClaimRecordsByUserId(Long userId) {
|
||||
log.info("根据用户ID查询优惠券领取记录,用户ID: {}", userId);
|
||||
return claimRecordMapper.selectByUserIdForAdmin(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceCouponClaimRecord> getClaimRecordsByCouponId(Long couponId) {
|
||||
log.info("根据优惠券ID查询领取记录,优惠券ID: {}", couponId);
|
||||
return claimRecordMapper.selectByCouponIdForAdmin(couponId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceCouponClaimRecord> getClaimRecordsByStatus(CouponStatus status) {
|
||||
log.info("根据状态查询领取记录,状态: {}", status);
|
||||
return claimRecordMapper.selectByStatusForAdmin(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getCouponUsageStats(Long couponId) {
|
||||
log.info("查询优惠券使用统计,优惠券ID: {}", couponId);
|
||||
return claimRecordMapper.selectCouponUsageStats(couponId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getCouponDetailStats(Long couponId) {
|
||||
log.info("查询优惠券详细统计,优惠券ID: {}", couponId);
|
||||
return claimRecordMapper.selectCouponDetailStats(couponId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getPeriodStats(String startDate, String endDate, String scenicId) {
|
||||
log.info("查询时间范围统计,开始日期: {}, 结束日期: {}, 景区ID: {}", startDate, endDate, scenicId);
|
||||
return claimRecordMapper.selectPeriodStats(startDate, endDate, scenicId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getAllCouponUsageOverview() {
|
||||
log.info("查询所有优惠券使用统计概览");
|
||||
|
||||
List<PriceCouponConfig> allCoupons = couponConfigMapper.selectAllForAdmin();
|
||||
List<Map<String, Object>> overview = new ArrayList<>();
|
||||
|
||||
for (PriceCouponConfig coupon : allCoupons) {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("couponId", coupon.getId());
|
||||
stats.put("couponName", coupon.getCouponName());
|
||||
stats.put("couponType", coupon.getCouponType());
|
||||
stats.put("totalQuantity", coupon.getTotalQuantity());
|
||||
stats.put("usedQuantity", coupon.getUsedQuantity());
|
||||
stats.put("remainingQuantity", coupon.getTotalQuantity() - coupon.getUsedQuantity());
|
||||
stats.put("isActive", coupon.getIsActive());
|
||||
stats.put("validFrom", coupon.getValidFrom());
|
||||
stats.put("validUntil", coupon.getValidUntil());
|
||||
|
||||
// 获取详细统计
|
||||
Map<String, Object> usageStats = claimRecordMapper.selectCouponUsageStats(coupon.getId());
|
||||
stats.putAll(usageStats);
|
||||
|
||||
overview.add(stats);
|
||||
}
|
||||
|
||||
return overview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getScenicCouponStats(String scenicId) {
|
||||
log.info("查询景区优惠券统计,景区ID: {}", scenicId);
|
||||
|
||||
// 获取景区优惠券配置统计
|
||||
Map<String, Object> configStats = couponConfigMapper.selectScenicCouponConfigStats(scenicId);
|
||||
|
||||
// 获取景区优惠券使用统计
|
||||
Map<String, Object> usageStats = claimRecordMapper.selectScenicCouponUsageStats(scenicId);
|
||||
|
||||
// 合并统计结果
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("scenic_id", scenicId);
|
||||
result.putAll(configStats);
|
||||
result.putAll(usageStats);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -0,0 +1,199 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.pricing.dto.*;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
|
||||
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
|
||||
import com.ycwl.basic.pricing.enums.CouponStatus;
|
||||
import com.ycwl.basic.pricing.enums.CouponType;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.pricing.exception.CouponInvalidException;
|
||||
import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper;
|
||||
import com.ycwl.basic.pricing.service.ICouponService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("pricingCouponServiceImpl")
|
||||
@RequiredArgsConstructor
|
||||
public class CouponServiceImpl implements ICouponService {
|
||||
|
||||
private final PriceCouponConfigMapper couponConfigMapper;
|
||||
private final PriceCouponClaimRecordMapper couponClaimRecordMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public CouponInfo selectBestCoupon(Long userId, List<ProductItem> products, BigDecimal totalAmount) {
|
||||
List<PriceCouponClaimRecord> userCoupons = couponClaimRecordMapper.selectUserAvailableCoupons(userId);
|
||||
if (userCoupons.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CouponInfo bestCoupon = null;
|
||||
BigDecimal maxDiscount = BigDecimal.ZERO;
|
||||
|
||||
for (PriceCouponClaimRecord record : userCoupons) {
|
||||
PriceCouponConfig coupon = couponConfigMapper.selectById(record.getCouponId());
|
||||
if (coupon == null || !isCouponApplicable(coupon, products, totalAmount)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BigDecimal discount = calculateCouponDiscount(coupon, products, totalAmount);
|
||||
if (discount.compareTo(maxDiscount) > 0) {
|
||||
maxDiscount = discount;
|
||||
bestCoupon = buildCouponInfo(coupon, discount);
|
||||
}
|
||||
}
|
||||
|
||||
return bestCoupon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal calculateCouponDiscount(PriceCouponConfig coupon, List<ProductItem> products, BigDecimal totalAmount) {
|
||||
if (!isCouponApplicable(coupon, products, totalAmount)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
BigDecimal discount;
|
||||
if (coupon.getCouponType() == CouponType.PERCENTAGE) {
|
||||
discount = totalAmount.multiply(coupon.getDiscountValue().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
|
||||
if (coupon.getMaxDiscount() != null && discount.compareTo(coupon.getMaxDiscount()) > 0) {
|
||||
discount = coupon.getMaxDiscount();
|
||||
}
|
||||
} else {
|
||||
discount = coupon.getDiscountValue();
|
||||
}
|
||||
|
||||
return discount.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCouponApplicable(PriceCouponConfig coupon, List<ProductItem> products, BigDecimal totalAmount) {
|
||||
// 1. 检查最小使用金额
|
||||
if (totalAmount.compareTo(coupon.getMinAmount()) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 检查景区限制
|
||||
if (coupon.getScenicId() != null && !coupon.getScenicId().isEmpty()) {
|
||||
boolean hasMatchingScenicProduct = false;
|
||||
for (ProductItem product : products) {
|
||||
if (coupon.getScenicId().equals(product.getScenicId())) {
|
||||
hasMatchingScenicProduct = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasMatchingScenicProduct) {
|
||||
log.debug("优惠券景区限制不匹配: 优惠券景区={}, 商品景区={}",
|
||||
coupon.getScenicId(),
|
||||
products.stream().map(ProductItem::getScenicId).distinct().toList());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查商品类型限制
|
||||
if (coupon.getApplicableProducts() == null || coupon.getApplicableProducts().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> applicableProductTypes = objectMapper.readValue(
|
||||
coupon.getApplicableProducts(), new TypeReference<List<String>>() {});
|
||||
|
||||
for (ProductItem product : products) {
|
||||
if (applicableProductTypes.contains(product.getProductType().getCode())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("解析适用商品类型失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CouponUseResult useCoupon(CouponUseRequest request) {
|
||||
PriceCouponClaimRecord record = couponClaimRecordMapper.selectUserCouponRecord(
|
||||
request.getUserId(), request.getCouponId());
|
||||
|
||||
if (record == null) {
|
||||
throw new CouponInvalidException("用户未拥有该优惠券");
|
||||
}
|
||||
|
||||
if (record.getStatus() != CouponStatus.CLAIMED) {
|
||||
throw new CouponInvalidException("优惠券状态无效: " + record.getStatus());
|
||||
}
|
||||
|
||||
int updateCount = couponConfigMapper.incrementUsedQuantity(request.getCouponId());
|
||||
if (updateCount == 0) {
|
||||
throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限");
|
||||
}
|
||||
|
||||
Date useTime = new Date();
|
||||
|
||||
// 设置使用时间、订单信息和景区信息
|
||||
record.setStatus(CouponStatus.USED);
|
||||
record.setUseTime(useTime);
|
||||
record.setOrderId(request.getOrderId());
|
||||
record.setUpdateTime(new Date());
|
||||
|
||||
// 如果请求中包含景区ID,记录到使用记录中
|
||||
if (request.getScenicId() != null && !request.getScenicId().isEmpty()) {
|
||||
record.setScenicId(request.getScenicId());
|
||||
}
|
||||
|
||||
couponClaimRecordMapper.updateCouponStatus(
|
||||
record.getId(), CouponStatus.USED, useTime, request.getOrderId(), request.getScenicId());
|
||||
|
||||
CouponUseResult result = new CouponUseResult();
|
||||
result.setCouponId(request.getCouponId());
|
||||
result.setUserId(request.getUserId());
|
||||
result.setOrderId(request.getOrderId());
|
||||
result.setUseTime(useTime);
|
||||
result.setDiscountAmount(request.getDiscountAmount());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CouponInfo> getUserAvailableCoupons(Long userId) {
|
||||
List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserAvailableCoupons(userId);
|
||||
List<CouponInfo> coupons = new ArrayList<>();
|
||||
|
||||
for (PriceCouponClaimRecord record : records) {
|
||||
PriceCouponConfig config = couponConfigMapper.selectById(record.getCouponId());
|
||||
if (config != null) {
|
||||
coupons.add(buildCouponInfo(config, null));
|
||||
}
|
||||
}
|
||||
|
||||
return coupons;
|
||||
}
|
||||
|
||||
private CouponInfo buildCouponInfo(PriceCouponConfig coupon, BigDecimal actualDiscountAmount) {
|
||||
CouponInfo info = new CouponInfo();
|
||||
info.setCouponId(coupon.getId());
|
||||
info.setCouponName(coupon.getCouponName());
|
||||
info.setDiscountType(coupon.getCouponType());
|
||||
info.setDiscountValue(coupon.getDiscountValue());
|
||||
info.setActualDiscountAmount(actualDiscountAmount);
|
||||
return info;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,122 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ycwl.basic.pricing.dto.BundleProductItem;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import com.ycwl.basic.pricing.entity.PriceBundleConfig;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.pricing.mapper.PriceBundleConfigMapper;
|
||||
import com.ycwl.basic.pricing.service.IPriceBundleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
//import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 一口价套餐服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("pricingBundleServiceImpl")
|
||||
@RequiredArgsConstructor
|
||||
public class PriceBundleServiceImpl implements IPriceBundleService {
|
||||
|
||||
private final PriceBundleConfigMapper bundleConfigMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public boolean isBundleApplicable(List<ProductItem> products) {
|
||||
List<PriceBundleConfig> bundles = getActiveBundles();
|
||||
if (bundles.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<String> productTypes = new HashSet<>();
|
||||
for (ProductItem product : products) {
|
||||
productTypes.add(product.getProductType().getCode());
|
||||
}
|
||||
|
||||
for (PriceBundleConfig bundle : bundles) {
|
||||
if (isProductsMatchBundle(productTypes, bundle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getBundlePrice(List<ProductItem> products) {
|
||||
if (!isBundleApplicable(products)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<PriceBundleConfig> bundles = getActiveBundles();
|
||||
Set<String> productTypes = new HashSet<>();
|
||||
for (ProductItem product : products) {
|
||||
productTypes.add(product.getProductType().getCode());
|
||||
}
|
||||
|
||||
for (PriceBundleConfig bundle : bundles) {
|
||||
if (isProductsMatchBundle(productTypes, bundle)) {
|
||||
return bundle.getBundlePrice();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "active-bundles")
|
||||
public List<PriceBundleConfig> getActiveBundles() {
|
||||
return bundleConfigMapper.selectActiveBundles();
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "all-bundles")
|
||||
public List<PriceBundleConfig> getAllBundles() {
|
||||
return bundleConfigMapper.selectActiveBundles();
|
||||
}
|
||||
|
||||
// ==================== 管理端接口(包含禁用的配置) ====================
|
||||
|
||||
@Override
|
||||
public List<PriceBundleConfig> getAllBundlesForAdmin() {
|
||||
return bundleConfigMapper.selectAllBundlesForAdmin();
|
||||
}
|
||||
|
||||
private boolean isProductsMatchBundle(Set<String> productTypes, PriceBundleConfig bundle) {
|
||||
try {
|
||||
// 检查包含的商品
|
||||
if (bundle.getIncludedProducts() != null && !bundle.getIncludedProducts().isEmpty()) {
|
||||
Set<String> requiredProducts = new HashSet<>();
|
||||
for (BundleProductItem item : bundle.getIncludedProducts()) {
|
||||
requiredProducts.add(item.getType());
|
||||
}
|
||||
if (!productTypes.containsAll(requiredProducts)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查排除的商品
|
||||
if (bundle.getExcludedProducts() != null && !bundle.getExcludedProducts().isEmpty()) {
|
||||
for (BundleProductItem item : bundle.getExcludedProducts()) {
|
||||
if (productTypes.contains(item.getType())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析一口价配置失败: bundleId={}", bundle.getId(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,388 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.*;
|
||||
import com.ycwl.basic.pricing.entity.PriceProductConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.pricing.exception.PriceCalculationException;
|
||||
import com.ycwl.basic.pricing.service.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 价格计算服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("pricingCalculationServiceImpl")
|
||||
@RequiredArgsConstructor
|
||||
public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
||||
|
||||
private final IProductConfigService productConfigService;
|
||||
private final ICouponService couponService;
|
||||
private final IPriceBundleService bundleService;
|
||||
private final IDiscountDetectionService discountDetectionService;
|
||||
private final IVoucherService voucherService;
|
||||
|
||||
@Override
|
||||
public PriceCalculationResult calculatePrice(PriceCalculationRequest request) {
|
||||
if (request.getProducts() == null || request.getProducts().isEmpty()) {
|
||||
throw new PriceCalculationException("商品列表不能为空");
|
||||
}
|
||||
|
||||
// 计算商品价格和原价
|
||||
PriceDetails priceDetails = calculateProductsPriceWithOriginal(request.getProducts());
|
||||
BigDecimal totalAmount = priceDetails.getTotalAmount();
|
||||
BigDecimal originalTotalAmount = priceDetails.getOriginalTotalAmount();
|
||||
|
||||
List<DiscountDetail> discountDetails = new ArrayList<>();
|
||||
|
||||
// 添加限时立减折扣(如果原价 > 实际价格)
|
||||
BigDecimal limitedTimeDiscount = originalTotalAmount.subtract(totalAmount);
|
||||
if (limitedTimeDiscount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
discountDetails.add(DiscountDetail.createLimitedTimeDiscount(limitedTimeDiscount));
|
||||
}
|
||||
|
||||
// 检查一口价优惠
|
||||
BigDecimal bundlePrice = bundleService.getBundlePrice(request.getProducts());
|
||||
if (bundlePrice != null && bundlePrice.compareTo(totalAmount) < 0) {
|
||||
BigDecimal bundleDiscount = totalAmount.subtract(bundlePrice);
|
||||
discountDetails.add(DiscountDetail.createBundleDiscount(bundleDiscount));
|
||||
totalAmount = bundlePrice;
|
||||
log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount);
|
||||
}
|
||||
|
||||
// 构建价格计算结果
|
||||
PriceCalculationResult result = new PriceCalculationResult();
|
||||
result.setOriginalAmount(originalTotalAmount); // 原总价
|
||||
result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计
|
||||
result.setProductDetails(request.getProducts());
|
||||
|
||||
// 使用新的优惠检测系统处理所有优惠(券码 + 优惠券)
|
||||
DiscountCombinationResult discountResult = calculateDiscounts(request, totalAmount);
|
||||
|
||||
if (Boolean.TRUE.equals(discountResult.getSuccess())) {
|
||||
// 合并所有优惠详情
|
||||
List<DiscountDetail> allDiscountDetails = new ArrayList<>(discountDetails);
|
||||
if (discountResult.getDiscountDetails() != null) {
|
||||
allDiscountDetails.addAll(discountResult.getDiscountDetails());
|
||||
}
|
||||
|
||||
// 重新排序
|
||||
allDiscountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
|
||||
|
||||
// 计算总优惠金额(包括限时立减、一口价和其他优惠)
|
||||
BigDecimal totalDiscountAmount = allDiscountDetails.stream()
|
||||
.map(DiscountDetail::getDiscountAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 设置结果
|
||||
result.setDiscountAmount(totalDiscountAmount);
|
||||
result.setDiscountDetails(allDiscountDetails);
|
||||
result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount));
|
||||
|
||||
// 设置使用的券码和优惠券信息
|
||||
setUsedDiscountInfo(result, discountResult, request);
|
||||
|
||||
// 如果是预览模式,设置可用优惠列表
|
||||
if (Boolean.TRUE.equals(request.getPreviewOnly())) {
|
||||
result.setAvailableDiscounts(discountResult.getAvailableDiscounts());
|
||||
}
|
||||
|
||||
} else {
|
||||
log.warn("优惠计算失败: {}", discountResult.getErrorMessage());
|
||||
|
||||
// 降级处理:仅使用基础优惠(限时立减、一口价)
|
||||
BigDecimal totalDiscountAmount = discountDetails.stream()
|
||||
.map(DiscountDetail::getDiscountAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
result.setDiscountAmount(totalDiscountAmount);
|
||||
result.setDiscountDetails(discountDetails);
|
||||
result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount));
|
||||
}
|
||||
|
||||
// 标记使用的优惠(仅在非预览模式下)
|
||||
if (!Boolean.TRUE.equals(request.getPreviewOnly())) {
|
||||
markDiscountsAsUsed(result, request);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private BigDecimal calculateProductsPrice(List<ProductItem> products) {
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
|
||||
for (ProductItem product : products) {
|
||||
BigDecimal itemPrice = calculateSingleProductPrice(product);
|
||||
product.setUnitPrice(itemPrice);
|
||||
|
||||
BigDecimal subtotal = itemPrice.multiply(BigDecimal.valueOf(product.getPurchaseCount()));
|
||||
product.setSubtotal(subtotal);
|
||||
|
||||
totalAmount = totalAmount.add(subtotal);
|
||||
}
|
||||
|
||||
return totalAmount.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private PriceDetails calculateProductsPriceWithOriginal(List<ProductItem> products) {
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
BigDecimal originalTotalAmount = BigDecimal.ZERO;
|
||||
|
||||
for (ProductItem product : products) {
|
||||
// 计算实际价格和原价
|
||||
ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product);
|
||||
|
||||
product.setUnitPrice(priceInfo.getActualPrice());
|
||||
product.setOriginalPrice(priceInfo.getOriginalPrice());
|
||||
|
||||
BigDecimal subtotal = priceInfo.getActualPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount()));
|
||||
BigDecimal originalSubtotal = priceInfo.getOriginalPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount()));
|
||||
|
||||
product.setSubtotal(subtotal);
|
||||
|
||||
totalAmount = totalAmount.add(subtotal);
|
||||
originalTotalAmount = originalTotalAmount.add(originalSubtotal);
|
||||
}
|
||||
|
||||
return new PriceDetails(
|
||||
totalAmount.setScale(2, RoundingMode.HALF_UP),
|
||||
originalTotalAmount.setScale(2, RoundingMode.HALF_UP)
|
||||
);
|
||||
}
|
||||
|
||||
private BigDecimal calculateSingleProductPrice(ProductItem product) {
|
||||
ProductType productType = product.getProductType();
|
||||
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
||||
|
||||
// 优先使用基于product_id的阶梯定价
|
||||
PriceTierConfig tierConfig = productConfigService.getTierConfig(
|
||||
productType.getCode(), productId, product.getQuantity());
|
||||
|
||||
if (tierConfig != null) {
|
||||
log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}",
|
||||
productType.getCode(), productId, product.getQuantity(), tierConfig.getPrice());
|
||||
return tierConfig.getPrice();
|
||||
}
|
||||
|
||||
// 使用基于product_id的基础配置
|
||||
try {
|
||||
PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId);
|
||||
if (baseConfig != null) {
|
||||
if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) {
|
||||
return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
} else {
|
||||
return baseConfig.getBasePrice();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置",
|
||||
productType, productId);
|
||||
}
|
||||
|
||||
// 兜底:使用default配置
|
||||
try {
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default");
|
||||
if (defaultConfig != null) {
|
||||
if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) {
|
||||
return defaultConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
} else {
|
||||
return defaultConfig.getBasePrice();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("未找到default配置: productType={}", productType.getCode());
|
||||
}
|
||||
|
||||
// 最后兜底:使用通用配置(向后兼容)
|
||||
List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode());
|
||||
if (!configs.isEmpty()) {
|
||||
PriceProductConfig baseConfig = configs.get(0); // 使用第一个配置作为默认
|
||||
if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) {
|
||||
return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
} else {
|
||||
return baseConfig.getBasePrice();
|
||||
}
|
||||
}
|
||||
|
||||
throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId);
|
||||
}
|
||||
|
||||
private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product) {
|
||||
ProductType productType = product.getProductType();
|
||||
String productId = product.getProductId() != null ? product.getProductId() : "default";
|
||||
|
||||
BigDecimal actualPrice;
|
||||
BigDecimal originalPrice = null;
|
||||
|
||||
// 优先使用基于product_id的阶梯定价
|
||||
PriceTierConfig tierConfig = productConfigService.getTierConfig(
|
||||
productType.getCode(), productId, product.getQuantity());
|
||||
|
||||
if (tierConfig != null) {
|
||||
actualPrice = tierConfig.getPrice();
|
||||
originalPrice = tierConfig.getOriginalPrice();
|
||||
log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}, originalPrice={}",
|
||||
productType.getCode(), productId, product.getQuantity(), actualPrice, originalPrice);
|
||||
} else {
|
||||
// 使用基于product_id的基础配置
|
||||
try {
|
||||
PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId);
|
||||
if (baseConfig != null) {
|
||||
actualPrice = baseConfig.getBasePrice();
|
||||
originalPrice = baseConfig.getOriginalPrice();
|
||||
|
||||
if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new PriceCalculationException("无法找到具体商品配置");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置",
|
||||
productType, productId);
|
||||
|
||||
// 兜底:使用default配置
|
||||
try {
|
||||
PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default");
|
||||
if (defaultConfig != null) {
|
||||
actualPrice = defaultConfig.getBasePrice();
|
||||
originalPrice = defaultConfig.getOriginalPrice();
|
||||
|
||||
if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new PriceCalculationException("无法找到default配置");
|
||||
}
|
||||
} catch (Exception defaultEx) {
|
||||
log.warn("未找到default配置: productType={}", productType.getCode());
|
||||
|
||||
// 最后兜底:使用通用配置(向后兼容)
|
||||
List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode());
|
||||
if (!configs.isEmpty()) {
|
||||
PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认
|
||||
actualPrice = baseConfig.getBasePrice();
|
||||
originalPrice = baseConfig.getOriginalPrice();
|
||||
|
||||
if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) {
|
||||
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
if (originalPrice != null) {
|
||||
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ProductPriceInfo(actualPrice, originalPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算优惠(券码 + 优惠券)
|
||||
*/
|
||||
private DiscountCombinationResult calculateDiscounts(PriceCalculationRequest request, BigDecimal currentAmount) {
|
||||
try {
|
||||
// 构建优惠检测上下文
|
||||
DiscountDetectionContext context = new DiscountDetectionContext();
|
||||
context.setUserId(request.getUserId());
|
||||
context.setFaceId(request.getFaceId());
|
||||
context.setScenicId(request.getScenicId());
|
||||
context.setProducts(request.getProducts());
|
||||
context.setCurrentAmount(currentAmount);
|
||||
context.setVoucherCode(request.getVoucherCode());
|
||||
context.setAutoUseCoupon(request.getAutoUseCoupon());
|
||||
context.setAutoUseVoucher(request.getAutoUseVoucher());
|
||||
|
||||
// 使用优惠检测服务计算最优组合
|
||||
if (Boolean.TRUE.equals(request.getPreviewOnly())) {
|
||||
return discountDetectionService.previewOptimalCombination(context);
|
||||
} else {
|
||||
return discountDetectionService.calculateOptimalCombination(context);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("计算优惠时发生异常", e);
|
||||
|
||||
// 返回失败结果
|
||||
DiscountCombinationResult failureResult = new DiscountCombinationResult();
|
||||
failureResult.setOriginalAmount(currentAmount);
|
||||
failureResult.setFinalAmount(currentAmount);
|
||||
failureResult.setTotalDiscountAmount(BigDecimal.ZERO);
|
||||
failureResult.setSuccess(false);
|
||||
failureResult.setErrorMessage("优惠计算失败: " + e.getMessage());
|
||||
return failureResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置使用的优惠信息到结果中
|
||||
*/
|
||||
private void setUsedDiscountInfo(PriceCalculationResult result, DiscountCombinationResult discountResult, PriceCalculationRequest request) {
|
||||
if (discountResult.getAppliedDiscounts() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (DiscountResult discountApplied : discountResult.getAppliedDiscounts()) {
|
||||
DiscountInfo discountInfo = discountApplied.getDiscountInfo();
|
||||
|
||||
if ("COUPON".equals(discountInfo.getProviderType()) && discountInfo.getCouponId() != null) {
|
||||
// 构建优惠券信息(这里可能需要重新查询完整信息)
|
||||
CouponInfo couponInfo = new CouponInfo();
|
||||
couponInfo.setCouponId(discountInfo.getCouponId());
|
||||
couponInfo.setCouponName(discountInfo.getDiscountName());
|
||||
couponInfo.setActualDiscountAmount(discountApplied.getActualDiscountAmount());
|
||||
result.setUsedCoupon(couponInfo);
|
||||
|
||||
} else if ("VOUCHER".equals(discountInfo.getProviderType()) && discountInfo.getVoucherCode() != null) {
|
||||
// 获取券码信息
|
||||
VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo(
|
||||
discountInfo.getVoucherCode(),
|
||||
request.getFaceId(),
|
||||
request.getScenicId()
|
||||
);
|
||||
if (voucherInfo != null) {
|
||||
voucherInfo.setActualDiscountAmount(discountApplied.getActualDiscountAmount());
|
||||
result.setUsedVoucher(voucherInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记优惠为已使用(仅在非预览模式下调用)
|
||||
*/
|
||||
private void markDiscountsAsUsed(PriceCalculationResult result, PriceCalculationRequest request) {
|
||||
try {
|
||||
// 标记券码为已使用
|
||||
if (result.getUsedVoucher() != null && result.getUsedVoucher().getVoucherCode() != null) {
|
||||
String remark = String.format("价格计算使用 - 订单金额: %s", result.getFinalAmount());
|
||||
voucherService.markVoucherAsUsed(result.getUsedVoucher().getVoucherCode(), remark);
|
||||
log.info("已标记券码为使用: {}", result.getUsedVoucher().getVoucherCode());
|
||||
}
|
||||
|
||||
// 优惠券的使用标记由原有的CouponService处理
|
||||
// 这里不需要额外处理
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("标记优惠使用状态时发生异常", e);
|
||||
// 不抛出异常,避免影响主流程
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,168 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.pricing.entity.*;
|
||||
import com.ycwl.basic.pricing.mapper.*;
|
||||
import com.ycwl.basic.pricing.service.IPricingManagementService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 价格管理服务实现(用于配置管理,手动处理时间字段)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("pricingManagementServiceImpl")
|
||||
@RequiredArgsConstructor
|
||||
public class PricingManagementServiceImpl implements IPricingManagementService {
|
||||
|
||||
private final PriceProductConfigMapper productConfigMapper;
|
||||
private final PriceTierConfigMapper tierConfigMapper;
|
||||
private final PriceCouponConfigMapper couponConfigMapper;
|
||||
private final PriceCouponClaimRecordMapper couponClaimRecordMapper;
|
||||
private final PriceBundleConfigMapper bundleConfigMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createProductConfig(PriceProductConfig config) {
|
||||
// 校验:如果是default配置,确保该商品类型只能有一个default配置
|
||||
if ("default".equals(config.getProductId())) {
|
||||
int existingCount = productConfigMapper.countDefaultConfigsByProductType(config.getProductType());
|
||||
if (existingCount > 0) {
|
||||
throw new IllegalArgumentException("商品类型 " + config.getProductType() + " 的default配置已存在,每种商品类型只能有一个default配置");
|
||||
}
|
||||
}
|
||||
|
||||
config.setCreateTime(new Date());
|
||||
config.setUpdateTime(new Date());
|
||||
productConfigMapper.insertProductConfig(config);
|
||||
return config.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateProductConfig(PriceProductConfig config) {
|
||||
config.setUpdateTime(new Date());
|
||||
return productConfigMapper.updateProductConfig(config) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createTierConfig(PriceTierConfig config) {
|
||||
// 校验:如果是default配置,检查是否可以创建
|
||||
if ("default".equals(config.getProductId())) {
|
||||
// 对于阶梯配置,可以有多个default配置(不同数量区间),不需要限制
|
||||
log.info("创建default阶梯配置: productType={}, quantity range: {}-{}",
|
||||
config.getProductType(), config.getMinQuantity(), config.getMaxQuantity());
|
||||
}
|
||||
|
||||
config.setCreateTime(new Date());
|
||||
config.setUpdateTime(new Date());
|
||||
tierConfigMapper.insertTierConfig(config);
|
||||
return config.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateTierConfig(PriceTierConfig config) {
|
||||
config.setUpdateTime(new Date());
|
||||
return tierConfigMapper.updateTierConfig(config) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createCouponConfig(PriceCouponConfig config) {
|
||||
config.setCreateTime(new Date());
|
||||
config.setUpdateTime(new Date());
|
||||
couponConfigMapper.insertCoupon(config);
|
||||
return config.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateCouponConfig(PriceCouponConfig config) {
|
||||
config.setUpdateTime(new Date());
|
||||
return couponConfigMapper.updateCoupon(config) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createCouponClaimRecord(PriceCouponClaimRecord record) {
|
||||
record.setClaimTime(new Date());
|
||||
record.setCreateTime(new Date());
|
||||
record.setUpdateTime(new Date());
|
||||
couponClaimRecordMapper.insertClaimRecord(record);
|
||||
return record.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) {
|
||||
record.setUpdateTime(new Date());
|
||||
return couponClaimRecordMapper.updateClaimRecord(record) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createBundleConfig(PriceBundleConfig config) {
|
||||
config.setCreateTime(new Date());
|
||||
config.setUpdateTime(new Date());
|
||||
bundleConfigMapper.insertBundleConfig(config);
|
||||
return config.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateBundleConfig(PriceBundleConfig config) {
|
||||
config.setUpdateTime(new Date());
|
||||
return bundleConfigMapper.updateBundleConfig(config) > 0;
|
||||
}
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateProductConfigStatus(Long id, Boolean isActive) {
|
||||
log.info("更新商品配置状态: id={}, isActive={}", id, isActive);
|
||||
return productConfigMapper.updateProductConfigStatus(id, isActive) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateTierConfigStatus(Long id, Boolean isActive) {
|
||||
log.info("更新阶梯配置状态: id={}, isActive={}", id, isActive);
|
||||
return tierConfigMapper.updateTierConfigStatus(id, isActive) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateBundleConfigStatus(Long id, Boolean isActive) {
|
||||
log.info("更新一口价配置状态: id={}, isActive={}", id, isActive);
|
||||
return bundleConfigMapper.updateBundleConfigStatus(id, isActive) > 0;
|
||||
}
|
||||
|
||||
// ==================== 删除操作 ====================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deleteProductConfig(Long id) {
|
||||
log.info("删除商品配置: id={}", id);
|
||||
return productConfigMapper.deleteById(id) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deleteTierConfig(Long id) {
|
||||
log.info("删除阶梯配置: id={}", id);
|
||||
return tierConfigMapper.deleteById(id) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deleteBundleConfig(Long id) {
|
||||
log.info("删除一口价配置: id={}", id);
|
||||
return bundleConfigMapper.deleteById(id) > 0;
|
||||
}
|
||||
}
|
@@ -0,0 +1,117 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.pricing.entity.PriceProductConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
||||
import com.ycwl.basic.pricing.exception.ProductConfigNotFoundException;
|
||||
import com.ycwl.basic.pricing.mapper.PriceProductConfigMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceTierConfigMapper;
|
||||
import com.ycwl.basic.pricing.service.IProductConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
//import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品配置管理服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("pricingProductConfigServiceImpl")
|
||||
@RequiredArgsConstructor
|
||||
public class ProductConfigServiceImpl implements IProductConfigService {
|
||||
|
||||
private final PriceProductConfigMapper productConfigMapper;
|
||||
private final PriceTierConfigMapper tierConfigMapper;
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "product-config", key = "#productType")
|
||||
public List<PriceProductConfig> getProductConfig(String productType) {
|
||||
return productConfigMapper.selectByProductType(productType);
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "product-config", key = "#productType + '_' + #productId")
|
||||
public PriceProductConfig getProductConfig(String productType, String productId) {
|
||||
PriceProductConfig config = productConfigMapper.selectByProductTypeAndId(productType, productId);
|
||||
if (config == null) {
|
||||
throw new ProductConfigNotFoundException("商品配置未找到: " + productType + ", productId: " + productId);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + #quantity")
|
||||
public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity) {
|
||||
PriceTierConfig config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity);
|
||||
|
||||
// 如果没有找到特定商品的阶梯配置,尝试使用default配置
|
||||
if (config == null && !"default".equals(productId)) {
|
||||
log.warn("阶梯定价配置未找到: productType={}, productId={}, quantity={}, 尝试使用default配置",
|
||||
productType, productId, quantity);
|
||||
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, "default", quantity);
|
||||
if (config != null) {
|
||||
log.debug("使用default阶梯配置: productType={}, quantity={}, price={}",
|
||||
productType, quantity, config.getPrice());
|
||||
}
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
log.warn("阶梯定价配置未找到: productType={}, productId={}, quantity={}",
|
||||
productType, productId, quantity);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "active-product-configs")
|
||||
public List<PriceProductConfig> getActiveProductConfigs() {
|
||||
return productConfigMapper.selectActiveConfigs();
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "all-product-configs")
|
||||
public List<PriceProductConfig> getAllProductConfigs() {
|
||||
return productConfigMapper.selectActiveConfigs();
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "tier-configs", key = "#productType")
|
||||
public List<PriceTierConfig> getTierConfigs(String productType) {
|
||||
return tierConfigMapper.selectByProductType(productType);
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "tier-configs", key = "#productType + '_' + #productId")
|
||||
public List<PriceTierConfig> getTierConfigs(String productType, String productId) {
|
||||
return tierConfigMapper.selectByProductTypeAndId(productType, productId);
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Cacheable(value = "all-tier-configs")
|
||||
public List<PriceTierConfig> getAllTierConfigs() {
|
||||
return tierConfigMapper.selectAllActiveConfigs();
|
||||
}
|
||||
|
||||
// ==================== 管理端接口(包含禁用的配置) ====================
|
||||
|
||||
@Override
|
||||
public List<PriceProductConfig> getAllProductConfigsForAdmin() {
|
||||
return productConfigMapper.selectAllConfigsForAdmin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceTierConfig> getAllTierConfigsForAdmin() {
|
||||
return tierConfigMapper.selectAllConfigsForAdmin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceTierConfig> getTierConfigsForAdmin(String productType) {
|
||||
return tierConfigMapper.selectByProductTypeForAdmin(productType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceTierConfig> getTierConfigsForAdmin(String productType, String productId) {
|
||||
return tierConfigMapper.selectByProductTypeAndIdForAdmin(productType, productId);
|
||||
}
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
||||
import com.ycwl.basic.pricing.service.VoucherBatchService;
|
||||
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class VoucherBatchServiceImpl implements VoucherBatchService {
|
||||
@Autowired
|
||||
private PriceVoucherBatchConfigMapper voucherBatchMapper;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private VoucherCodeService voucherCodeService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createBatch(VoucherBatchCreateReq req) {
|
||||
if (req.getBatchName() == null || req.getBatchName().trim().isEmpty()) {
|
||||
throw new BizException(400, "券码批次名称不能为空");
|
||||
}
|
||||
if (req.getScenicId() == null) {
|
||||
throw new BizException(400, "景区ID不能为空");
|
||||
}
|
||||
if (req.getBrokerId() == null) {
|
||||
throw new BizException(400, "推客ID不能为空");
|
||||
}
|
||||
if (req.getDiscountType() == null) {
|
||||
throw new BizException(400, "优惠类型不能为空");
|
||||
}
|
||||
if (req.getTotalCount() == null || req.getTotalCount() < 1) {
|
||||
throw new BizException(400, "券码数量必须大于0");
|
||||
}
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(req.getDiscountType());
|
||||
if (discountType == null) {
|
||||
throw new BizException(400, "无效的优惠类型");
|
||||
}
|
||||
|
||||
if (discountType != VoucherDiscountType.FREE_ALL && req.getDiscountValue() == null) {
|
||||
throw new BizException(400, "优惠金额不能为空");
|
||||
}
|
||||
|
||||
PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig();
|
||||
BeanUtils.copyProperties(req, batch);
|
||||
batch.setUsedCount(0);
|
||||
batch.setClaimedCount(0);
|
||||
batch.setStatus(1);
|
||||
batch.setCreateTime(new Date());
|
||||
String userIdStr = BaseContextHandler.getUserId();
|
||||
if (userIdStr != null) {
|
||||
batch.setCreateBy(Long.valueOf(userIdStr));
|
||||
}
|
||||
batch.setDeleted(0);
|
||||
|
||||
voucherBatchMapper.insert(batch);
|
||||
|
||||
voucherCodeService.generateVoucherCodes(batch.getId(), req.getScenicId(), req.getTotalCount());
|
||||
|
||||
return batch.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
|
||||
Page<PriceVoucherBatchConfig> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<PriceVoucherBatchConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherBatchConfig::getDeleted, 0)
|
||||
.eq(req.getScenicId() != null, PriceVoucherBatchConfig::getScenicId, req.getScenicId())
|
||||
.eq(req.getBrokerId() != null, PriceVoucherBatchConfig::getBrokerId, req.getBrokerId())
|
||||
.eq(req.getStatus() != null, PriceVoucherBatchConfig::getStatus, req.getStatus())
|
||||
.like(StringUtils.hasText(req.getBatchName()), PriceVoucherBatchConfig::getBatchName, req.getBatchName())
|
||||
.orderByDesc(PriceVoucherBatchConfig::getCreateTime);
|
||||
|
||||
Page<PriceVoucherBatchConfig> entityPage = voucherBatchMapper.selectPage(page, wrapper);
|
||||
|
||||
Page<VoucherBatchResp> respPage = new Page<>();
|
||||
BeanUtils.copyProperties(entityPage, respPage);
|
||||
|
||||
respPage.setRecords(entityPage.getRecords().stream().map(this::convertToResp).toList());
|
||||
|
||||
return respPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherBatchResp getBatchDetail(Long id) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id);
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
|
||||
return convertToResp(batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherBatchStatsResp getBatchStats(Long id) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id);
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
|
||||
VoucherBatchStatsResp stats = new VoucherBatchStatsResp();
|
||||
stats.setBatchId(batch.getId());
|
||||
stats.setBatchName(batch.getBatchName());
|
||||
stats.setTotalCount(batch.getTotalCount());
|
||||
stats.setClaimedCount(batch.getClaimedCount());
|
||||
stats.setUsedCount(batch.getUsedCount());
|
||||
stats.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount());
|
||||
|
||||
if (batch.getTotalCount() > 0) {
|
||||
stats.setClaimedRate((double) batch.getClaimedCount() / batch.getTotalCount() * 100);
|
||||
stats.setUsedRate((double) batch.getUsedCount() / batch.getTotalCount() * 100);
|
||||
} else {
|
||||
stats.setClaimedRate(0.0);
|
||||
stats.setUsedRate(0.0);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchStatus(Long id, Integer status) {
|
||||
PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig();
|
||||
batch.setId(id);
|
||||
batch.setStatus(status);
|
||||
|
||||
int updated = voucherBatchMapper.updateById(batch);
|
||||
if (updated == 0) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchClaimedCount(Long batchId) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId);
|
||||
if (batch != null) {
|
||||
batch.setClaimedCount(batch.getClaimedCount() + 1);
|
||||
voucherBatchMapper.updateById(batch);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchUsedCount(Long batchId) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId);
|
||||
if (batch != null) {
|
||||
batch.setUsedCount(batch.getUsedCount() + 1);
|
||||
voucherBatchMapper.updateById(batch);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId) {
|
||||
LambdaQueryWrapper<PriceVoucherBatchConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherBatchConfig::getScenicId, scenicId)
|
||||
.eq(PriceVoucherBatchConfig::getBrokerId, brokerId)
|
||||
.eq(PriceVoucherBatchConfig::getStatus, 1)
|
||||
.eq(PriceVoucherBatchConfig::getDeleted, 0)
|
||||
.apply("claimed_count < total_count")
|
||||
.orderByDesc(PriceVoucherBatchConfig::getCreateTime);
|
||||
|
||||
return voucherBatchMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
private VoucherBatchResp convertToResp(PriceVoucherBatchConfig batch) {
|
||||
VoucherBatchResp resp = new VoucherBatchResp();
|
||||
BeanUtils.copyProperties(batch, resp);
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType());
|
||||
if (discountType != null) {
|
||||
resp.setDiscountTypeName(discountType.getName());
|
||||
}
|
||||
|
||||
resp.setStatusName(batch.getStatus() == 1 ? "启用" : "禁用");
|
||||
resp.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount());
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
@@ -0,0 +1,277 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherCode;
|
||||
import com.ycwl.basic.pricing.enums.VoucherCodeStatus;
|
||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper;
|
||||
import com.ycwl.basic.pricing.service.VoucherBatchService;
|
||||
import com.ycwl.basic.pricing.service.VoucherCodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VoucherCodeServiceImpl implements VoucherCodeService {
|
||||
|
||||
// 券码生成相关常量
|
||||
private static final String SAFE_CHARS = "23456789ABCDEFGHJKMNPQRTUVWXYZ";
|
||||
private static final char[] SAFE_CHARS_ARRAY = SAFE_CHARS.toCharArray();
|
||||
private static final int CODE_LENGTH = 6;
|
||||
private static final int MAX_RETRY = 10;
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
@Autowired
|
||||
private PriceVoucherCodeMapper voucherCodeMapper;
|
||||
@Autowired
|
||||
private PriceVoucherBatchConfigMapper voucherBatchMapper;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private VoucherBatchService voucherBatchService;
|
||||
|
||||
@Override
|
||||
public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
|
||||
List<PriceVoucherCode> codes = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
PriceVoucherCode code = new PriceVoucherCode();
|
||||
code.setBatchId(batchId);
|
||||
code.setScenicId(scenicId);
|
||||
code.setCode(generateVoucherCode());
|
||||
code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode());
|
||||
code.setCreateTime(new Date());
|
||||
code.setDeleted(0);
|
||||
codes.add(code);
|
||||
}
|
||||
|
||||
for (PriceVoucherCode code : codes) {
|
||||
voucherCodeMapper.insert(code);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
|
||||
if (req.getScenicId() == null) {
|
||||
throw new BizException(400, "景区ID不能为空");
|
||||
}
|
||||
if (req.getBrokerId() == null) {
|
||||
throw new BizException(400, "推客ID不能为空");
|
||||
}
|
||||
if (req.getFaceId() == null) {
|
||||
throw new BizException(400, "用户faceId不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(req.getCode())) {
|
||||
throw new BizException(400, "券码不能为空");
|
||||
}
|
||||
|
||||
// 验证券码是否存在且未被领取
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getCode, req.getCode())
|
||||
.eq(PriceVoucherCode::getScenicId, req.getScenicId())
|
||||
.eq(PriceVoucherCode::getDeleted, 0);
|
||||
|
||||
PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper);
|
||||
if (voucherCode == null) {
|
||||
throw new BizException(400, "券码不存在或不属于该景区");
|
||||
}
|
||||
|
||||
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
|
||||
throw new BizException(400, "券码已被领取或已使用");
|
||||
}
|
||||
|
||||
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
|
||||
throw new BizException(400, "该用户在此景区已领取过券码");
|
||||
}
|
||||
|
||||
// 获取券码所属批次
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId());
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(400, "券码批次不存在");
|
||||
}
|
||||
|
||||
// 验证批次是否可用于该推客
|
||||
if (!Objects.equals(batch.getBrokerId(), req.getBrokerId())) {
|
||||
throw new BizException(400, "券码不属于该推客");
|
||||
}
|
||||
|
||||
// 更新券码状态
|
||||
voucherCode.setFaceId(req.getFaceId());
|
||||
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
|
||||
voucherCode.setClaimedTime(new Date());
|
||||
|
||||
voucherCodeMapper.updateById(voucherCode);
|
||||
|
||||
voucherBatchService.updateBatchClaimedCount(batch.getId());
|
||||
|
||||
return convertToResp(voucherCode, batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) {
|
||||
Page<PriceVoucherCode> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getDeleted, 0)
|
||||
.eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId())
|
||||
.eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId())
|
||||
.eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId())
|
||||
.eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus())
|
||||
.like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode())
|
||||
.orderByDesc(PriceVoucherCode::getId);
|
||||
|
||||
Page<PriceVoucherCode> entityPage = voucherCodeMapper.selectPage(page, wrapper);
|
||||
|
||||
Page<VoucherCodeResp> respPage = new Page<>();
|
||||
BeanUtils.copyProperties(entityPage, respPage);
|
||||
|
||||
List<VoucherCodeResp> respList = new ArrayList<>();
|
||||
for (PriceVoucherCode code : entityPage.getRecords()) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId());
|
||||
respList.add(convertToResp(code, batch));
|
||||
}
|
||||
respPage.setRecords(respList);
|
||||
|
||||
return respPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoucherCodeResp> getMyVoucherCodes(Long faceId) {
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getFaceId, faceId)
|
||||
.eq(PriceVoucherCode::getDeleted, 0)
|
||||
.orderByDesc(PriceVoucherCode::getClaimedTime);
|
||||
|
||||
List<PriceVoucherCode> codes = voucherCodeMapper.selectList(wrapper);
|
||||
|
||||
List<VoucherCodeResp> respList = new ArrayList<>();
|
||||
for (PriceVoucherCode code : codes) {
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId());
|
||||
respList.add(convertToResp(code, batch));
|
||||
}
|
||||
|
||||
return respList;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void markCodeAsUsed(Long codeId, String remark) {
|
||||
PriceVoucherCode code = voucherCodeMapper.selectById(codeId);
|
||||
if (code == null || code.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码不存在");
|
||||
}
|
||||
|
||||
if (!Objects.equals(code.getStatus(), VoucherCodeStatus.CLAIMED_UNUSED.getCode())) {
|
||||
throw new BizException(400, "券码状态异常,无法使用");
|
||||
}
|
||||
|
||||
code.setStatus(VoucherCodeStatus.USED.getCode());
|
||||
code.setUsedTime(new Date());
|
||||
code.setRemark(remark);
|
||||
|
||||
voucherCodeMapper.updateById(code);
|
||||
|
||||
voucherBatchService.updateBatchUsedCount(code.getBatchId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成6位安全券码(去除易混淆字符)
|
||||
* 字符集:数字2-9 + 大写字母(去除0,1,I,L,O,S)
|
||||
*
|
||||
* @return 6位券码
|
||||
*/
|
||||
private String generateVoucherCode() {
|
||||
for (int attempt = 0; attempt < MAX_RETRY; attempt++) {
|
||||
String code = generateRandomCode();
|
||||
log.debug("生成券码候选: {} (尝试第{}次)", code, attempt + 1);
|
||||
|
||||
if (!isCodeExists(code)) {
|
||||
log.info("成功生成券码: {} (字符集大小: {}, 理论组合数: {})",
|
||||
code, SAFE_CHARS.length(), Math.pow(SAFE_CHARS.length(), CODE_LENGTH));
|
||||
return code;
|
||||
}
|
||||
|
||||
log.warn("券码重复,重新生成: {}", code);
|
||||
}
|
||||
|
||||
// 如果重试次数用完仍有重复,抛出异常
|
||||
log.error("券码生成失败:达到最大重试次数 {}", MAX_RETRY);
|
||||
throw new RuntimeException("券码生成失败:达到最大重试次数,请稍后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机6位字符
|
||||
*
|
||||
* @return 随机6位字符
|
||||
*/
|
||||
private String generateRandomCode() {
|
||||
StringBuilder code = new StringBuilder(CODE_LENGTH);
|
||||
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||
int randomIndex = RANDOM.nextInt(SAFE_CHARS_ARRAY.length);
|
||||
code.append(SAFE_CHARS_ARRAY[randomIndex]);
|
||||
}
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查券码是否已存在
|
||||
*
|
||||
* @param code 券码
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean isCodeExists(String code) {
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getCode, code)
|
||||
.eq(PriceVoucherCode::getDeleted, 0);
|
||||
return voucherCodeMapper.selectCount(wrapper) > 0;
|
||||
}
|
||||
|
||||
private VoucherCodeResp convertToResp(PriceVoucherCode code, PriceVoucherBatchConfig batch) {
|
||||
VoucherCodeResp resp = new VoucherCodeResp();
|
||||
BeanUtils.copyProperties(code, resp);
|
||||
|
||||
if (batch != null) {
|
||||
resp.setBatchName(batch.getBatchName());
|
||||
resp.setDiscountType(batch.getDiscountType());
|
||||
resp.setDiscountValue(batch.getDiscountValue());
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType());
|
||||
if (discountType != null) {
|
||||
resp.setDiscountTypeName(discountType.getName());
|
||||
resp.setDiscountDescription(discountType.getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
VoucherCodeStatus status = VoucherCodeStatus.getByCode(code.getStatus());
|
||||
if (status != null) {
|
||||
resp.setStatusName(status.getName());
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
@@ -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,205 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherCode;
|
||||
import com.ycwl.basic.pricing.entity.VoucherPrintRecord;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper;
|
||||
import com.ycwl.basic.pricing.mapper.VoucherPrintRecordMapper;
|
||||
import com.ycwl.basic.pricing.service.VoucherPrintService;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 优惠券打印服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VoucherPrintServiceImpl implements VoucherPrintService {
|
||||
|
||||
@Autowired
|
||||
private VoucherPrintRecordMapper voucherPrintRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private PriceVoucherCodeMapper priceVoucherCodeMapper;
|
||||
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private static final String PRINT_LOCK_KEY = "voucher_print_lock:%s:%s:%s"; // faceId:brokerId:scenicId
|
||||
|
||||
// 原子计数器,确保流水号唯一性
|
||||
private static final AtomicLong counter = new AtomicLong(0);
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public VoucherPrintResp printVoucherTicket(VoucherPrintReq request) {
|
||||
// 参数验证
|
||||
if (request.getFaceId() == null) {
|
||||
throw new BaseException("用户faceId不能为空");
|
||||
}
|
||||
if (request.getBrokerId() == null) {
|
||||
throw new BaseException("经纪人ID不能为空");
|
||||
}
|
||||
FaceEntity face = faceRepository.getFace(request.getFaceId());
|
||||
if (face == null) {
|
||||
throw new BaseException("请上传人脸");
|
||||
}
|
||||
request.setScenicId(face.getScenicId());
|
||||
|
||||
Long currentUserId = Long.valueOf(BaseContextHandler.getUserId());
|
||||
|
||||
// 验证faceId是否属于当前用户
|
||||
validateFaceOwnership(request.getFaceId(), currentUserId);
|
||||
|
||||
// 使用Redis分布式锁防止重复打印
|
||||
String lockKey = String.format(PRINT_LOCK_KEY, request.getFaceId(), request.getBrokerId(), request.getScenicId());
|
||||
String lockValue = UUID.randomUUID().toString();
|
||||
|
||||
try {
|
||||
// 尝试获取锁,超时时间30秒
|
||||
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
|
||||
if (Boolean.FALSE.equals(lockAcquired)) {
|
||||
throw new BaseException("请求处理中,请稍后再试");
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的打印记录
|
||||
VoucherPrintRecord existingRecord = voucherPrintRecordMapper.selectByFaceBrokerScenic(
|
||||
request.getFaceId(), request.getScenicId());
|
||||
|
||||
if (existingRecord != null) {
|
||||
log.info("找到已存在的打印记录,返回该记录: {}", existingRecord.getId());
|
||||
return buildResponse(existingRecord);
|
||||
}
|
||||
|
||||
// 获取一个可用的券码(未被打印过的)
|
||||
PriceVoucherCode availableVoucher = getAvailableUnprintedVoucher(request.getScenicId());
|
||||
if (availableVoucher == null) {
|
||||
throw new BaseException("暂无可用优惠券");
|
||||
}
|
||||
|
||||
// 生成流水号
|
||||
String code = generateCode();
|
||||
|
||||
// 创建打印记录
|
||||
VoucherPrintRecord printRecord = new VoucherPrintRecord();
|
||||
printRecord.setCode(code);
|
||||
printRecord.setFaceId(request.getFaceId());
|
||||
printRecord.setBrokerId(request.getBrokerId());
|
||||
printRecord.setScenicId(request.getScenicId());
|
||||
printRecord.setVoucherCodeId(availableVoucher.getId());
|
||||
printRecord.setVoucherCode(availableVoucher.getCode());
|
||||
printRecord.setPrintStatus(0); // 待打印
|
||||
printRecord.setCreateTime(new Date());
|
||||
printRecord.setUpdateTime(new Date());
|
||||
printRecord.setDeleted(0);
|
||||
|
||||
voucherPrintRecordMapper.insert(printRecord);
|
||||
|
||||
// TODO: 调用打印机接口打印小票
|
||||
// printTicket(printRecord);
|
||||
|
||||
// 暂时标记为打印成功状态(实际应该在打印成功回调中更新)
|
||||
printRecord.setPrintStatus(1);
|
||||
voucherPrintRecordMapper.updatePrintStatus(printRecord.getId(), 1, null);
|
||||
|
||||
log.info("成功创建打印记录: {}, 券码: {}", code, availableVoucher.getCode());
|
||||
return buildResponse(printRecord);
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
|
||||
redisTemplate.delete(lockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherPrintResp queryPrintedVoucher(Long faceId) {
|
||||
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
throw new BaseException("请上传人脸");
|
||||
}
|
||||
|
||||
Long currentUserId = Long.valueOf(BaseContextHandler.getUserId());
|
||||
|
||||
// 验证faceId是否属于当前用户
|
||||
validateFaceOwnership(faceId, currentUserId);
|
||||
// 检查是否已存在相同的打印记录
|
||||
VoucherPrintRecord existingRecord = voucherPrintRecordMapper.selectByFaceBrokerScenic(
|
||||
faceId, face.getScenicId());
|
||||
if (existingRecord == null) {
|
||||
return null;
|
||||
}
|
||||
return buildResponse(existingRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证faceId是否属于当前用户
|
||||
*/
|
||||
private void validateFaceOwnership(Long faceId, Long currentUserId) {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
throw new BaseException("用户人脸信息不存在");
|
||||
}
|
||||
|
||||
if (!currentUserId.equals(face.getMemberId())) {
|
||||
throw new BaseException("无权限操作该人脸信息");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用且未被打印过的券码
|
||||
*/
|
||||
private PriceVoucherCode getAvailableUnprintedVoucher(Long scenicId) {
|
||||
return priceVoucherCodeMapper.findRandomUnprintedVoucher(scenicId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成流水号(优化版本)
|
||||
* 使用原子计数器确保唯一性,解决原方法在高并发下的重复问题
|
||||
*/
|
||||
private String generateCode() {
|
||||
// 方案:使用毫秒级时间戳 + 原子计数器
|
||||
// 格式:5位计数器 + SSS
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("SSS");
|
||||
String timestamp = sdf.format(new Date());
|
||||
long count = counter.incrementAndGet() % 100000; // 5位计数,循环使用
|
||||
return String.format("%05d", count) + timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建响应对象
|
||||
*/
|
||||
private VoucherPrintResp buildResponse(VoucherPrintRecord record) {
|
||||
VoucherPrintResp response = new VoucherPrintResp();
|
||||
BeanUtils.copyProperties(record, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用打印机接口(待实现)
|
||||
*/
|
||||
private void printTicket(VoucherPrintRecord record) {
|
||||
// TODO: 实现打印机接口调用逻辑
|
||||
log.info("TODO: 调用打印机打印小票,记录ID: {}, 券码: {}", record.getId(), record.getVoucherCode());
|
||||
}
|
||||
}
|
@@ -0,0 +1,273 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherCode;
|
||||
import com.ycwl.basic.pricing.enums.VoucherCodeStatus;
|
||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper;
|
||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 券码服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoucherServiceImpl implements IVoucherService {
|
||||
|
||||
private final PriceVoucherCodeMapper voucherCodeMapper;
|
||||
private final PriceVoucherBatchConfigMapper voucherBatchConfigMapper;
|
||||
|
||||
@Override
|
||||
public VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId) {
|
||||
if (!StringUtils.hasText(voucherCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查询券码信息
|
||||
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
|
||||
if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查询批次信息
|
||||
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId());
|
||||
if (batchConfig == null || batchConfig.getDeleted() == 1 || batchConfig.getStatus() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证景区匹配
|
||||
if (scenicId != null && !scenicId.equals(voucherCodeEntity.getScenicId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoucherInfo voucherInfo = buildVoucherInfo(voucherCodeEntity, batchConfig);
|
||||
|
||||
// 检查券码状态和可用性
|
||||
if (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) {
|
||||
// 未领取状态,检查是否可以领取
|
||||
if (faceId != null && canClaimVoucher(faceId, voucherCodeEntity.getScenicId())) {
|
||||
voucherInfo.setAvailable(true);
|
||||
} else {
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("您已在该景区领取过券码");
|
||||
}
|
||||
} else if (VoucherCodeStatus.CLAIMED_UNUSED.getCode().equals(voucherCodeEntity.getStatus())) {
|
||||
// 已领取未使用,检查是否为当前用户
|
||||
if (faceId != null && faceId.equals(voucherCodeEntity.getFaceId())) {
|
||||
voucherInfo.setAvailable(true);
|
||||
} else {
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码已被其他用户领取");
|
||||
}
|
||||
} else {
|
||||
// 已使用
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码已使用");
|
||||
}
|
||||
|
||||
return voucherInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该faceId在scenicId下的券码详情列表
|
||||
* @param faceId 用户面部ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 券码详情列表,包含所有状态的券码(已领取未使用、已使用等),如果没有券码则返回空列表
|
||||
*/
|
||||
public List<VoucherInfo> getVoucherDetails(Long faceId, Long scenicId) {
|
||||
if (faceId == null || scenicId == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectUserVouchers(faceId, scenicId);
|
||||
List<VoucherInfo> voucherInfos = new ArrayList<>();
|
||||
|
||||
for (PriceVoucherCode voucherCode : voucherCodes) {
|
||||
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCode.getBatchId());
|
||||
if (batchConfig != null && batchConfig.getDeleted() == 0) {
|
||||
VoucherInfo voucherInfo = buildVoucherInfo(voucherCode, batchConfig);
|
||||
|
||||
// 设置可用性状态
|
||||
VoucherCodeStatus statusEnum = VoucherCodeStatus.getByCode(voucherCode.getStatus());
|
||||
if (statusEnum != null) {
|
||||
switch (statusEnum) {
|
||||
case CLAIMED_UNUSED:
|
||||
voucherInfo.setAvailable(true);
|
||||
break;
|
||||
case USED:
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码已使用");
|
||||
break;
|
||||
case UNCLAIMED:
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码未领取");
|
||||
break;
|
||||
default:
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码状态异常");
|
||||
}
|
||||
} else {
|
||||
voucherInfo.setAvailable(false);
|
||||
voucherInfo.setUnavailableReason("券码状态未知");
|
||||
}
|
||||
|
||||
voucherInfos.add(voucherInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return voucherInfos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context) {
|
||||
if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable()) ||
|
||||
context.getProducts() == null || context.getProducts().isEmpty()) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
VoucherDiscountType discountType = voucherInfo.getDiscountType();
|
||||
BigDecimal discountValue = voucherInfo.getDiscountValue();
|
||||
|
||||
if (discountType == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
return switch (discountType) {
|
||||
case FREE_ALL -> {
|
||||
// 全场免费,返回当前总金额
|
||||
yield context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO;
|
||||
}
|
||||
case REDUCE_PRICE -> {
|
||||
// 商品降价,每个商品减免固定金额
|
||||
if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
yield BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal totalDiscount = BigDecimal.ZERO;
|
||||
for (ProductItem product : context.getProducts()) {
|
||||
BigDecimal productDiscount = discountValue.multiply(BigDecimal.valueOf(product.getQuantity()));
|
||||
totalDiscount = totalDiscount.add(productDiscount);
|
||||
}
|
||||
yield totalDiscount;
|
||||
}
|
||||
case DISCOUNT -> {
|
||||
// 商品打折,按百分比计算
|
||||
if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0 ||
|
||||
discountValue.compareTo(BigDecimal.valueOf(100)) >= 0) {
|
||||
yield BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal currentAmount = context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO;
|
||||
BigDecimal discountRate = discountValue.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
|
||||
yield currentAmount.multiply(discountRate).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context) {
|
||||
List<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,8 +1,13 @@
|
||||
package com.ycwl.basic.repository;
|
||||
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
|
||||
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -10,12 +15,17 @@ import org.springframework.stereotype.Component;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SourceRepository {
|
||||
@Autowired
|
||||
private SourceMapper sourceMapper;
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
@Autowired
|
||||
private IVoucherService iVoucherService;
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
public void addSource(SourceEntity source) {
|
||||
sourceMapper.add(source);
|
||||
@@ -42,6 +52,19 @@ public class SourceRepository {
|
||||
}
|
||||
|
||||
public boolean getUserIsBuy(Long userId, int type, Long faceId) {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
log.info("faceId:{} is not exist", faceId);
|
||||
return false;
|
||||
}
|
||||
// 确认人员faceId是否有券码
|
||||
List<VoucherInfo> voucherDetails = iVoucherService.getVoucherDetails(faceId, face.getScenicId());
|
||||
if (voucherDetails != null && !voucherDetails.isEmpty()) {
|
||||
VoucherInfo voucherInfo = voucherDetails.getFirst();
|
||||
if (voucherInfo.getDiscountType().equals(VoucherDiscountType.FREE_ALL)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
switch (type) {
|
||||
case 1:
|
||||
List<SourceEntity> videoSourceList = sourceMapper.listVideoByFaceRelation(userId, faceId);
|
||||
|
@@ -3,6 +3,10 @@ package com.ycwl.basic.repository;
|
||||
import com.ycwl.basic.biz.PriceBiz;
|
||||
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
||||
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||
import com.ycwl.basic.utils.JacksonUtil;
|
||||
import com.ycwl.basic.mapper.VideoMapper;
|
||||
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||
@@ -15,6 +19,7 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
@@ -29,6 +34,8 @@ public class VideoRepository {
|
||||
@Autowired
|
||||
@Lazy
|
||||
private PriceBiz priceBiz;
|
||||
@Autowired
|
||||
private IVoucherService iVoucherService;
|
||||
|
||||
public VideoEntity getVideo(Long videoId) {
|
||||
if (redisTemplate.hasKey(String.format(VIDEO_CACHE_KEY, videoId))) {
|
||||
@@ -106,6 +113,14 @@ public class VideoRepository {
|
||||
if (buy.isBuy()) {
|
||||
return true;
|
||||
}
|
||||
// 确认人员faceId是否有券码
|
||||
List<VoucherInfo> voucherDetails = iVoucherService.getVoucherDetails(memberVideo.getFaceId(), memberVideo.getScenicId());
|
||||
if (voucherDetails != null && !voucherDetails.isEmpty()) {
|
||||
VoucherInfo voucherInfo = voucherDetails.getFirst();
|
||||
if (voucherInfo.getDiscountType().equals(VoucherDiscountType.FREE_ALL)) {
|
||||
isBuy = true;
|
||||
}
|
||||
}
|
||||
return isBuy;
|
||||
}
|
||||
|
||||
|
126
src/main/resources/mapper/PriceVoucherCodeMapper.xml
Normal file
126
src/main/resources/mapper/PriceVoucherCodeMapper.xml
Normal file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.pricing.entity.PriceVoucherCode">
|
||||
<id column="id" property="id" jdbcType="BIGINT"/>
|
||||
<result column="batch_id" property="batchId" jdbcType="BIGINT"/>
|
||||
<result column="scenic_id" property="scenicId" jdbcType="BIGINT"/>
|
||||
<result column="code" property="code" jdbcType="VARCHAR"/>
|
||||
<result column="status" property="status" jdbcType="TINYINT"/>
|
||||
<result column="face_id" property="faceId" jdbcType="BIGINT"/>
|
||||
<result column="claimed_time" property="claimedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="used_time" property="usedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="remark" property="remark" jdbcType="VARCHAR"/>
|
||||
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="deleted" property="deleted" jdbcType="TINYINT"/>
|
||||
<result column="deleted_at" property="deletedAt" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time,
|
||||
remark, create_time, update_time, deleted, deleted_at
|
||||
</sql>
|
||||
|
||||
<select id="selectByCode" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM price_voucher_code
|
||||
WHERE code = #{code}
|
||||
AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="countByFaceIdAndScenicId" resultType="java.lang.Integer">
|
||||
SELECT COUNT(1)
|
||||
FROM price_voucher_code
|
||||
WHERE face_id = #{faceId}
|
||||
AND scenic_id = #{scenicId}
|
||||
AND deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="selectAvailableVouchersByFaceIdAndScenicId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM price_voucher_code
|
||||
WHERE face_id = #{faceId}
|
||||
AND scenic_id = #{scenicId}
|
||||
AND status = 1
|
||||
AND deleted = 0
|
||||
ORDER BY claimed_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectUnclaimedVouchersByBatchId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM price_voucher_code
|
||||
WHERE batch_id = #{batchId}
|
||||
AND status = 0
|
||||
AND deleted = 0
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<update id="claimVoucher">
|
||||
UPDATE price_voucher_code
|
||||
SET status = 1,
|
||||
face_id = #{faceId},
|
||||
claimed_time = #{claimedTime},
|
||||
update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
AND status = 0
|
||||
AND deleted = 0
|
||||
</update>
|
||||
|
||||
<update id="useVoucher">
|
||||
UPDATE price_voucher_code
|
||||
SET status = 2,
|
||||
used_time = #{usedTime},
|
||||
remark = #{remark},
|
||||
update_time = NOW()
|
||||
WHERE code = #{code}
|
||||
AND status = 1
|
||||
AND deleted = 0
|
||||
</update>
|
||||
|
||||
<select id="selectByBatchId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM price_voucher_code
|
||||
WHERE batch_id = #{batchId}
|
||||
AND deleted = 0
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectUserVouchers" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM price_voucher_code
|
||||
WHERE face_id = #{faceId}
|
||||
<if test="scenicId != null">
|
||||
AND scenic_id = #{scenicId}
|
||||
</if>
|
||||
AND deleted = 0
|
||||
ORDER BY claimed_time DESC
|
||||
</select>
|
||||
|
||||
<select id="findFirstAvailableByBatchId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM price_voucher_code
|
||||
WHERE batch_id = #{batchId}
|
||||
AND status = 0
|
||||
AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="findRandomUnprintedVoucher" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM price_voucher_code pvc
|
||||
WHERE pvc.scenic_id = #{scenicId}
|
||||
AND pvc.status = 0
|
||||
AND pvc.deleted = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM voucher_print_record vpr
|
||||
WHERE vpr.voucher_code_id = pvc.id
|
||||
AND vpr.deleted = 0
|
||||
)
|
||||
ORDER BY RAND()
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
51
src/main/resources/mapper/VoucherPrintRecordMapper.xml
Normal file
51
src/main/resources/mapper/VoucherPrintRecordMapper.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ycwl.basic.pricing.mapper.VoucherPrintRecordMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.ycwl.basic.pricing.entity.VoucherPrintRecord">
|
||||
<id column="id" property="id" jdbcType="BIGINT"/>
|
||||
<result column="code" property="code" jdbcType="VARCHAR"/>
|
||||
<result column="face_id" property="faceId" jdbcType="BIGINT"/>
|
||||
<result column="broker_id" property="brokerId" jdbcType="BIGINT"/>
|
||||
<result column="scenic_id" property="scenicId" jdbcType="BIGINT"/>
|
||||
<result column="voucher_code_id" property="voucherCodeId" jdbcType="BIGINT"/>
|
||||
<result column="voucher_code" property="voucherCode" jdbcType="VARCHAR"/>
|
||||
<result column="print_status" property="printStatus" jdbcType="TINYINT"/>
|
||||
<result column="error_message" property="errorMessage" jdbcType="VARCHAR"/>
|
||||
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="deleted" property="deleted" jdbcType="TINYINT"/>
|
||||
<result column="deleted_at" property="deletedAt" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, code, face_id, broker_id, scenic_id, voucher_code_id, voucher_code,
|
||||
print_status, error_message, create_time, update_time, deleted, deleted_at
|
||||
</sql>
|
||||
|
||||
<select id="selectByFaceBrokerScenic" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM voucher_print_record
|
||||
WHERE face_id = #{faceId}
|
||||
AND scenic_id = #{scenicId}
|
||||
AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectByVoucherCodeId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM voucher_print_record
|
||||
WHERE voucher_code_id = #{voucherCodeId}
|
||||
AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<update id="updatePrintStatus">
|
||||
UPDATE voucher_print_record
|
||||
SET print_status = #{printStatus},
|
||||
error_message = #{errorMessage},
|
||||
update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
</mapper>
|
Reference in New Issue
Block a user