feat(pricing): 添加券码管理和使用功能

- 新增券码批次配置和券码实体
- 实现券码创建、领取、使用等接口
- 添加券码状态和优惠类型枚举
- 优化价格计算逻辑,支持券码优惠
- 新增优惠检测和应用相关功能
This commit is contained in:
2025-08-21 09:35:08 +08:00
parent e9035af542
commit eb327723cd
52 changed files with 2572 additions and 455 deletions

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,62 @@
package com.ycwl.basic.pricing.service;
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
import com.ycwl.basic.pricing.dto.VoucherInfo;
import java.math.BigDecimal;
import java.util.List;
/**
* 券码服务接口
*/
public interface IVoucherService {
/**
* 验证并获取券码信息
* @param voucherCode 券码
* @param faceId 用户faceId
* @param scenicId 景区ID
* @return 券码信息(如果有效)
*/
VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId);
/**
* 获取用户在指定景区的可用券码列表
* @param faceId 用户faceId
* @param scenicId 景区ID
* @return 可用券码列表
*/
List<VoucherInfo> getAvailableVouchers(Long faceId, Long scenicId);
/**
* 标记券码为已使用
* @param voucherCode 券码
* @param remark 使用备注
*/
void markVoucherAsUsed(String voucherCode, String remark);
/**
* 检查用户是否可以在指定景区领取券码
* @param faceId 用户faceId
* @param scenicId 景区ID
* @return 是否可以领取
*/
boolean canClaimVoucher(Long faceId, Long scenicId);
/**
* 计算券码优惠金额
* @param voucherInfo 券码信息
* @param context 检测上下文
* @return 优惠金额
*/
BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context);
/**
* 获取最优的券码(如果用户有多个可用券码)
* @param faceId 用户faceId
* @param scenicId 景区ID
* @param context 检测上下文
* @return 最优券码信息
*/
VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -13,6 +13,8 @@ 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;
@@ -144,13 +146,13 @@ public class CouponServiceImpl implements ICouponService {
throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限");
}
LocalDateTime useTime = LocalDateTime.now();
Date useTime = new Date();
// 设置使用时间、订单信息和景区信息
record.setStatus(CouponStatus.USED);
record.setUseTime(useTime);
record.setOrderId(request.getOrderId());
record.setUpdatedTime(LocalDateTime.now());
record.setUpdatedTime(new Date());
// 如果请求中包含景区ID,记录到使用记录中
if (request.getScenicId() != null && !request.getScenicId().isEmpty()) {

View File

@@ -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;
}
};
}
}

View File

@@ -5,10 +5,7 @@ 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.ICouponService;
import com.ycwl.basic.pricing.service.IPriceBundleService;
import com.ycwl.basic.pricing.service.IPriceCalculationService;
import com.ycwl.basic.pricing.service.IProductConfigService;
import com.ycwl.basic.pricing.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -30,6 +27,8 @@ 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) {
@@ -59,35 +58,60 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount);
}
// 构建价格计算结果
PriceCalculationResult result = new PriceCalculationResult();
result.setOriginalAmount(originalTotalAmount); // 原总价
result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计
result.setProductDetails(request.getProducts());
// 处理优惠券
BigDecimal couponDiscountAmount = BigDecimal.ZERO;
if (Boolean.TRUE.equals(request.getAutoUseCoupon()) && request.getUserId() != null) {
CouponInfo bestCoupon = couponService.selectBestCoupon(
request.getUserId(), request.getProducts(), totalAmount);
if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
result.setUsedCoupon(bestCoupon);
couponDiscountAmount = bestCoupon.getActualDiscountAmount();
discountDetails.add(DiscountDetail.createCouponDiscount(bestCoupon.getCouponName(), couponDiscountAmount));
}
}
// 使用新的优惠检测系统处理所有优惠(券码 + 优惠券
DiscountCombinationResult discountResult = calculateDiscounts(request, totalAmount);
// 计算总优惠金额
BigDecimal totalDiscountAmount = discountDetails.stream()
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));
}
// 按排序排列折扣明细
discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
result.setDiscountAmount(totalDiscountAmount);
result.setDiscountDetails(discountDetails);
result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount));
// 标记使用的优惠(仅在非预览模式下)
if (!Boolean.TRUE.equals(request.getPreviewOnly())) {
markDiscountsAsUsed(result, request);
}
return result;
}
@@ -269,4 +293,96 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
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);
// 不抛出异常,避免影响主流程
}
}
}

View File

@@ -8,7 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 价格管理服务实现(用于配置管理,手动处理时间字段)
@@ -35,8 +35,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
}
}
config.setCreatedTime(LocalDateTime.now());
config.setUpdatedTime(LocalDateTime.now());
config.setCreatedTime(new Date());
config.setUpdatedTime(new Date());
productConfigMapper.insertProductConfig(config);
return config.getId();
}
@@ -44,7 +44,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
@Override
@Transactional
public boolean updateProductConfig(PriceProductConfig config) {
config.setUpdatedTime(LocalDateTime.now());
config.setUpdatedTime(new Date());
return productConfigMapper.updateProductConfig(config) > 0;
}
@@ -58,8 +58,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
config.getProductType(), config.getMinQuantity(), config.getMaxQuantity());
}
config.setCreatedTime(LocalDateTime.now());
config.setUpdatedTime(LocalDateTime.now());
config.setCreatedTime(new Date());
config.setUpdatedTime(new Date());
tierConfigMapper.insertTierConfig(config);
return config.getId();
}
@@ -67,15 +67,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
@Override
@Transactional
public boolean updateTierConfig(PriceTierConfig config) {
config.setUpdatedTime(LocalDateTime.now());
config.setUpdatedTime(new Date());
return tierConfigMapper.updateTierConfig(config) > 0;
}
@Override
@Transactional
public Long createCouponConfig(PriceCouponConfig config) {
config.setCreatedTime(LocalDateTime.now());
config.setUpdatedTime(LocalDateTime.now());
config.setCreatedTime(new Date());
config.setUpdatedTime(new Date());
couponConfigMapper.insertCoupon(config);
return config.getId();
}
@@ -83,16 +83,16 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
@Override
@Transactional
public boolean updateCouponConfig(PriceCouponConfig config) {
config.setUpdatedTime(LocalDateTime.now());
config.setUpdatedTime(new Date());
return couponConfigMapper.updateCoupon(config) > 0;
}
@Override
@Transactional
public Long createCouponClaimRecord(PriceCouponClaimRecord record) {
record.setClaimTime(LocalDateTime.now());
record.setCreatedTime(LocalDateTime.now());
record.setUpdatedTime(LocalDateTime.now());
record.setClaimTime(new Date());
record.setCreatedTime(new Date());
record.setUpdatedTime(new Date());
couponClaimRecordMapper.insertClaimRecord(record);
return record.getId();
}
@@ -100,15 +100,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
@Override
@Transactional
public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) {
record.setUpdatedTime(LocalDateTime.now());
record.setUpdatedTime(new Date());
return couponClaimRecordMapper.updateClaimRecord(record) > 0;
}
@Override
@Transactional
public Long createBundleConfig(PriceBundleConfig config) {
config.setCreatedTime(LocalDateTime.now());
config.setUpdatedTime(LocalDateTime.now());
config.setCreatedTime(new Date());
config.setUpdatedTime(new Date());
bundleConfigMapper.insertBundleConfig(config);
return config.getId();
}
@@ -116,7 +116,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
@Override
@Transactional
public boolean updateBundleConfig(PriceBundleConfig config) {
config.setUpdatedTime(LocalDateTime.now());
config.setUpdatedTime(new Date());
return bundleConfigMapper.updateBundleConfig(config) > 0;
}

View File

@@ -0,0 +1,193 @@
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.Date;
@Service
@RequiredArgsConstructor
public class VoucherBatchServiceImpl implements VoucherBatchService {
private final PriceVoucherBatchConfigMapper voucherBatchMapper;
private final 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.setCreatedTime(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::getCreatedTime);
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::getCreatedTime);
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;
}
}

View File

@@ -0,0 +1,194 @@
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 org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class VoucherCodeServiceImpl implements VoucherCodeService {
private final PriceVoucherCodeMapper voucherCodeMapper;
private final PriceVoucherBatchConfigMapper voucherBatchMapper;
private final 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.setCreatedTime(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 (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
throw new BizException(400, "该用户在此景区已领取过券码");
}
PriceVoucherBatchConfig batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId());
if (batch == null) {
throw new BizException(400, "暂无可用券码批次");
}
PriceVoucherCode availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId());
if (availableCode == null) {
throw new BizException(400, "券码已领完");
}
availableCode.setFaceId(req.getFaceId());
availableCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
availableCode.setClaimedTime(new Date());
voucherCodeMapper.updateById(availableCode);
voucherBatchService.updateBatchClaimedCount(batch.getId());
return convertToResp(availableCode, 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::getCreatedTime);
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 (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;
}
private String generateVoucherCode() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
}
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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,223 @@
package com.ycwl.basic.pricing.service.impl;
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.dto.VoucherInfo;
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
import com.ycwl.basic.pricing.entity.PriceVoucherCode;
import com.ycwl.basic.pricing.enums.VoucherCodeStatus;
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper;
import com.ycwl.basic.pricing.service.IVoucherService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 券码服务实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VoucherServiceImpl implements IVoucherService {
private final PriceVoucherCodeMapper voucherCodeMapper;
private final PriceVoucherBatchConfigMapper voucherBatchConfigMapper;
@Override
public VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId) {
if (!StringUtils.hasText(voucherCode)) {
return null;
}
// 查询券码信息
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) {
return null;
}
// 查询批次信息
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId());
if (batchConfig == null || batchConfig.getDeleted() == 1 || batchConfig.getStatus() == 0) {
return null;
}
// 验证景区匹配
if (scenicId != null && !scenicId.equals(voucherCodeEntity.getScenicId())) {
return null;
}
VoucherInfo voucherInfo = buildVoucherInfo(voucherCodeEntity, batchConfig);
// 检查券码状态和可用性
if (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) {
// 未领取状态,检查是否可以领取
if (faceId != null && canClaimVoucher(faceId, voucherCodeEntity.getScenicId())) {
voucherInfo.setAvailable(true);
} else {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("您已在该景区领取过券码");
}
} else if (VoucherCodeStatus.CLAIMED_UNUSED.getCode().equals(voucherCodeEntity.getStatus())) {
// 已领取未使用,检查是否为当前用户
if (faceId != null && faceId.equals(voucherCodeEntity.getFaceId())) {
voucherInfo.setAvailable(true);
} else {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("券码已被其他用户领取");
}
} else {
// 已使用
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("券码已使用");
}
return voucherInfo;
}
@Override
public List<VoucherInfo> getAvailableVouchers(Long faceId, Long scenicId) {
if (faceId == null || scenicId == null) {
return new ArrayList<>();
}
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByFaceIdAndScenicId(faceId, scenicId);
List<VoucherInfo> voucherInfos = new ArrayList<>();
for (PriceVoucherCode voucherCode : voucherCodes) {
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCode.getBatchId());
if (batchConfig != null && batchConfig.getDeleted() == 0 && batchConfig.getStatus() == 1) {
VoucherInfo voucherInfo = buildVoucherInfo(voucherCode, batchConfig);
voucherInfo.setAvailable(true);
voucherInfos.add(voucherInfo);
}
}
return voucherInfos;
}
@Override
public void markVoucherAsUsed(String voucherCode, String remark) {
if (!StringUtils.hasText(voucherCode)) {
return;
}
int result = voucherCodeMapper.useVoucher(voucherCode, LocalDateTime.now(), remark);
if (result > 0) {
// 更新批次统计
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
if (voucherCodeEntity != null) {
voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1);
}
log.info("券码已标记为使用: {}", voucherCode);
}
}
@Override
public boolean canClaimVoucher(Long faceId, Long scenicId) {
if (faceId == null || scenicId == null) {
return false;
}
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
return count == 0;
}
@Override
public BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context) {
if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable()) ||
context.getProducts() == null || context.getProducts().isEmpty()) {
return BigDecimal.ZERO;
}
VoucherDiscountType discountType = voucherInfo.getDiscountType();
BigDecimal discountValue = voucherInfo.getDiscountValue();
if (discountType == null) {
return BigDecimal.ZERO;
}
return switch (discountType) {
case FREE_ALL -> {
// 全场免费,返回当前总金额
yield context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO;
}
case REDUCE_PRICE -> {
// 商品降价,每个商品减免固定金额
if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0) {
yield BigDecimal.ZERO;
}
BigDecimal totalDiscount = BigDecimal.ZERO;
for (ProductItem product : context.getProducts()) {
BigDecimal productDiscount = discountValue.multiply(BigDecimal.valueOf(product.getQuantity()));
totalDiscount = totalDiscount.add(productDiscount);
}
yield totalDiscount;
}
case DISCOUNT -> {
// 商品打折,按百分比计算
if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0 ||
discountValue.compareTo(BigDecimal.valueOf(100)) >= 0) {
yield BigDecimal.ZERO;
}
BigDecimal currentAmount = context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO;
BigDecimal discountRate = discountValue.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
yield currentAmount.multiply(discountRate).setScale(2, RoundingMode.HALF_UP);
}
};
}
@Override
public VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context) {
List<VoucherInfo> availableVouchers = getAvailableVouchers(faceId, scenicId);
if (availableVouchers.isEmpty()) {
return null;
}
// 计算每个券码的优惠金额,选择最优的
VoucherInfo bestVoucher = null;
BigDecimal maxDiscount = BigDecimal.ZERO;
for (VoucherInfo voucher : availableVouchers) {
BigDecimal discount = calculateVoucherDiscount(voucher, context);
voucher.setActualDiscountAmount(discount);
if (discount.compareTo(maxDiscount) > 0) {
maxDiscount = discount;
bestVoucher = voucher;
}
}
return bestVoucher;
}
/**
* 构建券码信息DTO
*/
private VoucherInfo buildVoucherInfo(PriceVoucherCode voucherCode, PriceVoucherBatchConfig batchConfig) {
VoucherInfo voucherInfo = new VoucherInfo();
voucherInfo.setVoucherId(voucherCode.getId());
voucherInfo.setVoucherCode(voucherCode.getCode());
voucherInfo.setBatchId(batchConfig.getId());
voucherInfo.setBatchName(batchConfig.getBatchName());
voucherInfo.setScenicId(voucherCode.getScenicId());
voucherInfo.setBrokerId(batchConfig.getBrokerId());
voucherInfo.setDiscountType(VoucherDiscountType.getByCode(batchConfig.getDiscountType()));
voucherInfo.setDiscountValue(batchConfig.getDiscountValue());
voucherInfo.setStatus(voucherCode.getStatus());
voucherInfo.setClaimedTime(voucherCode.getClaimedTime());
voucherInfo.setUsedTime(voucherCode.getUsedTime());
return voucherInfo;
}
}