You've already forked FrameTour-BE
feat(pricing): 添加一口价购买功能
- 新增 OnePricePurchaseController 控制器 - 新增 OnePriceConfigFilterRequest、OnePriceConfigRequest、OnePriceInfo等 DTO 类 - 新增 PriceOnePriceConfig 实体类和对应的 Mapper 接口 - 实现 OnePricePurchaseDiscountProvider 优惠提供者 - 实现 OnePricePurchaseServiceImpl 服务实现类 -定义 IOnePricePurchaseService服务接口 - 优化 DiscountDetail 类,添加创建一口价折扣的方法 - 修改 CLAUDE.md,将 error 方法改为 fail 方法
This commit is contained in:
@@ -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<PriceOnePriceConfig> getActiveConfigs();
|
||||
|
||||
/**
|
||||
* 根据景区查询启用的一口价配置
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 一口价配置列表
|
||||
*/
|
||||
List<PriceOnePriceConfig> 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<PriceOnePriceConfig> pageConfigs(OnePriceConfigFilterRequest request);
|
||||
|
||||
/**
|
||||
* 查询所有配置(包含禁用的)- 管理端使用
|
||||
*
|
||||
* @return 配置列表
|
||||
*/
|
||||
List<PriceOnePriceConfig> getAllConfigsForAdmin();
|
||||
}
|
||||
@@ -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<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
|
||||
List<DiscountInfo> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<PriceOnePriceConfig> getActiveConfigs() {
|
||||
return onePriceConfigMapper.selectActiveConfigs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceOnePriceConfig> 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<PriceOnePriceConfig> pageConfigs(OnePriceConfigFilterRequest request) {
|
||||
// 开启分页
|
||||
PageHelper.startPage(request.getPageNum(), request.getPageSize());
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<PriceOnePriceConfig> 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<PriceOnePriceConfig> list = onePriceConfigMapper.selectList(queryWrapper);
|
||||
|
||||
// 返回分页结果
|
||||
return new PageInfo<>(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PriceOnePriceConfig> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user