From 50c84ac1c99cd08b68bc5395a3d88b86c4d924c7 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 5 Sep 2025 11:09:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(pricing):=20=E6=B7=BB=E5=8A=A0=E4=B8=80?= =?UTF-8?q?=E5=8F=A3=E4=BB=B7=E8=B4=AD=E4=B9=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 OnePricePurchaseController 控制器 - 新增 OnePriceConfigFilterRequest、OnePriceConfigRequest、OnePriceInfo等 DTO 类 - 新增 PriceOnePriceConfig 实体类和对应的 Mapper 接口 - 实现 OnePricePurchaseDiscountProvider 优惠提供者 - 实现 OnePricePurchaseServiceImpl 服务实现类 -定义 IOnePricePurchaseService服务接口 - 优化 DiscountDetail 类,添加创建一口价折扣的方法 - 修改 CLAUDE.md,将 error 方法改为 fail 方法 --- .../java/com/ycwl/basic/pricing/CLAUDE.md | 2 +- .../OnePricePurchaseController.java | 192 +++++++++++++++ .../basic/pricing/dto/DiscountDetail.java | 14 +- .../dto/OnePriceConfigFilterRequest.java | 37 +++ .../pricing/dto/OnePriceConfigRequest.java | 65 ++++++ .../ycwl/basic/pricing/dto/OnePriceInfo.java | 59 +++++ .../pricing/entity/PriceOnePriceConfig.java | 122 ++++++++++ .../mapper/PriceOnePriceConfigMapper.java | 93 ++++++++ .../service/IOnePricePurchaseService.java | 106 +++++++++ .../OnePricePurchaseDiscountProvider.java | 197 ++++++++++++++++ .../impl/OnePricePurchaseServiceImpl.java | 219 ++++++++++++++++++ 11 files changed, 1104 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/pricing/controller/OnePricePurchaseController.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/OnePriceConfigFilterRequest.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/OnePriceConfigRequest.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/OnePriceInfo.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceOnePriceConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceOnePriceConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IOnePricePurchaseService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseDiscountProvider.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseServiceImpl.java diff --git a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md index 03c701a..088b6f4 100644 --- a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md +++ b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md @@ -239,7 +239,7 @@ public class PriceCalculationException extends RuntimeException { // 在PricingExceptionHandler中统一处理 @ExceptionHandler(PriceCalculationException.class) public ApiResponse handlePriceCalculationException(PriceCalculationException e) { - return ApiResponse.error(ErrorCode.PRICE_CALCULATION_ERROR, e.getMessage()); + return ApiResponse.fail(ErrorCode.PRICE_CALCULATION_ERROR, e.getMessage()); } ``` diff --git a/src/main/java/com/ycwl/basic/pricing/controller/OnePricePurchaseController.java b/src/main/java/com/ycwl/basic/pricing/controller/OnePricePurchaseController.java new file mode 100644 index 0000000..8cc3505 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/OnePricePurchaseController.java @@ -0,0 +1,192 @@ +package com.ycwl.basic.pricing.controller; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.dto.OnePriceConfigFilterRequest; +import com.ycwl.basic.pricing.dto.OnePriceConfigRequest; +import com.ycwl.basic.pricing.entity.PriceOnePriceConfig; +import com.ycwl.basic.pricing.service.IOnePricePurchaseService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 一口价购买管理控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing/admin/one-price") +@RequiredArgsConstructor +public class OnePricePurchaseController { + + private final IOnePricePurchaseService onePricePurchaseService; + + /** + * 分页查询一口价配置 + */ + @GetMapping("/") + public ApiResponse> pageConfigs(OnePriceConfigFilterRequest request) { + log.info("分页查询一口价配置: {}", request); + + PageInfo pageInfo = onePricePurchaseService.pageConfigs(request); + + log.info("查询到一口价配置数量: {}", pageInfo.getList().size()); + + return ApiResponse.success(pageInfo); + } + + /** + * 查询所有一口价配置 + */ + @GetMapping("/all") + public ApiResponse> getAllConfigs() { + log.info("查询所有一口价配置"); + + List configs = onePricePurchaseService.getAllConfigsForAdmin(); + + log.info("查询到一口价配置数量: {}", configs.size()); + + return ApiResponse.success(configs); + } + + /** + * 根据ID查询一口价配置 + */ + @GetMapping("/{id}") + public ApiResponse getConfigById(@PathVariable Long id) { + log.info("根据ID查询一口价配置: id={}", id); + + PriceOnePriceConfig config = onePricePurchaseService.getConfigById(id); + + if (config == null) { + return ApiResponse.fail("配置不存在"); + } + + return ApiResponse.success(config); + } + + /** + * 创建一口价配置 + */ + @PostMapping("/") + public ApiResponse createConfig(@RequestBody OnePriceConfigRequest request) { + log.info("创建一口价配置: {}", request); + + // 转换为实体对象 + PriceOnePriceConfig config = new PriceOnePriceConfig(); + config.setConfigName(request.getConfigName()); + config.setScenicId(request.getScenicId()); + config.setOnePrice(request.getOnePrice()); + config.setOriginalPrice(request.getOriginalPrice()); + config.setDescription(request.getDescription()); + config.setIsActive(request.getIsActive()); + config.setStartTime(request.getStartTime()); + config.setEndTime(request.getEndTime()); + config.setCanUseCoupon(request.getCanUseCoupon()); + config.setCanUseVoucher(request.getCanUseVoucher()); + + Long configId = onePricePurchaseService.createConfig(config); + + if (configId != null) { + log.info("一口价配置创建成功: configId={}", configId); + return ApiResponse.success(configId); + } else { + return ApiResponse.fail("创建一口价配置失败"); + } + } + + /** + * 更新一口价配置 + */ + @PutMapping("/{id}") + public ApiResponse updateConfig(@PathVariable Long id, @RequestBody OnePriceConfigRequest request) { + log.info("更新一口价配置: id={}, request={}", id, request); + + // 转换为实体对象 + PriceOnePriceConfig config = new PriceOnePriceConfig(); + config.setId(id); + config.setConfigName(request.getConfigName()); + config.setScenicId(request.getScenicId()); + config.setOnePrice(request.getOnePrice()); + config.setOriginalPrice(request.getOriginalPrice()); + config.setDescription(request.getDescription()); + config.setIsActive(request.getIsActive()); + config.setStartTime(request.getStartTime()); + config.setEndTime(request.getEndTime()); + config.setCanUseCoupon(request.getCanUseCoupon()); + config.setCanUseVoucher(request.getCanUseVoucher()); + + boolean success = onePricePurchaseService.updateConfig(config); + + if (success) { + log.info("一口价配置更新成功: id={}", id); + return ApiResponse.success(true); + } else { + return ApiResponse.fail("更新一口价配置失败"); + } + } + + /** + * 删除一口价配置 + */ + @DeleteMapping("/{id}") + public ApiResponse deleteConfig(@PathVariable Long id) { + log.info("删除一口价配置: id={}", id); + + boolean success = onePricePurchaseService.deleteConfig(id); + + if (success) { + log.info("一口价配置删除成功: id={}", id); + return ApiResponse.success(true); + } else { + return ApiResponse.fail("删除一口价配置失败"); + } + } + + /** + * 启用/禁用一口价配置 + */ + @PutMapping("/{id}/status") + public ApiResponse updateConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("更新一口价配置状态: id={}, isActive={}", id, isActive); + + boolean success = onePricePurchaseService.updateConfigStatus(id, isActive); + + if (success) { + log.info("一口价配置状态更新成功: id={}, isActive={}", id, isActive); + return ApiResponse.success(true); + } else { + return ApiResponse.fail("更新一口价配置状态失败"); + } + } + + /** + * 根据景区查询启用的一口价配置 + */ + @GetMapping("/scenic/{scenicId}") + public ApiResponse> getConfigsByScenic(@PathVariable Long scenicId) { + log.info("根据景区查询启用的一口价配置: scenicId={}", scenicId); + + List configs = onePricePurchaseService.getActiveConfigsByScenic(scenicId); + + log.info("景区 {} 查询到一口价配置数量: {}", scenicId, configs.size()); + + return ApiResponse.success(configs); + } + + /** + * 检查景区是否适用一口价 + */ + @GetMapping("/check/{scenicId}") + public ApiResponse checkOnePriceApplicable(@PathVariable Long scenicId) { + log.info("检查景区是否适用一口价: scenicId={}", scenicId); + + boolean applicable = onePricePurchaseService.isOnePriceApplicable(scenicId); + + log.info("景区 {} 一口价适用性: {}", scenicId, applicable); + + return ApiResponse.success(applicable); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java index 72621a4..57b0b7b 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java @@ -86,5 +86,17 @@ public class DiscountDetail { detail.setSortOrder(5); // 打包购买排在后面 return detail; } - + + /** + * 创建一口价购买折扣明细 + */ + public static DiscountDetail createOnePriceDiscount(BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("ONE_PRICE_PURCHASE"); + detail.setDiscountName("一口价购买优惠"); + detail.setDiscountAmount(discountAmount); + detail.setDescription("一口价购买,价格更优惠"); + detail.setSortOrder(4); // 一口价排在打包购买之前 + return detail; + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/OnePriceConfigFilterRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/OnePriceConfigFilterRequest.java new file mode 100644 index 0000000..0695b4a --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/OnePriceConfigFilterRequest.java @@ -0,0 +1,37 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +/** + * 一口价配置筛选请求DTO + */ +@Data +public class OnePriceConfigFilterRequest { + + /** + * 页码 + */ + private Integer pageNum = 1; + + /** + * 页大小 + */ + private Integer pageSize = 10; + + /** + * 景区ID(筛选条件) + */ + private Long scenicId; + + /** + * 配置名称(模糊查询) + */ + private String configName; + + // 移除faceId字段,一口价是景区级别的统一价格 + + /** + * 启用状态 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/OnePriceConfigRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/OnePriceConfigRequest.java new file mode 100644 index 0000000..3156bd8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/OnePriceConfigRequest.java @@ -0,0 +1,65 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 一口价配置请求DTO + */ +@Data +public class OnePriceConfigRequest { + + /** + * 配置名称 + */ + private String configName; + + /** + * 景区ID + */ + private Long scenicId; + + // 移除faceId字段,一口价是景区级别的统一价格 + + /** + * 一口价格 + */ + private BigDecimal onePrice; + + /** + * 原价(用于计算和展示优惠金额) + */ + private BigDecimal originalPrice; + + /** + * 描述 + */ + private String description; + + /** + * 是否启用 + */ + private Boolean isActive; + + /** + * 开始时间 + */ + private LocalDateTime startTime; + + /** + * 结束时间 + */ + private LocalDateTime endTime; + + /** + * 是否可与优惠券叠加使用 + */ + private Boolean canUseCoupon; + + /** + * 是否可与券码叠加使用 + */ + private Boolean canUseVoucher; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/OnePriceInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/OnePriceInfo.java new file mode 100644 index 0000000..4489a07 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/OnePriceInfo.java @@ -0,0 +1,59 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 一口价信息DTO(用于优惠检测) + */ +@Data +public class OnePriceInfo { + + /** + * 配置ID + */ + private Long configId; + + /** + * 配置名称 + */ + private String configName; + + /** + * 景区ID + */ + private Long scenicId; + + // 移除faceId字段,一口价是景区级别的统一价格 + + /** + * 一口价格 + */ + private BigDecimal onePrice; + + /** + * 原价(用于计算和展示优惠金额) + */ + private BigDecimal originalPrice; + + /** + * 实际优惠金额(当前总价 - 一口价) + */ + private BigDecimal actualDiscountAmount; + + /** + * 是否可与优惠券叠加使用 + */ + private Boolean canUseCoupon; + + /** + * 是否可与券码叠加使用 + */ + private Boolean canUseVoucher; + + /** + * 描述 + */ + private String description; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceOnePriceConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceOnePriceConfig.java new file mode 100644 index 0000000..2de06ab --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceOnePriceConfig.java @@ -0,0 +1,122 @@ +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.time.LocalDateTime; + +/** + * 一口价购买配置实体 + * 一口价:用户(faceId)在特定景区内所有商品的一次性购买优惠价格 + */ +@Data +@TableName("price_one_price_config") +public class PriceOnePriceConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 配置名称 + */ + private String configName; + + /** + * 景区ID + */ + private Long scenicId; + + // 移除faceId字段,一口价是景区级别的统一价格 + + /** + * 一口价格(该景区内所有商品的统一价格) + */ + private BigDecimal onePrice; + + /** + * 原价(用于计算和展示优惠金额) + */ + private BigDecimal originalPrice; + + /** + * 描述 + */ + private String description; + + /** + * 是否启用 + */ + private Boolean isActive; + + /** + * 开始时间(可选,用于限时一口价) + */ + private LocalDateTime startTime; + + /** + * 结束时间(可选,用于限时一口价) + */ + private LocalDateTime endTime; + + /** + * 是否可与优惠券叠加使用 + */ + private Boolean canUseCoupon; + + /** + * 是否可与券码叠加使用 + */ + private Boolean canUseVoucher; + + @TableField("create_time") + private LocalDateTime createTime; + + @TableField("update_time") + private LocalDateTime updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + @TableField("deleted_at") + private LocalDateTime deletedAt; + + /** + * 检查是否在有效时间范围内 + */ + public boolean isTimeValid() { + LocalDateTime now = LocalDateTime.now(); + + // 如果没有设置时间范围,则始终有效 + if (startTime == null && endTime == null) { + return true; + } + + // 检查开始时间 + if (startTime != null && now.isBefore(startTime)) { + return false; + } + + // 检查结束时间 + if (endTime != null && now.isAfter(endTime)) { + return false; + } + + return true; + } + + // 移除isUserMatch方法,一口价适用于所有用户 + + /** + * 检查景区是否匹配 + */ + public boolean isScenicMatch(Long userScenicId) { + return scenicId != null && scenicId.equals(userScenicId); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceOnePriceConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceOnePriceConfigMapper.java new file mode 100644 index 0000000..0692515 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceOnePriceConfigMapper.java @@ -0,0 +1,93 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceOnePriceConfig; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +/** + * 一口价配置Mapper + */ +@Mapper +public interface PriceOnePriceConfigMapper extends BaseMapper { + + /** + * 查询启用的一口价配置 + */ + @Select("SELECT * FROM price_one_price_config WHERE is_active = 1 AND deleted = 0 " + + "ORDER BY scenic_id") + List selectActiveConfigs(); + + /** + * 根据景区ID查询启用的配置 + */ + @Select("SELECT * FROM price_one_price_config " + + "WHERE is_active = 1 AND deleted = 0 " + + "AND scenic_id = #{scenicId} " + + "LIMIT 1") + PriceOnePriceConfig selectConfigByScenic(@Param("scenicId") Long scenicId); + + /** + * 查询特定景区的所有配置 + */ + @Select("SELECT * FROM price_one_price_config " + + "WHERE is_active = 1 AND deleted = 0 " + + "AND scenic_id = #{scenicId}") + List selectConfigsByScenic(@Param("scenicId") Long scenicId); + + /** + * 根据ID查询启用的配置 + */ + @Select("SELECT * FROM price_one_price_config " + + "WHERE id = #{id} AND is_active = 1 AND deleted = 0") + PriceOnePriceConfig selectActiveConfigById(Long id); + + /** + * 查询所有配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_one_price_config WHERE deleted = 0 " + + "ORDER BY is_active DESC, create_time DESC") + List selectAllConfigsForAdmin(); + + /** + * 插入配置 + */ + @Insert("INSERT INTO price_one_price_config " + + "(config_name, scenic_id, one_price, original_price, description, is_active, " + + "start_time, end_time, can_use_coupon, can_use_voucher, " + + "create_time, update_time, create_by, deleted) " + + "VALUES " + + "(#{configName}, #{scenicId}, #{onePrice}, #{originalPrice}, #{description}, #{isActive}, " + + "#{startTime}, #{endTime}, #{canUseCoupon}, #{canUseVoucher}, " + + "NOW(), NOW(), #{createBy}, 0)") + @Options(useGeneratedKeys = true, keyProperty = "id") + int insertConfig(PriceOnePriceConfig config); + + /** + * 更新配置 + */ + @Update("UPDATE price_one_price_config SET " + + "config_name = #{configName}, scenic_id = #{scenicId}, " + + "one_price = #{onePrice}, original_price = #{originalPrice}, " + + "description = #{description}, is_active = #{isActive}, " + + "start_time = #{startTime}, end_time = #{endTime}, " + + "can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, " + + "update_time = NOW(), update_by = #{updateBy} " + + "WHERE id = #{id} AND deleted = 0") + int updateConfig(PriceOnePriceConfig config); + + /** + * 更新配置状态 + */ + @Update("UPDATE price_one_price_config SET is_active = #{isActive}, update_time = NOW() " + + "WHERE id = #{id} AND deleted = 0") + int updateConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); + + /** + * 逻辑删除配置 + */ + @Update("UPDATE price_one_price_config SET deleted = 1, deleted_at = NOW() " + + "WHERE id = #{id}") + int deleteConfig(Long id); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IOnePricePurchaseService.java b/src/main/java/com/ycwl/basic/pricing/service/IOnePricePurchaseService.java new file mode 100644 index 0000000..dc2c99f --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IOnePricePurchaseService.java @@ -0,0 +1,106 @@ +package com.ycwl.basic.pricing.service; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.dto.OnePriceConfigFilterRequest; +import com.ycwl.basic.pricing.dto.OnePriceInfo; +import com.ycwl.basic.pricing.entity.PriceOnePriceConfig; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 一口价购买服务接口 + * 一口价:特定景区内所有商品的统一优惠价格 + */ +public interface IOnePricePurchaseService { + + /** + * 检查指定景区是否适用一口价 + * + * @param scenicId 景区ID + * @return 是否适用 + */ + boolean isOnePriceApplicable(Long scenicId); + + /** + * 获取指定景区的一口价信息 + * + * @param scenicId 景区ID + * @param currentAmount 当前总金额(用于计算优惠) + * @return 一口价信息,如果不适用则返回null + */ + OnePriceInfo getOnePriceInfo(Long scenicId, BigDecimal currentAmount); + + /** + * 获取所有启用的一口价配置 + * + * @return 一口价配置列表 + */ + List getActiveConfigs(); + + /** + * 根据景区查询启用的一口价配置 + * + * @param scenicId 景区ID + * @return 一口价配置列表 + */ + List getActiveConfigsByScenic(Long scenicId); + + // ==================== 管理端接口 ==================== + + /** + * 创建一口价配置 + * + * @param config 配置信息 + * @return 配置ID + */ + Long createConfig(PriceOnePriceConfig config); + + /** + * 更新一口价配置 + * + * @param config 配置信息 + * @return 是否更新成功 + */ + boolean updateConfig(PriceOnePriceConfig config); + + /** + * 删除一口价配置 + * + * @param id 配置ID + * @return 是否删除成功 + */ + boolean deleteConfig(Long id); + + /** + * 启用/禁用一口价配置 + * + * @param id 配置ID + * @param isActive 是否启用 + * @return 是否操作成功 + */ + boolean updateConfigStatus(Long id, Boolean isActive); + + /** + * 根据ID查询配置 + * + * @param id 配置ID + * @return 配置信息 + */ + PriceOnePriceConfig getConfigById(Long id); + + /** + * 分页查询一口价配置 + * + * @param request 筛选请求参数 + * @return 分页结果 + */ + PageInfo pageConfigs(OnePriceConfigFilterRequest request); + + /** + * 查询所有配置(包含禁用的)- 管理端使用 + * + * @return 配置列表 + */ + List getAllConfigsForAdmin(); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseDiscountProvider.java new file mode 100644 index 0000000..c53efa0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseDiscountProvider.java @@ -0,0 +1,197 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.DiscountInfo; +import com.ycwl.basic.pricing.dto.DiscountResult; +import com.ycwl.basic.pricing.dto.OnePriceInfo; +import com.ycwl.basic.pricing.service.IDiscountProvider; +import com.ycwl.basic.pricing.service.IOnePricePurchaseService; +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 OnePricePurchaseDiscountProvider implements IDiscountProvider { + + private final IOnePricePurchaseService onePricePurchaseService; + + @Override + public String getProviderType() { + return "ONE_PRICE_PURCHASE"; + } + + @Override + public int getPriority() { + return 60; // 中等优先级,在券码和优惠券之间 + } + + @Override + public List detectAvailableDiscounts(DiscountDetectionContext context) { + List discounts = new ArrayList<>(); + + try { + if (context.getScenicId() == null || context.getCurrentAmount() == null) { + log.debug("一口价检测失败: 景区ID或当前金额为空"); + return discounts; + } + + // 检查是否适用一口价 + if (!onePricePurchaseService.isOnePriceApplicable(context.getScenicId())) { + log.debug("景区 {} 不适用一口价", context.getScenicId()); + return discounts; + } + + // 获取一口价信息 + OnePriceInfo onePriceInfo = onePricePurchaseService.getOnePriceInfo( + context.getScenicId(), context.getCurrentAmount()); + + if (onePriceInfo == null) { + log.debug("景区 {} 未找到一口价配置", context.getScenicId()); + return discounts; + } + + // 只有当一口价小于当前金额时才算优惠 + if (onePriceInfo.getOnePrice().compareTo(context.getCurrentAmount()) >= 0) { + log.debug("景区 {} 一口价 {} 不小于当前金额 {}", + context.getScenicId(), onePriceInfo.getOnePrice(), context.getCurrentAmount()); + return discounts; + } + + // 创建优惠信息 + DiscountInfo discountInfo = new DiscountInfo(); + discountInfo.setProviderType(getProviderType()); + discountInfo.setDiscountName("一口价购买优惠"); + discountInfo.setDiscountAmount(onePriceInfo.getActualDiscountAmount()); + discountInfo.setDiscountDescription("景区一口价购买,价格更优惠"); + discountInfo.setOnePriceInfo(onePriceInfo); + + discounts.add(discountInfo); + + log.info("检测到一口价优惠: 景区={}, 一口价={}, 优惠金额={}", + context.getScenicId(), onePriceInfo.getOnePrice(), + onePriceInfo.getActualDiscountAmount()); + + } catch (Exception e) { + log.error("一口价优惠检测失败", e); + } + + return discounts; + } + + @Override + public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) { + DiscountResult result = new DiscountResult(); + result.setDiscountInfo(discountInfo); + result.setSuccess(false); + + try { + if (!getProviderType().equals(discountInfo.getProviderType())) { + result.setFailureReason("优惠类型不匹配"); + return result; + } + + OnePriceInfo onePriceInfo = discountInfo.getOnePriceInfo(); + if (onePriceInfo == null) { + result.setFailureReason("一口价信息为空"); + return result; + } + + // 检查优惠的叠加限制 + boolean canUseWithOtherDiscounts = checkDiscountCombinationRules(onePriceInfo, context); + if (!canUseWithOtherDiscounts) { + result.setFailureReason("一口价不可与其他优惠叠加使用"); + return result; + } + + // 应用一口价优惠 + BigDecimal discountAmount = onePriceInfo.getActualDiscountAmount(); + BigDecimal finalAmount = context.getCurrentAmount().subtract(discountAmount); + + // 如果使用一口价,则最终金额就是一口价金额 + finalAmount = onePriceInfo.getOnePrice(); + discountAmount = context.getCurrentAmount().subtract(finalAmount); + + result.setSuccess(true); + result.setActualDiscountAmount(discountAmount); + result.setFinalAmount(finalAmount); + result.setFailureReason("一口价购买优惠已应用"); + + log.info("一口价优惠应用成功: 优惠金额={}, 最终金额={}", discountAmount, finalAmount); + + } catch (Exception e) { + log.error("一口价优惠应用失败", e); + result.setFailureReason("一口价优惠应用失败: " + e.getMessage()); + } + + return result; + } + + @Override + public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) { + try { + if (!getProviderType().equals(discountInfo.getProviderType())) { + return false; + } + + OnePriceInfo onePriceInfo = discountInfo.getOnePriceInfo(); + if (onePriceInfo == null) { + return false; + } + + // 检查一口价是否仍然有效(时间范围等) + return onePricePurchaseService.isOnePriceApplicable(context.getScenicId()); + + } catch (Exception e) { + log.error("检查一口价可用性失败", e); + return false; + } + } + + @Override + public BigDecimal getMaxPossibleDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) { + try { + OnePriceInfo onePriceInfo = discountInfo.getOnePriceInfo(); + if (onePriceInfo != null && onePriceInfo.getActualDiscountAmount() != null) { + return onePriceInfo.getActualDiscountAmount(); + } + } catch (Exception e) { + log.error("获取一口价最大优惠金额失败", e); + } + + return BigDecimal.ZERO; + } + + /** + * 检查优惠叠加规则 + */ + private boolean checkDiscountCombinationRules(OnePriceInfo onePriceInfo, DiscountDetectionContext context) { + // 根据配置检查是否可以与优惠券、券码叠加使用 + // 这里可以根据业务需求实现具体的叠加规则 + + // 如果配置了不可与优惠券叠加,且请求中要使用优惠券,则返回false + if (Boolean.FALSE.equals(onePriceInfo.getCanUseCoupon()) && + Boolean.TRUE.equals(context.getAutoUseCoupon())) { + log.debug("一口价配置不允许与优惠券叠加使用"); + return false; + } + + // 如果配置了不可与券码叠加,且请求中要使用券码,则返回false + if (Boolean.FALSE.equals(onePriceInfo.getCanUseVoucher()) && + (Boolean.TRUE.equals(context.getAutoUseVoucher()) || + context.getVoucherCode() != null)) { + log.debug("一口价配置不允许与券码叠加使用"); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseServiceImpl.java new file mode 100644 index 0000000..bc75046 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseServiceImpl.java @@ -0,0 +1,219 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.dto.OnePriceConfigFilterRequest; +import com.ycwl.basic.pricing.dto.OnePriceInfo; +import com.ycwl.basic.pricing.entity.PriceOnePriceConfig; +import com.ycwl.basic.pricing.mapper.PriceOnePriceConfigMapper; +import com.ycwl.basic.pricing.service.IOnePricePurchaseService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 一口价购买服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class OnePricePurchaseServiceImpl implements IOnePricePurchaseService { + + private final PriceOnePriceConfigMapper onePriceConfigMapper; + + @Override + public boolean isOnePriceApplicable(Long scenicId) { + PriceOnePriceConfig config = findConfigByScenic(scenicId); + return config != null; + } + + @Override + public OnePriceInfo getOnePriceInfo(Long scenicId, BigDecimal currentAmount) { + PriceOnePriceConfig config = findConfigByScenic(scenicId); + + if (config == null) { + return null; + } + + OnePriceInfo info = new OnePriceInfo(); + info.setConfigId(config.getId()); + info.setConfigName(config.getConfigName()); + info.setScenicId(scenicId); + info.setOnePrice(config.getOnePrice()); + info.setOriginalPrice(config.getOriginalPrice()); + info.setDescription(config.getDescription()); + info.setCanUseCoupon(config.getCanUseCoupon()); + info.setCanUseVoucher(config.getCanUseVoucher()); + + // 计算实际优惠金额 + if (currentAmount != null && config.getOnePrice().compareTo(currentAmount) < 0) { + BigDecimal discountAmount = currentAmount.subtract(config.getOnePrice()); + info.setActualDiscountAmount(discountAmount); + } else { + info.setActualDiscountAmount(BigDecimal.ZERO); + } + + return info; + } + + @Override + public List getActiveConfigs() { + return onePriceConfigMapper.selectActiveConfigs(); + } + + @Override + public List getActiveConfigsByScenic(Long scenicId) { + return onePriceConfigMapper.selectConfigsByScenic(scenicId); + } + + // ==================== 管理端接口实现 ==================== + + @Override + public Long createConfig(PriceOnePriceConfig config) { + try { + // 设置默认值 + if (config.getIsActive() == null) { + config.setIsActive(true); + } + if (config.getCanUseCoupon() == null) { + config.setCanUseCoupon(true); + } + if (config.getCanUseVoucher() == null) { + config.setCanUseVoucher(true); + } + if (config.getDeleted() == null) { + config.setDeleted(0); + } + + int result = onePriceConfigMapper.insertConfig(config); + if (result > 0) { + log.info("创建一口价配置成功: id={}, name={}", config.getId(), config.getConfigName()); + return config.getId(); + } + return null; + } catch (Exception e) { + log.error("创建一口价配置失败", e); + throw new RuntimeException("创建一口价配置失败: " + e.getMessage()); + } + } + + @Override + public boolean updateConfig(PriceOnePriceConfig config) { + try { + int result = onePriceConfigMapper.updateConfig(config); + if (result > 0) { + log.info("更新一口价配置成功: id={}", config.getId()); + return true; + } + return false; + } catch (Exception e) { + log.error("更新一口价配置失败: id={}", config.getId(), e); + throw new RuntimeException("更新一口价配置失败: " + e.getMessage()); + } + } + + @Override + public boolean deleteConfig(Long id) { + try { + int result = onePriceConfigMapper.deleteConfig(id); + if (result > 0) { + log.info("删除一口价配置成功: id={}", id); + return true; + } + return false; + } catch (Exception e) { + log.error("删除一口价配置失败: id={}", id, e); + throw new RuntimeException("删除一口价配置失败: " + e.getMessage()); + } + } + + @Override + public boolean updateConfigStatus(Long id, Boolean isActive) { + try { + int result = onePriceConfigMapper.updateConfigStatus(id, isActive); + if (result > 0) { + log.info("更新一口价配置状态成功: id={}, isActive={}", id, isActive); + return true; + } + return false; + } catch (Exception e) { + log.error("更新一口价配置状态失败: id={}", id, e); + throw new RuntimeException("更新一口价配置状态失败: " + e.getMessage()); + } + } + + @Override + public PriceOnePriceConfig getConfigById(Long id) { + return onePriceConfigMapper.selectById(id); + } + + @Override + public PageInfo pageConfigs(OnePriceConfigFilterRequest request) { + // 开启分页 + PageHelper.startPage(request.getPageNum(), request.getPageSize()); + + // 构建查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("deleted", 0); // 未删除的记录 + + // 添加筛选条件 + if (request.getScenicId() != null) { + queryWrapper.eq("scenic_id", request.getScenicId()); + } + if (StringUtils.hasText(request.getConfigName())) { + queryWrapper.like("config_name", request.getConfigName()); // 模糊查询 + } + if (request.getIsActive() != null) { + queryWrapper.eq("is_active", request.getIsActive()); + } + + // 按创建时间降序排列 + queryWrapper.orderByDesc("create_time"); + + // 执行查询 + List list = onePriceConfigMapper.selectList(queryWrapper); + + // 返回分页结果 + return new PageInfo<>(list); + } + + @Override + public List getAllConfigsForAdmin() { + return onePriceConfigMapper.selectAllConfigsForAdmin(); + } + + // ==================== 私有方法 ==================== + + /** + * 根据景区查找配置 + */ + private PriceOnePriceConfig findConfigByScenic(Long scenicId) { + if (scenicId == null) { + return null; + } + + // 查询匹配的配置 + PriceOnePriceConfig config = onePriceConfigMapper.selectConfigByScenic(scenicId); + + if (config == null) { + return null; + } + + // 检查时间有效性 + if (!config.isTimeValid()) { + return null; + } + + // 检查景区匹配性 + if (!config.isScenicMatch(scenicId)) { + return null; + } + + return config; + } +} \ No newline at end of file