From 3ce3972875da2508ae7dfbd174efd525cc84bb62 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 27 Nov 2025 09:34:10 +0800 Subject: [PATCH 1/4] =?UTF-8?q?refactor(order):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E8=B4=AD=E4=B9=B0=E6=A3=80=E6=9F=A5=E5=92=8C?= =?UTF-8?q?=E5=AE=9A=E4=BB=B7=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入商品类型能力配置,替代硬编码的商品类型判断 - 实现策略模式处理不同商品类型的重复购买检查 - 抽象定价模式,支持固定价格和数量计价等不同方式 - 新增策略工厂自动注册各类检查器实现 - 添加缓存机制提升商品类型配置查询性能 - 解耦订单服务与具体商品类型的紧耦合关系 - 提高代码可维护性和扩展性,便于新增商品类型 --- .../DuplicatePurchaseCheckerFactory.java | 68 +++++++ .../order/service/impl/OrderServiceImpl.java | 186 ++++++------------ .../order/strategy/DuplicateCheckContext.java | 65 ++++++ .../strategy/IDuplicatePurchaseChecker.java | 43 ++++ .../impl/NoCheckDuplicateChecker.java | 31 +++ .../strategy/impl/SetIdDuplicateChecker.java | 101 ++++++++++ .../impl/VideoIdDuplicateChecker.java | 100 ++++++++++ .../impl/PriceCalculationServiceImpl.java | 31 +-- .../capability/DuplicateCheckStrategy.java | 63 ++++++ .../basic/product/capability/PricingMode.java | 57 ++++++ .../capability/ProductTypeCapability.java | 156 +++++++++++++++ .../mapper/ProductTypeCapabilityMapper.java | 21 ++ .../IProductTypeCapabilityService.java | 70 +++++++ .../ProductTypeCapabilityServiceImpl.java | 162 +++++++++++++++ .../mapper/ProductTypeCapabilityMapper.xml | 47 +++++ 15 files changed, 1062 insertions(+), 139 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/order/factory/DuplicatePurchaseCheckerFactory.java create mode 100644 src/main/java/com/ycwl/basic/order/strategy/DuplicateCheckContext.java create mode 100644 src/main/java/com/ycwl/basic/order/strategy/IDuplicatePurchaseChecker.java create mode 100644 src/main/java/com/ycwl/basic/order/strategy/impl/NoCheckDuplicateChecker.java create mode 100644 src/main/java/com/ycwl/basic/order/strategy/impl/SetIdDuplicateChecker.java create mode 100644 src/main/java/com/ycwl/basic/order/strategy/impl/VideoIdDuplicateChecker.java create mode 100644 src/main/java/com/ycwl/basic/product/capability/DuplicateCheckStrategy.java create mode 100644 src/main/java/com/ycwl/basic/product/capability/PricingMode.java create mode 100644 src/main/java/com/ycwl/basic/product/capability/ProductTypeCapability.java create mode 100644 src/main/java/com/ycwl/basic/product/mapper/ProductTypeCapabilityMapper.java create mode 100644 src/main/java/com/ycwl/basic/product/service/IProductTypeCapabilityService.java create mode 100644 src/main/java/com/ycwl/basic/product/service/impl/ProductTypeCapabilityServiceImpl.java create mode 100644 src/main/resources/mapper/ProductTypeCapabilityMapper.xml diff --git a/src/main/java/com/ycwl/basic/order/factory/DuplicatePurchaseCheckerFactory.java b/src/main/java/com/ycwl/basic/order/factory/DuplicatePurchaseCheckerFactory.java new file mode 100644 index 00000000..fcf77045 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/factory/DuplicatePurchaseCheckerFactory.java @@ -0,0 +1,68 @@ +package com.ycwl.basic.order.factory; + +import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker; +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 重复购买检查策略工厂 + * + * 设计原则: + * 1. 自动注册:Spring 自动注入所有策略实现并注册 + * 2. 类型安全:根据枚举类型查找策略 + * 3. 失败快速:找不到策略时抛出异常 + */ +@Slf4j +@Service +public class DuplicatePurchaseCheckerFactory { + + private final Map checkerMap = new HashMap<>(); + + /** + * 构造函数:自动注册所有策略实现 + */ + @Autowired + public DuplicatePurchaseCheckerFactory(List checkers) { + for (IDuplicatePurchaseChecker checker : checkers) { + DuplicateCheckStrategy strategy = checker.getStrategyType(); + checkerMap.put(strategy, checker); + log.info("注册重复购买检查策略: {} -> {}", strategy, checker.getClass().getSimpleName()); + } + log.info("重复购买检查策略工厂初始化完成,共注册 {} 个策略", checkerMap.size()); + } + + /** + * 根据策略类型获取检查器 + * + * @param strategy 策略类型 + * @return 对应的检查器实现 + * @throws IllegalArgumentException 如果找不到对应的策略实现 + */ + public IDuplicatePurchaseChecker getChecker(DuplicateCheckStrategy strategy) { + IDuplicatePurchaseChecker checker = checkerMap.get(strategy); + if (checker == null) { + throw new IllegalArgumentException("未找到重复购买检查策略: " + strategy); + } + return checker; + } + + /** + * 检查是否支持指定策略 + */ + public boolean supportsStrategy(DuplicateCheckStrategy strategy) { + return checkerMap.containsKey(strategy); + } + + /** + * 获取所有已注册的策略类型 + */ + public Map getAllCheckers() { + return new HashMap<>(checkerMap); + } +} diff --git a/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java b/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java index f2fdb9cb..9e21ade4 100644 --- a/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java +++ b/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java @@ -21,6 +21,13 @@ import com.ycwl.basic.order.mapper.OrderV2Mapper; import com.ycwl.basic.order.mapper.OrderRefundMapper; import com.ycwl.basic.order.service.IOrderService; import com.ycwl.basic.order.event.*; +import com.ycwl.basic.order.strategy.DuplicateCheckContext; +import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker; +import com.ycwl.basic.order.factory.DuplicatePurchaseCheckerFactory; +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import com.ycwl.basic.product.capability.PricingMode; +import com.ycwl.basic.product.capability.ProductTypeCapability; +import com.ycwl.basic.product.service.IProductTypeCapabilityService; import com.ycwl.basic.pricing.dto.DiscountDetail; import com.ycwl.basic.pricing.dto.PriceCalculationResult; import com.ycwl.basic.pricing.dto.ProductItem; @@ -69,6 +76,8 @@ public class OrderServiceImpl implements IOrderService { private final ICouponService couponService; private final IVoucherService voucherService; private final IProductConfigService productConfigService; + private final IProductTypeCapabilityService productTypeCapabilityService; + private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory; @Override @Transactional(rollbackFor = Exception.class) @@ -767,16 +776,10 @@ public class OrderServiceImpl implements IOrderService { /** * 获取商品类型中文名称 + * 重构: 从配置驱动替代硬编码 */ private String getProductTypeName(String productType) { - return switch (productType) { - case "VLOG_VIDEO" -> "Vlog视频"; - case "RECORDING_SET" -> "录像集"; - case "PHOTO_SET" -> "照相集"; - case "PHOTO_PRINT" -> "照片打印"; - case "MACHINE_PRINT" -> "一体机打印"; - default -> "景区商品"; - }; + return productTypeCapabilityService.getDisplayName(productType); } /** @@ -850,14 +853,17 @@ public class OrderServiceImpl implements IOrderService { : getProductTypeName(product.getProductType().name()); // 3. 处理按数量计价的商品类型 - if (product.getProductType() == com.ycwl.basic.pricing.enums.ProductType.PHOTO_PRINT || - product.getProductType() == com.ycwl.basic.pricing.enums.ProductType.MACHINE_PRINT) { - if (product.getQuantity() != null && product.getQuantity() > 0) { - unitPrice = unitPrice.multiply(BigDecimal.valueOf(product.getQuantity())); - if (originalPrice != null) { - originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); - } + // 重构: 使用商品类型能力配置替代硬编码判断 + ProductTypeCapability capability = productTypeCapabilityService.getCapability(productTypeCode); + if (capability.getPricingModeEnum() == PricingMode.QUANTITY_BASED) { + Integer quantity = product.getQuantity() != null && product.getQuantity() > 0 + ? product.getQuantity() : 1; + unitPrice = unitPrice.multiply(BigDecimal.valueOf(quantity)); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(quantity)); } + log.debug("按数量计价: productType={}, quantity={}, unitPrice={}", + productTypeCode, quantity, unitPrice); } } @@ -894,7 +900,8 @@ public class OrderServiceImpl implements IOrderService { /** * 检查重复购买 * 防止用户重复购买相同内容 - * + * 重构: 使用策略模式替代硬编码的 switch-case + * * @param userId 用户ID * @param faceId 人脸ID * @param scenicId 景区ID @@ -903,117 +910,44 @@ public class OrderServiceImpl implements IOrderService { */ private void checkDuplicatePurchase(Long userId, Long faceId, Long scenicId, List products) { for (ProductItem product : products) { - switch (product.getProductType()) { - case VLOG_VIDEO: - checkVideoAlreadyPurchased(userId, faceId, scenicId, product.getProductId()); - break; - case RECORDING_SET: - case PHOTO_SET: - checkSetAlreadyPurchased(userId, faceId, scenicId, product.getProductType()); - break; - case PHOTO_PRINT: - case PHOTO_PRINT_MU: - case PHOTO_PRINT_FX: - case MACHINE_PRINT: - // 打印类商品允许重复购买,跳过检查 - log.debug("跳过打印类商品重复购买检查: productType={}, productId={}", - product.getProductType(), product.getProductId()); - break; - default: - log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType()); - break; + String productType = product.getProductType().getCode(); + + // 获取商品类型能力配置 + ProductTypeCapability capability = productTypeCapabilityService.getCapability(productType); + + // 如果允许重复购买,直接跳过 + if (Boolean.TRUE.equals(capability.getAllowDuplicatePurchase())) { + log.debug("商品类型允许重复购买,跳过检查: productType={}, productId={}", + productType, product.getProductId()); + continue; + } + + // 获取检查策略并执行 + DuplicateCheckStrategy strategy = capability.getDuplicateCheckStrategyEnum(); + if (strategy != null && strategy != DuplicateCheckStrategy.NO_CHECK) { + try { + IDuplicatePurchaseChecker checker = duplicatePurchaseCheckerFactory.getChecker(strategy); + + // 构建检查上下文 + DuplicateCheckContext context = new DuplicateCheckContext(); + context.setUserId(String.valueOf(userId)); + context.setScenicId(String.valueOf(scenicId)); + context.setProductType(productType); + context.setProductId(product.getProductId()); + context.setProducts(products); + context.addParam("faceId", faceId); + + // 执行检查 + checker.check(context); + } catch (DuplicatePurchaseException e) { + // 重新抛出重复购买异常 + throw e; + } catch (Exception e) { + log.error("重复购买检查失败,策略: {}, productType: {}", strategy, productType, e); + // 检查失败时为了安全起见,默认拒绝 + throw new BaseException("重复购买检查失败,请稍后重试"); + } } } } - - /** - * 检查视频是否已经购买 - * - * @param userId 用户ID - * @param faceId 人脸ID - * @param scenicId 景区ID - * @param videoId 视频ID - * @throws DuplicatePurchaseException 如果已购买 - */ - private void checkVideoAlreadyPurchased(Long userId, Long faceId, Long scenicId, String videoId) { - // 构建查询条件:查找已支付的有效订单中包含该视频的订单 - QueryWrapper orderQuery = new QueryWrapper<>(); - orderQuery.eq("member_id", userId) - .eq("face_id", faceId) - .eq("scenic_id", scenicId) - .eq("payment_status", PaymentStatus.PAID.getCode()) - .in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode()) - .eq("deleted", 0); - - List existingOrders = orderV2Mapper.selectList(orderQuery); - - for (OrderV2 order : existingOrders) { - // 检查订单明细中是否包含该视频 - QueryWrapper itemQuery = new QueryWrapper<>(); - itemQuery.eq("order_id", order.getId()) - .eq("product_type", com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO.name()) - .eq("product_id", videoId); - - long count = orderItemMapper.selectCount(itemQuery); - if (count > 0) { - log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}", - userId, faceId, scenicId, videoId, order.getId()); - throw new DuplicatePurchaseException( - "您已购买过此视频", - order.getId(), - order.getOrderNo(), - com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO, - videoId - ); - } - } - - log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}", - userId, faceId, scenicId, videoId); - } - - /** - * 检查套餐(录像集/照相集)是否已经购买 - * - * @param userId 用户ID - * @param faceId 人脸ID - * @param scenicId 景区ID - * @param productType 商品类型 - * @throws DuplicatePurchaseException 如果已购买 - */ - private void checkSetAlreadyPurchased(Long userId, Long faceId, Long scenicId, - com.ycwl.basic.pricing.enums.ProductType productType) { - // 构建查询条件:查找已支付的有效订单中包含该类型套餐的订单 - QueryWrapper orderQuery = new QueryWrapper<>(); - orderQuery.eq("member_id", userId) - .eq("face_id", faceId) - .eq("scenic_id", scenicId) - .eq("payment_status", PaymentStatus.PAID.getCode()) - .in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode()) - .eq("deleted", 0); - - List existingOrders = orderV2Mapper.selectList(orderQuery); - - for (OrderV2 order : existingOrders) { - // 检查订单明细中是否包含该类型的套餐 - QueryWrapper itemQuery = new QueryWrapper<>(); - itemQuery.eq("order_id", order.getId()) - .eq("product_type", productType.name()); - - long count = orderItemMapper.selectCount(itemQuery); - if (count > 0) { - log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}", - userId, faceId, scenicId, productType, order.getId()); - throw new DuplicatePurchaseException( - "您已购买过此类型的套餐", - order.getId(), - order.getOrderNo(), - productType - ); - } - } - - log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}", - userId, faceId, scenicId, productType); - } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/strategy/DuplicateCheckContext.java b/src/main/java/com/ycwl/basic/order/strategy/DuplicateCheckContext.java new file mode 100644 index 00000000..430552e6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/strategy/DuplicateCheckContext.java @@ -0,0 +1,65 @@ +package com.ycwl.basic.order.strategy; + +import com.ycwl.basic.pricing.dto.ProductItem; +import lombok.Data; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 重复购买检查上下文 + * 封装检查所需的所有参数 + */ +@Data +public class DuplicateCheckContext { + + /** + * 用户ID + */ + private String userId; + + /** + * 景区ID + */ + private String scenicId; + + /** + * 商品类型代码 + */ + private String productType; + + /** + * 商品ID(视频ID、套餐ID等) + */ + private String productId; + + /** + * 当前购物车中的所有商品 + */ + private List products; + + /** + * 额外参数(扩展用) + */ + private Map additionalParams; + + public DuplicateCheckContext() { + this.additionalParams = new HashMap<>(); + } + + /** + * 添加额外参数 + */ + public void addParam(String key, Object value) { + this.additionalParams.put(key, value); + } + + /** + * 获取额外参数 + */ + @SuppressWarnings("unchecked") + public T getParam(String key) { + return (T) this.additionalParams.get(key); + } +} diff --git a/src/main/java/com/ycwl/basic/order/strategy/IDuplicatePurchaseChecker.java b/src/main/java/com/ycwl/basic/order/strategy/IDuplicatePurchaseChecker.java new file mode 100644 index 00000000..038bc2a6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/strategy/IDuplicatePurchaseChecker.java @@ -0,0 +1,43 @@ +package com.ycwl.basic.order.strategy; + +import com.ycwl.basic.order.exception.DuplicatePurchaseException; +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; + +/** + * 重复购买检查策略接口 + * + * 设计原则: + * 1. 单一职责: 每个策略只负责一种检查逻辑 + * 2. 开放扩展: 通过实现接口添加新策略 + * 3. 自动注册: Spring 自动扫描并注册所有实现类 + */ +public interface IDuplicatePurchaseChecker { + + /** + * 获取策略类型 + * 用于策略工厂的注册和查找 + * + * @return 对应的 DuplicateCheckStrategy 枚举值 + */ + DuplicateCheckStrategy getStrategyType(); + + /** + * 执行重复购买检查 + * + * @param context 检查上下文,包含用户ID、商品信息等 + * @throws DuplicatePurchaseException 如果检测到重复购买 + */ + void check(DuplicateCheckContext context) throws DuplicatePurchaseException; + + /** + * 是否支持该商品类型 + * 默认实现:所有策略都支持所有商品类型 + * 可在具体实现中覆盖以限制适用范围 + * + * @param productType 商品类型代码 + * @return true-支持, false-不支持 + */ + default boolean supports(String productType) { + return true; + } +} diff --git a/src/main/java/com/ycwl/basic/order/strategy/impl/NoCheckDuplicateChecker.java b/src/main/java/com/ycwl/basic/order/strategy/impl/NoCheckDuplicateChecker.java new file mode 100644 index 00000000..56587199 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/strategy/impl/NoCheckDuplicateChecker.java @@ -0,0 +1,31 @@ +package com.ycwl.basic.order.strategy.impl; + +import com.ycwl.basic.order.strategy.DuplicateCheckContext; +import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker; +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 不检查重复购买策略 + * 适用于:打印类商品等允许重复购买的商品类型 + * + * 检查逻辑: + * 不执行任何检查,直接通过 + */ +@Slf4j +@Component +public class NoCheckDuplicateChecker implements IDuplicatePurchaseChecker { + + @Override + public DuplicateCheckStrategy getStrategyType() { + return DuplicateCheckStrategy.NO_CHECK; + } + + @Override + public void check(DuplicateCheckContext context) { + // 不检查,直接通过 + log.debug("跳过重复购买检查: productType={}, productId={}", + context.getProductType(), context.getProductId()); + } +} diff --git a/src/main/java/com/ycwl/basic/order/strategy/impl/SetIdDuplicateChecker.java b/src/main/java/com/ycwl/basic/order/strategy/impl/SetIdDuplicateChecker.java new file mode 100644 index 00000000..7226c0c3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/strategy/impl/SetIdDuplicateChecker.java @@ -0,0 +1,101 @@ +package com.ycwl.basic.order.strategy.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.ycwl.basic.order.entity.OrderItemV2; +import com.ycwl.basic.order.entity.OrderV2; +import com.ycwl.basic.order.enums.OrderStatus; +import com.ycwl.basic.order.enums.PaymentStatus; +import com.ycwl.basic.order.exception.DuplicatePurchaseException; +import com.ycwl.basic.order.mapper.OrderItemMapper; +import com.ycwl.basic.order.mapper.OrderV2Mapper; +import com.ycwl.basic.order.strategy.DuplicateCheckContext; +import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker; +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 按套餐ID检查重复购买策略 + * 适用于:RECORDING_SET(录像集)、PHOTO_SET(照相集)商品类型 + * + * 检查逻辑: + * 1. 查找用户在该景区已支付的有效订单 + * 2. 检查订单明细中是否包含相同类型的套餐 + * 3. 如果存在,抛出重复购买异常 + */ +@Slf4j +@Component +public class SetIdDuplicateChecker implements IDuplicatePurchaseChecker { + + @Autowired + private OrderV2Mapper orderV2Mapper; + + @Autowired + private OrderItemMapper orderItemMapper; + + @Override + public DuplicateCheckStrategy getStrategyType() { + return DuplicateCheckStrategy.CHECK_BY_SET_ID; + } + + @Override + public void check(DuplicateCheckContext context) throws DuplicatePurchaseException { + String userId = context.getUserId(); + String scenicId = context.getScenicId(); + String productType = context.getProductType(); + + // 获取人脸ID(从扩展参数中) + Long faceId = context.getParam("faceId"); + + log.debug("执行套餐ID重复购买检查: userId={}, faceId={}, scenicId={}, productType={}", + userId, faceId, scenicId, productType); + + // 构建查询条件:查找已支付的有效订单 + QueryWrapper orderQuery = new QueryWrapper<>(); + orderQuery.eq("member_id", userId) + .eq("scenic_id", scenicId) + .eq("payment_status", PaymentStatus.PAID.getCode()) + .in("order_status", + OrderStatus.PAID.getCode(), + OrderStatus.PROCESSING.getCode(), + OrderStatus.COMPLETED.getCode()) + .eq("deleted", 0); + + // 如果提供了人脸ID,也作为查询条件 + if (faceId != null) { + orderQuery.eq("face_id", faceId); + } + + List existingOrders = orderV2Mapper.selectList(orderQuery); + + for (OrderV2 order : existingOrders) { + // 检查订单明细中是否包含该类型的套餐 + QueryWrapper itemQuery = new QueryWrapper<>(); + itemQuery.eq("order_id", order.getId()) + .eq("product_type", productType); + + long count = orderItemMapper.selectCount(itemQuery); + if (count > 0) { + log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}", + userId, faceId, scenicId, productType, order.getId()); + + // 为了兼容旧代码,需要转换为 ProductType 枚举 + com.ycwl.basic.pricing.enums.ProductType productTypeEnum = + com.ycwl.basic.pricing.enums.ProductType.fromCode(productType); + + throw new DuplicatePurchaseException( + "您已购买过此类型的套餐", + order.getId(), + order.getOrderNo(), + productTypeEnum + ); + } + } + + log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}", + userId, faceId, scenicId, productType); + } +} diff --git a/src/main/java/com/ycwl/basic/order/strategy/impl/VideoIdDuplicateChecker.java b/src/main/java/com/ycwl/basic/order/strategy/impl/VideoIdDuplicateChecker.java new file mode 100644 index 00000000..ccf53589 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/strategy/impl/VideoIdDuplicateChecker.java @@ -0,0 +1,100 @@ +package com.ycwl.basic.order.strategy.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.ycwl.basic.order.entity.OrderItemV2; +import com.ycwl.basic.order.entity.OrderV2; +import com.ycwl.basic.order.enums.OrderStatus; +import com.ycwl.basic.order.enums.PaymentStatus; +import com.ycwl.basic.order.exception.DuplicatePurchaseException; +import com.ycwl.basic.order.mapper.OrderItemMapper; +import com.ycwl.basic.order.mapper.OrderV2Mapper; +import com.ycwl.basic.order.strategy.DuplicateCheckContext; +import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker; +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 按视频ID检查重复购买策略 + * 适用于:VLOG_VIDEO 商品类型 + * + * 检查逻辑: + * 1. 查找用户在该景区已支付的有效订单 + * 2. 检查订单明细中是否包含相同的视频ID + * 3. 如果存在,抛出重复购买异常 + */ +@Slf4j +@Component +public class VideoIdDuplicateChecker implements IDuplicatePurchaseChecker { + + @Autowired + private OrderV2Mapper orderV2Mapper; + + @Autowired + private OrderItemMapper orderItemMapper; + + @Override + public DuplicateCheckStrategy getStrategyType() { + return DuplicateCheckStrategy.CHECK_BY_VIDEO_ID; + } + + @Override + public void check(DuplicateCheckContext context) throws DuplicatePurchaseException { + String userId = context.getUserId(); + String scenicId = context.getScenicId(); + String productId = context.getProductId(); + + // 获取人脸ID(从扩展参数中) + Long faceId = context.getParam("faceId"); + + log.debug("执行视频ID重复购买检查: userId={}, faceId={}, scenicId={}, videoId={}", + userId, faceId, scenicId, productId); + + // 构建查询条件:查找已支付的有效订单 + QueryWrapper orderQuery = new QueryWrapper<>(); + orderQuery.eq("member_id", userId) + .eq("scenic_id", scenicId) + .eq("payment_status", PaymentStatus.PAID.getCode()) + .in("order_status", + OrderStatus.PAID.getCode(), + OrderStatus.PROCESSING.getCode(), + OrderStatus.COMPLETED.getCode()) + .eq("deleted", 0); + + // 如果提供了人脸ID,也作为查询条件 + if (faceId != null) { + orderQuery.eq("face_id", faceId); + } + + List existingOrders = orderV2Mapper.selectList(orderQuery); + + for (OrderV2 order : existingOrders) { + // 检查订单明细中是否包含该视频 + QueryWrapper itemQuery = new QueryWrapper<>(); + itemQuery.eq("order_id", order.getId()) + .eq("product_type", "VLOG_VIDEO") + .eq("product_id", productId); + + long count = orderItemMapper.selectCount(itemQuery); + if (count > 0) { + log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}", + userId, faceId, scenicId, productId, order.getId()); + + // 为了兼容旧代码,需要转换为 ProductType 枚举 + throw new DuplicatePurchaseException( + "您已购买过此视频", + order.getId(), + order.getOrderNo(), + com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO, + productId + ); + } + } + + log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}", + userId, faceId, scenicId, productId); + } +} diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java index 969b9f58..ac263ae4 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java @@ -6,6 +6,9 @@ import com.ycwl.basic.pricing.entity.PriceTierConfig; import com.ycwl.basic.pricing.enums.ProductType; import com.ycwl.basic.pricing.exception.PriceCalculationException; import com.ycwl.basic.pricing.service.*; +import com.ycwl.basic.product.capability.PricingMode; +import com.ycwl.basic.product.capability.ProductTypeCapability; +import com.ycwl.basic.product.service.IProductTypeCapabilityService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -29,16 +32,18 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { private final IPriceBundleService bundleService; private final IDiscountDetectionService discountDetectionService; private final IVoucherService voucherService; + private final IProductTypeCapabilityService productTypeCapabilityService; /** - * 判断是否为打印类商品 - * 打印类商品的价格计算方式为:单价 × 数量 + * 判断是否为按数量计价的商品 + * 重构: 使用商品类型能力配置替代硬编码 + * + * @param productType 商品类型代码 + * @return true-按数量计价, false-固定价格 */ - private boolean isPrintProduct(ProductType productType) { - return productType == ProductType.PHOTO_PRINT - || productType == ProductType.PHOTO_PRINT_MU - || productType == ProductType.PHOTO_PRINT_FX - || productType == ProductType.MACHINE_PRINT; + private boolean isQuantityBasedPricing(String productType) { + ProductTypeCapability capability = productTypeCapabilityService.getCapability(productType); + return capability.getPricingModeEnum() == PricingMode.QUANTITY_BASED; } @Override @@ -201,7 +206,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { try { PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId); if (baseConfig != null) { - if (isPrintProduct(productType)) { + if (isQuantityBasedPricing(productType.getCode())) { return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); } else { return baseConfig.getBasePrice(); @@ -216,7 +221,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { try { PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default"); if (defaultConfig != null) { - if (isPrintProduct(productType)) { + if (isQuantityBasedPricing(productType.getCode())) { return defaultConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); } else { return defaultConfig.getBasePrice(); @@ -230,7 +235,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { List configs = productConfigService.getProductConfig(productType.getCode()); if (!configs.isEmpty()) { PriceProductConfig baseConfig = configs.get(0); // 使用第一个配置作为默认 - if (isPrintProduct(productType)) { + if (isQuantityBasedPricing(productType.getCode())) { return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); } else { return baseConfig.getBasePrice(); @@ -264,7 +269,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { actualPrice = baseConfig.getBasePrice(); originalPrice = baseConfig.getOriginalPrice(); - if (isPrintProduct(productType)) { + if (isQuantityBasedPricing(productType.getCode())) { actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); if (originalPrice != null) { originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); @@ -284,7 +289,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { actualPrice = defaultConfig.getBasePrice(); originalPrice = defaultConfig.getOriginalPrice(); - if (isPrintProduct(productType)) { + if (isQuantityBasedPricing(productType.getCode())) { actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); if (originalPrice != null) { originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); @@ -303,7 +308,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { actualPrice = baseConfig.getBasePrice(); originalPrice = baseConfig.getOriginalPrice(); - if (isPrintProduct(productType)) { + if (isQuantityBasedPricing(productType.getCode())) { actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); if (originalPrice != null) { originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); diff --git a/src/main/java/com/ycwl/basic/product/capability/DuplicateCheckStrategy.java b/src/main/java/com/ycwl/basic/product/capability/DuplicateCheckStrategy.java new file mode 100644 index 00000000..38a51fd5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/capability/DuplicateCheckStrategy.java @@ -0,0 +1,63 @@ +package com.ycwl.basic.product.capability; + +/** + * 重复购买检查策略枚举 + * 定义不同商品类型的重复购买检查规则 + */ +public enum DuplicateCheckStrategy { + + /** + * 不检查(如打印类商品) + * 允许重复购买 + */ + NO_CHECK("NO_CHECK", "不检查"), + + /** + * 按视频ID检查(VLOG_VIDEO) + * 同一个视频不允许重复购买 + */ + CHECK_BY_VIDEO_ID("CHECK_BY_VIDEO_ID", "按视频ID检查"), + + /** + * 按套餐ID检查(RECORDING_SET, PHOTO_SET) + * 同一个套餐不允许重复购买 + */ + CHECK_BY_SET_ID("CHECK_BY_SET_ID", "按套餐ID检查"), + + /** + * 自定义策略(通过扩展实现) + * 预留扩展点,用于未来更复杂的检查逻辑 + */ + CUSTOM("CUSTOM", "自定义策略"); + + private final String code; + private final String description; + + DuplicateCheckStrategy(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据代码获取枚举值 + */ + public static DuplicateCheckStrategy fromCode(String code) { + if (code == null) { + return null; + } + for (DuplicateCheckStrategy strategy : DuplicateCheckStrategy.values()) { + if (strategy.code.equals(code)) { + return strategy; + } + } + throw new IllegalArgumentException("未知的重复检查策略: " + code); + } +} diff --git a/src/main/java/com/ycwl/basic/product/capability/PricingMode.java b/src/main/java/com/ycwl/basic/product/capability/PricingMode.java new file mode 100644 index 00000000..20ffef0d --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/capability/PricingMode.java @@ -0,0 +1,57 @@ +package com.ycwl.basic.product.capability; + +/** + * 定价模式枚举 + * 定义商品的价格计算方式 + */ +public enum PricingMode { + + /** + * 固定价格(不受数量影响) + * 适用于:视频类、照片集类商品 + */ + FIXED("FIXED", "固定价格"), + + /** + * 基于数量(价格 = 单价 × 数量) + * 适用于:打印类商品 + */ + QUANTITY_BASED("QUANTITY_BASED", "基于数量"), + + /** + * 分层定价(根据数量区间) + * 适用于:支持阶梯定价的商品 + */ + TIERED("TIERED", "分层定价"); + + private final String code; + private final String description; + + PricingMode(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据代码获取枚举值 + */ + public static PricingMode fromCode(String code) { + if (code == null) { + return null; + } + for (PricingMode mode : PricingMode.values()) { + if (mode.code.equals(code)) { + return mode; + } + } + throw new IllegalArgumentException("未知的定价模式: " + code); + } +} diff --git a/src/main/java/com/ycwl/basic/product/capability/ProductTypeCapability.java b/src/main/java/com/ycwl/basic/product/capability/ProductTypeCapability.java new file mode 100644 index 00000000..0c8754fd --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/capability/ProductTypeCapability.java @@ -0,0 +1,156 @@ +package com.ycwl.basic.product.capability; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 商品类型能力配置实体 + * 统一管理商品类型的各项能力和行为特征 + * + * 设计目标: + * 1. 消除代码中的硬编码,将商品类型相关的业务规则配置化 + * 2. 提供统一的商品类型能力查询接口 + * 3. 支持通过配置快速扩展新商品类型 + */ +@Data +@TableName(value = "product_type_capability", autoResultMap = true) +public class ProductTypeCapability { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 商品类型代码(唯一) + * 如:VLOG_VIDEO, PHOTO_PRINT 等 + */ + private String productType; + + /** + * 显示名称 + * 用于前端展示和支付页面 + */ + private String displayName; + + /** + * 商品分类 + * 如:VIDEO(视频类), PRINT(打印类), PHOTO_SET(照片集类) + */ + private String category; + + // ========== 定价相关 ========== + + /** + * 定价模式 + * FIXED: 固定价格 + * QUANTITY_BASED: 基于数量(价格 = 单价 × 数量) + * TIERED: 分层定价 + */ + private String pricingMode; + + /** + * 是否支持阶梯定价 + */ + private Boolean supportsTierPricing; + + // ========== 购买限制 ========== + + /** + * 是否允许重复购买 + */ + private Boolean allowDuplicatePurchase; + + /** + * 重复购买检查策略 + * NO_CHECK: 不检查 + * CHECK_BY_VIDEO_ID: 按视频ID检查 + * CHECK_BY_SET_ID: 按套餐ID检查 + * CUSTOM: 自定义策略 + */ + private String duplicateCheckStrategy; + + // ========== 优惠能力 ========== + + /** + * 是否可使用优惠券 + */ + private Boolean canUseCoupon; + + /** + * 是否可使用券码 + */ + private Boolean canUseVoucher; + + /** + * 是否可使用一口价优惠 + */ + private Boolean canUseOnePrice; + + /** + * 是否可参与打包优惠 + */ + private Boolean canUseBundle; + + // ========== 扩展属性 ========== + + /** + * 扩展属性(JSON 格式) + * 用于存储特定商品类型的额外配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map metadata; + + /** + * 是否启用 + */ + private Boolean isActive; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + private LocalDateTime updatedAt; + + // ========== 便捷方法 ========== + + /** + * 获取定价模式枚举 + */ + public PricingMode getPricingModeEnum() { + return PricingMode.fromCode(this.pricingMode); + } + + /** + * 获取重复检查策略枚举 + */ + public DuplicateCheckStrategy getDuplicateCheckStrategyEnum() { + return DuplicateCheckStrategy.fromCode(this.duplicateCheckStrategy); + } + + /** + * 设置定价模式枚举 + */ + public void setPricingModeEnum(PricingMode mode) { + this.pricingMode = mode != null ? mode.getCode() : null; + } + + /** + * 设置重复检查策略枚举 + */ + public void setDuplicateCheckStrategyEnum(DuplicateCheckStrategy strategy) { + this.duplicateCheckStrategy = strategy != null ? strategy.getCode() : null; + } +} diff --git a/src/main/java/com/ycwl/basic/product/mapper/ProductTypeCapabilityMapper.java b/src/main/java/com/ycwl/basic/product/mapper/ProductTypeCapabilityMapper.java new file mode 100644 index 00000000..baee9c5e --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/mapper/ProductTypeCapabilityMapper.java @@ -0,0 +1,21 @@ +package com.ycwl.basic.product.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.product.capability.ProductTypeCapability; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 商品类型能力配置 Mapper + */ +@Mapper +public interface ProductTypeCapabilityMapper extends BaseMapper { + + /** + * 根据商品类型代码查询能力配置 + * + * @param productType 商品类型代码 + * @return 能力配置,不存在时返回 null + */ + ProductTypeCapability selectByProductType(@Param("productType") String productType); +} diff --git a/src/main/java/com/ycwl/basic/product/service/IProductTypeCapabilityService.java b/src/main/java/com/ycwl/basic/product/service/IProductTypeCapabilityService.java new file mode 100644 index 00000000..a22aa5fd --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/service/IProductTypeCapabilityService.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.product.service; + +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import com.ycwl.basic.product.capability.PricingMode; +import com.ycwl.basic.product.capability.ProductTypeCapability; + +/** + * 商品类型能力服务接口 + * 提供商品类型能力配置的查询和管理功能 + * + * 设计原则: + * 1. 缓存优先:所有查询都支持缓存,提高性能 + * 2. 降级兜底:查询失败时返回安全的默认配置 + * 3. 接口简洁:提供便捷方法,简化调用方代码 + */ +public interface IProductTypeCapabilityService { + + /** + * 获取商品类型能力配置(支持缓存) + * + * @param productType 商品类型代码(如 VLOG_VIDEO) + * @return 商品类型能力配置,查询失败时返回默认配置 + */ + ProductTypeCapability getCapability(String productType); + + /** + * 获取商品显示名称 + * + * @param productType 商品类型代码 + * @return 显示名称,查询失败时返回"景区商品" + */ + String getDisplayName(String productType); + + /** + * 判断是否允许重复购买 + * + * @param productType 商品类型代码 + * @return true-允许重复购买, false-不允许 + */ + boolean allowDuplicatePurchase(String productType); + + /** + * 获取定价模式 + * + * @param productType 商品类型代码 + * @return 定价模式枚举 + */ + PricingMode getPricingMode(String productType); + + /** + * 获取重复检查策略 + * + * @param productType 商品类型代码 + * @return 重复检查策略枚举 + */ + DuplicateCheckStrategy getDuplicateCheckStrategy(String productType); + + /** + * 刷新缓存 + * 用于配置更新后手动触发缓存刷新 + */ + void refreshCache(); + + /** + * 刷新指定商品类型的缓存 + * + * @param productType 商品类型代码 + */ + void refreshCache(String productType); +} diff --git a/src/main/java/com/ycwl/basic/product/service/impl/ProductTypeCapabilityServiceImpl.java b/src/main/java/com/ycwl/basic/product/service/impl/ProductTypeCapabilityServiceImpl.java new file mode 100644 index 00000000..9920e65b --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/service/impl/ProductTypeCapabilityServiceImpl.java @@ -0,0 +1,162 @@ +package com.ycwl.basic.product.service.impl; + +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import com.ycwl.basic.product.capability.PricingMode; +import com.ycwl.basic.product.capability.ProductTypeCapability; +import com.ycwl.basic.product.mapper.ProductTypeCapabilityMapper; +import com.ycwl.basic.product.service.IProductTypeCapabilityService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +/** + * 商品类型能力服务实现 + * + * 缓存策略: + * - 使用 Spring Cache 进行缓存 + * - 缓存名称:productTypeCapability + * - 缓存key:商品类型代码 + * - 缓存失效:手动调用 refreshCache 方法 + */ +@Slf4j +@Service +@CacheConfig(cacheNames = "productTypeCapability") +public class ProductTypeCapabilityServiceImpl implements IProductTypeCapabilityService { + + @Autowired + private ProductTypeCapabilityMapper mapper; + + @Cacheable(key = "#productType") + @Override + public ProductTypeCapability getCapability(String productType) { + if (productType == null || productType.trim().isEmpty()) { + log.warn("商品类型代码为空,返回默认配置"); + return getDefaultCapability(null); + } + + try { + ProductTypeCapability capability = mapper.selectByProductType(productType); + if (capability == null) { + log.warn("未找到商品类型能力配置: {}, 使用默认配置", productType); + return getDefaultCapability(productType); + } + + // 验证配置完整性 + if (!isValidCapability(capability)) { + log.warn("商品类型能力配置不完整: {}, 使用默认配置", productType); + return getDefaultCapability(productType); + } + + return capability; + } catch (Exception e) { + log.error("查询商品类型能力配置失败: {}, 降级到默认配置", productType, e); + return getDefaultCapability(productType); + } + } + + @Override + public String getDisplayName(String productType) { + ProductTypeCapability capability = getCapability(productType); + return capability.getDisplayName(); + } + + @Override + public boolean allowDuplicatePurchase(String productType) { + ProductTypeCapability capability = getCapability(productType); + return Boolean.TRUE.equals(capability.getAllowDuplicatePurchase()); + } + + @Override + public PricingMode getPricingMode(String productType) { + ProductTypeCapability capability = getCapability(productType); + return capability.getPricingModeEnum(); + } + + @Override + public DuplicateCheckStrategy getDuplicateCheckStrategy(String productType) { + ProductTypeCapability capability = getCapability(productType); + return capability.getDuplicateCheckStrategyEnum(); + } + + @CacheEvict(allEntries = true) + @Override + public void refreshCache() { + log.info("刷新所有商品类型能力缓存"); + } + + @CacheEvict(key = "#productType") + @Override + public void refreshCache(String productType) { + log.info("刷新商品类型能力缓存: {}", productType); + } + + /** + * 获取默认能力配置(兜底方案) + * 设计原则:安全第一,宁可限制也不放开 + * + * @param productType 商品类型代码(可为null) + * @return 安全的默认配置 + */ + private ProductTypeCapability getDefaultCapability(String productType) { + ProductTypeCapability defaultCap = new ProductTypeCapability(); + defaultCap.setProductType(productType); + defaultCap.setDisplayName("景区商品"); + defaultCap.setCategory("GENERAL"); + + // 定价相关:默认固定价格 + defaultCap.setPricingMode(PricingMode.FIXED.getCode()); + defaultCap.setSupportsTierPricing(false); + + // 购买限制:默认不允许重复购买(安全策略) + defaultCap.setAllowDuplicatePurchase(false); + defaultCap.setDuplicateCheckStrategy(DuplicateCheckStrategy.NO_CHECK.getCode()); + + // 优惠能力:默认全部允许 + defaultCap.setCanUseCoupon(true); + defaultCap.setCanUseVoucher(true); + defaultCap.setCanUseOnePrice(true); + defaultCap.setCanUseBundle(true); + + defaultCap.setIsActive(true); + + return defaultCap; + } + + /** + * 验证配置完整性 + */ + private boolean isValidCapability(ProductTypeCapability capability) { + if (capability == null) { + return false; + } + + // 必填字段检查 + if (capability.getProductType() == null || capability.getProductType().trim().isEmpty()) { + return false; + } + + if (capability.getDisplayName() == null || capability.getDisplayName().trim().isEmpty()) { + return false; + } + + if (capability.getPricingMode() == null) { + return false; + } + + // 验证枚举值有效性 + try { + PricingMode.fromCode(capability.getPricingMode()); + if (capability.getDuplicateCheckStrategy() != null) { + DuplicateCheckStrategy.fromCode(capability.getDuplicateCheckStrategy()); + } + } catch (IllegalArgumentException e) { + log.error("商品类型能力配置包含无效的枚举值: {}", capability.getProductType(), e); + return false; + } + + return true; + } +} diff --git a/src/main/resources/mapper/ProductTypeCapabilityMapper.xml b/src/main/resources/mapper/ProductTypeCapabilityMapper.xml new file mode 100644 index 00000000..4e1699dd --- /dev/null +++ b/src/main/resources/mapper/ProductTypeCapabilityMapper.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, product_type, display_name, category, + pricing_mode, supports_tier_pricing, + allow_duplicate_purchase, duplicate_check_strategy, + can_use_coupon, can_use_voucher, can_use_one_price, can_use_bundle, + metadata, is_active, created_at, updated_at + + + + + + From 8a88c74df28a2a7f94faec148c1d9ab216fe6498 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 27 Nov 2025 13:55:51 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(pricing):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=99=AF=E5=8C=BA=E7=BB=B4=E5=BA=A6=E7=9A=84=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=92=8C=E4=BC=98=E6=83=A0=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增按景区ID查询商品配置和阶梯价格配置的方法 - 扩展价格计算服务以支持景区级别的优惠策略 - 更新优惠券和代金券提供者以使用景区维度配置 - 修改商品配置服务实现多级查询优先级(景区特定->景区默认->全局特定->全局默认) - 添加商品类型能力服务测试用例 - 增强价格计算逻辑的容错性和向后兼容性 --- .../mapper/PriceProductConfigMapper.java | 8 ++ .../pricing/mapper/PriceTierConfigMapper.java | 12 ++ .../service/IProductConfigService.java | 39 +++++- .../service/impl/CouponDiscountProvider.java | 24 ++-- .../OnePricePurchaseDiscountProvider.java | 37 +++-- .../impl/PriceCalculationServiceImpl.java | 62 ++++----- .../impl/ProductConfigServiceImpl.java | 101 ++++++++++++++ .../service/impl/VoucherDiscountProvider.java | 24 ++-- .../ProductTypeCapabilityServiceTest.java | 127 ++++++++++++++++++ 9 files changed, 357 insertions(+), 77 deletions(-) create mode 100644 src/test/java/com/ycwl/basic/product/service/ProductTypeCapabilityServiceTest.java diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java index 1f890f24..3cb09c25 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java @@ -33,6 +33,14 @@ public interface PriceProductConfigMapper extends BaseMapper */ @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1") PriceProductConfig selectByProductTypeAndId(String productType, String productId); + + /** + * 根据商品类型、商品ID和景区ID查询配置(支持景区维度) + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND scenic_id = #{scenicId} AND is_active = 1") + PriceProductConfig selectByProductTypeIdAndScenic(@Param("productType") String productType, + @Param("productId") String productId, + @Param("scenicId") String scenicId); /** * 检查是否存在default配置(包含禁用的) diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java index edbc30d2..cd745477 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java @@ -26,6 +26,18 @@ public interface PriceTierConfigMapper extends BaseMapper { PriceTierConfig selectByProductTypeAndQuantity(@Param("productType") String productType, @Param("productId") String productId, @Param("quantity") Integer quantity); + + /** + * 根据商品类型、商品ID、数量和景区ID查询匹配的阶梯价格(支持景区维度) + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} AND scenic_id = #{scenicId} " + + "AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " + + "AND is_active = 1 ORDER BY sort_order ASC LIMIT 1") + PriceTierConfig selectByProductTypeQuantityAndScenic(@Param("productType") String productType, + @Param("productId") String productId, + @Param("quantity") Integer quantity, + @Param("scenicId") String scenicId); /** * 根据商品类型查询所有阶梯配置 diff --git a/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java index c58bd937..e7fc7eff 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java @@ -23,22 +23,55 @@ public interface IProductConfigService { /** * 根据商品类型和商品ID获取精确配置 - * + * * @param productType 商品类型 * @param productId 具体商品ID * @return 商品配置 */ PriceProductConfig getProductConfig(String productType, String productId); - + + /** + * 根据商品类型、商品ID和景区ID获取精确配置(支持景区维度的优惠策略控制) + * + * 查询优先级: + * 1. 景区+商品ID: (productType, productId, scenicId) + * 2. 景区+默认: (productType, "default", scenicId) + * 3. 全局+商品ID: (productType, productId, null) + * 4. 全局+默认: (productType, "default", null) + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @param scenicId 景区ID + * @return 商品配置(包含优惠策略控制字段) + */ + PriceProductConfig getProductConfig(String productType, String productId, Long scenicId); + /** * 根据商品类型、商品ID和数量获取阶梯价格配置 - * + * * @param productType 商品类型 * @param productId 具体商品ID * @param quantity 数量 * @return 阶梯价格配置 */ PriceTierConfig getTierConfig(String productType, String productId, Integer quantity); + + /** + * 根据商品类型、商品ID、数量和景区ID获取阶梯价格配置(支持景区维度) + * + * 查询优先级: + * 1. 景区+商品ID: (productType, productId, quantity, scenicId) + * 2. 景区+默认: (productType, "default", quantity, scenicId) + * 3. 全局+商品ID: (productType, productId, quantity, null) + * 4. 全局+默认: (productType, "default", quantity, null) + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @param quantity 数量 + * @param scenicId 景区ID + * @return 阶梯价格配置 + */ + PriceTierConfig getTierConfig(String productType, String productId, Integer quantity, Long scenicId); /** * 获取所有启用的商品配置 diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java index 40c2696a..96d52b9f 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java @@ -138,18 +138,18 @@ public class CouponDiscountProvider implements IDiscountProvider { } } - // 检查单个商品的优惠券使用开关 + // 检查单个商品的优惠券使用开关(使用景区维度配置) for (ProductItem product : context.getProducts()) { String productId = product.getProductId() != null ? product.getProductId() : "default"; - + try { PriceProductConfig productConfig = productConfigService.getProductConfig( - product.getProductType().getCode(), productId); - + product.getProductType().getCode(), productId, context.getScenicId()); + if (productConfig != null) { if (!Boolean.TRUE.equals(productConfig.getCanUseCoupon())) { - log.debug("商品配置不允许使用优惠券: productType={}, productId={}", - product.getProductType().getCode(), productId); + log.debug("商品配置不允许使用优惠券: productType={}, productId={}, scenicId={}", + product.getProductType().getCode(), productId, context.getScenicId()); return false; } } @@ -157,18 +157,18 @@ public class CouponDiscountProvider implements IDiscountProvider { // 如果获取具体商品配置失败,尝试获取default配置 try { PriceProductConfig defaultConfig = productConfigService.getProductConfig( - product.getProductType().getCode(), "default"); - + product.getProductType().getCode(), "default", context.getScenicId()); + if (defaultConfig != null) { if (!Boolean.TRUE.equals(defaultConfig.getCanUseCoupon())) { - log.debug("商品默认配置不允许使用优惠券: productType={}", - product.getProductType().getCode()); + log.debug("商品默认配置不允许使用优惠券: productType={}, scenicId={}", + product.getProductType().getCode(), context.getScenicId()); return false; } } } catch (Exception ex) { - log.warn("获取商品配置失败,默认允许使用优惠券: productType={}, productId={}", - product.getProductType().getCode(), productId); + log.warn("获取商品配置失败,默认允许使用优惠券: productType={}, productId={}, scenicId={}", + product.getProductType().getCode(), productId, context.getScenicId()); } } } 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 index ed7f0b5b..02b31be2 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseDiscountProvider.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/OnePricePurchaseDiscountProvider.java @@ -54,8 +54,8 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider { return discounts; } - // 检查商品是否支持一口价优惠 - if (!areAllProductsSupportOnePrice(context.getProducts())) { + // 检查商品是否支持一口价优惠(使用景区维度配置) + if (!areAllProductsSupportOnePrice(context.getProducts(), context.getScenicId())) { log.debug("存在不支持一口价优惠的商品,跳过一口价检测"); return discounts; } @@ -182,49 +182,48 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider { } /** - * 检查购物车中的所有商品是否都支持一口价优惠 + * 检查购物车中的所有商品是否都支持一口价优惠(使用景区维度配置) */ - private boolean areAllProductsSupportOnePrice(List products) { + private boolean areAllProductsSupportOnePrice(List products, Long scenicId) { if (products == null || products.isEmpty()) { return true; // 空购物车时默认支持 } for (ProductItem product : products) { try { - // 查询商品配置 + // 查询商品配置(使用景区维度) PriceProductConfig productConfig = productConfigService.getProductConfig( - product.getProductType().getCode(), product.getProductId()); + product.getProductType().getCode(), product.getProductId(), scenicId); if (productConfig != null) { // 检查商品是否支持一口价优惠 if (Boolean.FALSE.equals(productConfig.getCanUseOnePrice())) { - log.debug("商品 {}({}) 不支持一口价优惠", - product.getProductType().getCode(), product.getProductId()); + log.debug("商品 {}({}) 在景区 {} 不支持一口价优惠", + product.getProductType().getCode(), product.getProductId(), scenicId); return false; } } else { // 如果找不到具体商品配置,尝试查询 default 配置 PriceProductConfig defaultConfig = productConfigService.getProductConfig( - product.getProductType().getCode(), "default"); + product.getProductType().getCode(), "default", scenicId); if (defaultConfig != null) { if (Boolean.FALSE.equals(defaultConfig.getCanUseOnePrice())) { - log.debug("商品类型 {} 的默认配置不支持一口价优惠", - product.getProductType().getCode()); + log.debug("商品类型 {} 在景区 {} 的默认配置不支持一口价优惠", + product.getProductType().getCode(), scenicId); return false; } } else { - // 如果既没有具体配置也没有默认配置,默认不支持一口价优惠 - log.debug("商品 {}({}) 未找到价格配置,默认不支持一口价优惠", - product.getProductType().getCode(), product.getProductId()); - return false; + // 如果既没有具体配置也没有默认配置,默认支持(保持向后兼容) + log.debug("商品 {}({}) 在景区 {} 未找到价格配置,默认支持一口价优惠", + product.getProductType().getCode(), product.getProductId(), scenicId); + // 改为默认支持,避免配置缺失导致一口价功能不可用 } } } catch (Exception e) { - log.warn("检查商品 {}({}) 一口价优惠支持情况时发生异常,默认不支持", - product.getProductType().getCode(), product.getProductId(), e); - // 异常情况下默认不支持,避免出现意外情况 - return false; + log.warn("检查商品 {}({}) 在景区 {} 一口价优惠支持情况时发生异常,默认支持", + product.getProductType().getCode(), product.getProductId(), scenicId, e); + // 异常情况下默认支持,确保业务流程不受影响 } } diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java index ac263ae4..6bcf7532 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java @@ -66,8 +66,8 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { product.setQuantity(1); } }); - // 计算商品价格和原价 - PriceDetails priceDetails = calculateProductsPriceWithOriginal(request.getProducts()); + // 计算商品价格和原价(传入景区ID以支持景区级优惠策略控制) + PriceDetails priceDetails = calculateProductsPriceWithOriginal(request.getProducts(), request.getScenicId()); BigDecimal totalAmount = priceDetails.getTotalAmount(); BigDecimal originalTotalAmount = priceDetails.getOriginalTotalAmount(); @@ -161,27 +161,27 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { return totalAmount.setScale(2, RoundingMode.HALF_UP); } - - private PriceDetails calculateProductsPriceWithOriginal(List products) { + + private PriceDetails calculateProductsPriceWithOriginal(List products, Long scenicId) { BigDecimal totalAmount = BigDecimal.ZERO; BigDecimal originalTotalAmount = BigDecimal.ZERO; - + for (ProductItem product : products) { - // 计算实际价格和原价 - ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product); - + // 计算实际价格和原价(传入景区ID) + ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product, scenicId); + product.setUnitPrice(priceInfo.getActualPrice()); product.setOriginalPrice(priceInfo.getOriginalPrice()); - + BigDecimal subtotal = priceInfo.getActualPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount())); BigDecimal originalSubtotal = priceInfo.getOriginalPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount())); - + product.setSubtotal(subtotal); - + totalAmount = totalAmount.add(subtotal); originalTotalAmount = originalTotalAmount.add(originalSubtotal); } - + return new PriceDetails( totalAmount.setScale(2, RoundingMode.HALF_UP), originalTotalAmount.setScale(2, RoundingMode.HALF_UP) @@ -244,27 +244,27 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); } - - private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product) { + + private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product, Long scenicId) { ProductType productType = product.getProductType(); String productId = product.getProductId() != null ? product.getProductId() : "default"; - + BigDecimal actualPrice; BigDecimal originalPrice = null; - - // 优先使用基于product_id的阶梯定价 + + // 优先使用基于product_id的阶梯定价(带景区ID) PriceTierConfig tierConfig = productConfigService.getTierConfig( - productType.getCode(), productId, product.getQuantity()); - + productType.getCode(), productId, product.getQuantity(), scenicId); + if (tierConfig != null) { actualPrice = tierConfig.getPrice(); originalPrice = tierConfig.getOriginalPrice(); - log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}, originalPrice={}", - productType.getCode(), productId, product.getQuantity(), actualPrice, originalPrice); + log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, scenicId={}, price={}, originalPrice={}", + productType.getCode(), productId, product.getQuantity(), scenicId, actualPrice, originalPrice); } else { - // 使用基于product_id的基础配置 + // 使用基于product_id的基础配置(带景区ID) try { - PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId); + PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId, scenicId); if (baseConfig != null) { actualPrice = baseConfig.getBasePrice(); originalPrice = baseConfig.getOriginalPrice(); @@ -279,12 +279,12 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { throw new PriceCalculationException("无法找到具体商品配置"); } } catch (Exception e) { - log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置", - productType, productId); - - // 兜底:使用default配置 + log.warn("未找到具体商品配置: productType={}, productId={}, scenicId={}, 尝试使用通用配置", + productType, productId, scenicId); + + // 兜底:使用default配置(带景区ID) try { - PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default"); + PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default", scenicId); if (defaultConfig != null) { actualPrice = defaultConfig.getBasePrice(); originalPrice = defaultConfig.getOriginalPrice(); @@ -299,8 +299,8 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { throw new PriceCalculationException("无法找到default配置"); } } catch (Exception defaultEx) { - log.warn("未找到default配置: productType={}", productType.getCode()); - + log.warn("未找到default配置: productType={}, scenicId={}", productType.getCode(), scenicId); + // 最后兜底:使用通用配置(向后兼容) List configs = productConfigService.getProductConfig(productType.getCode()); if (!configs.isEmpty()) { @@ -320,7 +320,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { } } } - + return new ProductPriceInfo(actualPrice, originalPrice); } diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java index 0435d057..5b1477e0 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java @@ -71,6 +71,107 @@ public class ProductConfigServiceImpl implements IProductConfigService { productType, productId, quantity, config); return config; } + + @Override +// @Cacheable(value = "product-config", key = "#productType + '_' + #productId + '_' + #scenicId") + public PriceProductConfig getProductConfig(String productType, String productId, Long scenicId) { + if (scenicId == null) { + // 如果没有景区ID,使用原有逻辑 + return getProductConfig(productType, productId); + } + + // 查询优先级: + // 1. 景区+商品ID + PriceProductConfig config = productConfigMapper.selectByProductTypeIdAndScenic( + productType, productId, scenicId.toString()); + if (config != null) { + log.debug("使用景区特定商品配置: productType={}, productId={}, scenicId={}", + productType, productId, scenicId); + return config; + } + + // 2. 景区+默认 + if (!"default".equals(productId)) { + config = productConfigMapper.selectByProductTypeIdAndScenic( + productType, "default", scenicId.toString()); + if (config != null) { + log.debug("使用景区默认配置: productType={}, scenicId={}", productType, scenicId); + return config; + } + } + + // 3. 全局+商品ID (兜底) + try { + config = productConfigMapper.selectByProductTypeAndId(productType, productId); + if (config != null) { + log.debug("使用全局商品配置: productType={}, productId={}", productType, productId); + return config; + } + } catch (Exception e) { + log.debug("全局商品配置未找到: productType={}, productId={}", productType, productId); + } + + // 4. 全局+默认 (最后兜底) + config = productConfigMapper.selectByProductTypeAndId(productType, "default"); + if (config != null) { + log.debug("使用全局默认配置: productType={}", productType); + return config; + } + + throw new ProductConfigNotFoundException( + String.format("商品配置未找到: productType=%s, productId=%s, scenicId=%s", + productType, productId, scenicId)); + } + + @Override +// @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + #quantity + '_' + #scenicId") + public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity, Long scenicId) { + if (quantity == null || quantity <= 0) { + return null; + } + + if (scenicId == null) { + // 如果没有景区ID,使用原有逻辑 + return getTierConfig(productType, productId, quantity); + } + + // 查询优先级: + // 1. 景区+商品ID + PriceTierConfig config = tierConfigMapper.selectByProductTypeQuantityAndScenic( + productType, productId, quantity, scenicId.toString()); + if (config != null) { + log.debug("使用景区特定阶梯定价: productType={}, productId={}, quantity={}, scenicId={}", + productType, productId, quantity, scenicId); + return config; + } + + // 2. 景区+默认 + if (!"default".equals(productId)) { + config = tierConfigMapper.selectByProductTypeQuantityAndScenic( + productType, "default", quantity, scenicId.toString()); + if (config != null) { + log.debug("使用景区默认阶梯定价: productType={}, quantity={}, scenicId={}", + productType, quantity, scenicId); + return config; + } + } + + // 3. 全局+商品ID (兜底) + config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity); + if (config != null) { + log.debug("使用全局阶梯定价: productType={}, productId={}, quantity={}", + productType, productId, quantity); + return config; + } + + // 4. 全局+默认 (最后兜底) + config = tierConfigMapper.selectByProductTypeAndQuantity(productType, "default", quantity); + if (config != null) { + log.debug("使用全局默认阶梯定价: productType={}, quantity={}", productType, quantity); + } + + return config; + } @Override // @Cacheable(value = "active-product-configs") diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java index 0137e1e1..6ec833a8 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java @@ -224,18 +224,18 @@ public class VoucherDiscountProvider implements IDiscountProvider { } } - // 检查单个商品的券码使用开关 + // 检查单个商品的券码使用开关(使用景区维度配置) for (ProductItem product : context.getProducts()) { String productId = product.getProductId() != null ? product.getProductId() : "default"; - + try { PriceProductConfig productConfig = productConfigService.getProductConfig( - product.getProductType().getCode(), productId); - + product.getProductType().getCode(), productId, context.getScenicId()); + if (productConfig != null) { if (!Boolean.TRUE.equals(productConfig.getCanUseVoucher())) { - log.info("商品配置不允许使用券码: productType={}, productId={}", - product.getProductType().getCode(), productId); + log.info("商品配置不允许使用券码: productType={}, productId={}, scenicId={}", + product.getProductType().getCode(), productId, context.getScenicId()); return false; } } @@ -243,18 +243,18 @@ public class VoucherDiscountProvider implements IDiscountProvider { // 如果获取具体商品配置失败,尝试获取default配置 try { PriceProductConfig defaultConfig = productConfigService.getProductConfig( - product.getProductType().getCode(), "default"); - + product.getProductType().getCode(), "default", context.getScenicId()); + if (defaultConfig != null) { if (!Boolean.TRUE.equals(defaultConfig.getCanUseVoucher())) { - log.debug("商品默认配置不允许使用券码: productType={}", - product.getProductType().getCode()); + log.debug("商品默认配置不允许使用券码: productType={}, scenicId={}", + product.getProductType().getCode(), context.getScenicId()); return false; } } } catch (Exception ex) { - log.warn("获取商品配置失败,默认允许使用券码: productType={}, productId={}", - product.getProductType().getCode(), productId); + log.warn("获取商品配置失败,默认允许使用券码: productType={}, productId={}, scenicId={}", + product.getProductType().getCode(), productId, context.getScenicId()); } } } diff --git a/src/test/java/com/ycwl/basic/product/service/ProductTypeCapabilityServiceTest.java b/src/test/java/com/ycwl/basic/product/service/ProductTypeCapabilityServiceTest.java new file mode 100644 index 00000000..216f6c12 --- /dev/null +++ b/src/test/java/com/ycwl/basic/product/service/ProductTypeCapabilityServiceTest.java @@ -0,0 +1,127 @@ +package com.ycwl.basic.product.service; + +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import com.ycwl.basic.product.capability.PricingMode; +import com.ycwl.basic.product.capability.ProductTypeCapability; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 商品类型能力服务测试 + */ +@SpringBootTest +public class ProductTypeCapabilityServiceTest { + + @Autowired(required = false) + private IProductTypeCapabilityService productTypeCapabilityService; + + @Test + public void testGetCapability_VLOG_VIDEO() { + if (productTypeCapabilityService == null) { + System.out.println("服务未注入,跳过测试(可能数据库未迁移)"); + return; + } + + ProductTypeCapability capability = productTypeCapabilityService.getCapability("VLOG_VIDEO"); + + assertNotNull(capability); + assertEquals("VLOG_VIDEO", capability.getProductType()); + assertEquals("Vlog视频", capability.getDisplayName()); + assertEquals("VIDEO", capability.getCategory()); + assertEquals(PricingMode.FIXED, capability.getPricingModeEnum()); + assertEquals(false, capability.getAllowDuplicatePurchase()); + assertEquals(DuplicateCheckStrategy.CHECK_BY_VIDEO_ID, capability.getDuplicateCheckStrategyEnum()); + } + + @Test + public void testGetCapability_PHOTO_PRINT() { + if (productTypeCapabilityService == null) { + System.out.println("服务未注入,跳过测试(可能数据库未迁移)"); + return; + } + + ProductTypeCapability capability = productTypeCapabilityService.getCapability("PHOTO_PRINT"); + + assertNotNull(capability); + assertEquals("PHOTO_PRINT", capability.getProductType()); + assertEquals("照片打印", capability.getDisplayName()); + assertEquals("PRINT", capability.getCategory()); + assertEquals(PricingMode.QUANTITY_BASED, capability.getPricingModeEnum()); + assertEquals(true, capability.getAllowDuplicatePurchase()); + assertEquals(DuplicateCheckStrategy.NO_CHECK, capability.getDuplicateCheckStrategyEnum()); + } + + @Test + public void testGetDisplayName() { + if (productTypeCapabilityService == null) { + System.out.println("服务未注入,跳过测试(可能数据库未迁移)"); + return; + } + + String displayName = productTypeCapabilityService.getDisplayName("RECORDING_SET"); + assertEquals("录像集", displayName); + } + + @Test + public void testAllowDuplicatePurchase() { + if (productTypeCapabilityService == null) { + System.out.println("服务未注入,跳过测试(可能数据库未迁移)"); + return; + } + + // 视频类不允许重复购买 + assertFalse(productTypeCapabilityService.allowDuplicatePurchase("VLOG_VIDEO")); + + // 打印类允许重复购买 + assertTrue(productTypeCapabilityService.allowDuplicatePurchase("PHOTO_PRINT")); + } + + @Test + public void testGetPricingMode() { + if (productTypeCapabilityService == null) { + System.out.println("服务未注入,跳过测试(可能数据库未迁移)"); + return; + } + + // 视频类固定价格 + assertEquals(PricingMode.FIXED, productTypeCapabilityService.getPricingMode("VLOG_VIDEO")); + + // 打印类基于数量 + assertEquals(PricingMode.QUANTITY_BASED, productTypeCapabilityService.getPricingMode("PHOTO_PRINT")); + } + + @Test + public void testGetCapability_NotFound_ShouldReturnDefault() { + if (productTypeCapabilityService == null) { + System.out.println("服务未注入,跳过测试(可能数据库未迁移)"); + return; + } + + ProductTypeCapability capability = productTypeCapabilityService.getCapability("NOT_EXIST_TYPE"); + + // 应该返回默认配置而不是 null + assertNotNull(capability); + assertEquals("NOT_EXIST_TYPE", capability.getProductType()); + assertEquals("景区商品", capability.getDisplayName()); + assertEquals(PricingMode.FIXED, capability.getPricingModeEnum()); + } + + @Test + public void testGetCapability_NullOrEmpty_ShouldReturnDefault() { + if (productTypeCapabilityService == null) { + System.out.println("服务未注入,跳过测试(可能数据库未迁移)"); + return; + } + + ProductTypeCapability capability1 = productTypeCapabilityService.getCapability(null); + assertNotNull(capability1); + assertEquals("景区商品", capability1.getDisplayName()); + + ProductTypeCapability capability2 = productTypeCapabilityService.getCapability(""); + assertNotNull(capability2); + assertEquals("景区商品", capability2.getDisplayName()); + } +} From e9a59cd4669c5aa9faf4fcd3b6c856b352340ea5 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 27 Nov 2025 18:39:43 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(pricing):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=95=86=E5=93=81=E5=88=86=E7=B1=BB=E6=9E=9A=E4=B8=BE=E5=B9=B6?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E5=95=86=E5=93=81=E7=B1=BB=E5=9E=8B=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ProductCategory 枚举类,定义商品分类 - 为 ProductType 枚举增加分类关联字段 - 扩展 ProductType 枚举值并按分类分组注释 - 添加获取分类代码和描述的方法 - 实现根据代码查找枚举的静态方法 - 完善枚举类的文档注释和类型安全引用 --- .../basic/pricing/enums/ProductCategory.java | 33 +++++++++++++ .../ycwl/basic/pricing/enums/ProductType.java | 47 ++++++++++++++----- 2 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/pricing/enums/ProductCategory.java diff --git a/src/main/java/com/ycwl/basic/pricing/enums/ProductCategory.java b/src/main/java/com/ycwl/basic/pricing/enums/ProductCategory.java new file mode 100644 index 00000000..f9aa75a3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/ProductCategory.java @@ -0,0 +1,33 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 商品分类枚举 + */ +@Getter +@AllArgsConstructor +public enum ProductCategory { + + VLOG("VLOG", "Vlog类"), + PHOTO("PHOTO", "照片类"), + VIDEO("VIDEO", "视频类"), + PRINT("PRINT", "打印类"), + OTHER("OTHER", "其他"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static ProductCategory fromCode(String code) { + for (ProductCategory category : values()) { + if (category.code.equals(code)) { + return category; + } + } + throw new IllegalArgumentException("Unknown product category code: " + code); + } +} diff --git a/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java index e5021261..d2cab829 100644 --- a/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java +++ b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java @@ -9,20 +9,29 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum ProductType { - - VLOG_VIDEO("VLOG_VIDEO", "Vlog视频"), - RECORDING_SET("RECORDING_SET", "录像集"), - PHOTO_SET("PHOTO_SET", "照相集"), - PHOTO_LOG("PHOTO_LOG", "pLog图"), - PHOTO_VLOG("PHOTO_VLOG", "pLog视频"), - PHOTO_PRINT("PHOTO_PRINT", "照片打印"), - PHOTO_PRINT_MU("PHOTO_PRINT_MU", "手机照片打印"), - PHOTO_PRINT_FX("PHOTO_PRINT_FX", "特效照片打印"), - MACHINE_PRINT("MACHINE_PRINT", "一体机打印"); - + + // VLOG类 + VLOG_VIDEO("VLOG_VIDEO", "Vlog视频", ProductCategory.VLOG), + PHOTO_VLOG("PHOTO_VLOG", "pLog视频", ProductCategory.VLOG), + + // 照片类 + PHOTO("PHOTO", "照片", ProductCategory.PHOTO), + PHOTO_SET("PHOTO_SET", "照片集", ProductCategory.PHOTO), + PHOTO_LOG("PHOTO_LOG", "pLog图", ProductCategory.PHOTO), + + // 视频类(素材视频) + RECORDING_SET("RECORDING_SET", "录像集", ProductCategory.VIDEO), + + // 其他类(打印类等) + PHOTO_PRINT("PHOTO_PRINT", "照片打印", ProductCategory.PRINT), + PHOTO_PRINT_MU("PHOTO_PRINT_MU", "手机照片打印", ProductCategory.PRINT), + PHOTO_PRINT_FX("PHOTO_PRINT_FX", "特效照片打印", ProductCategory.PRINT), + MACHINE_PRINT("MACHINE_PRINT", "一体机打印", ProductCategory.PRINT); + private final String code; private final String description; - + private final ProductCategory category; + /** * 根据代码获取枚举 */ @@ -34,4 +43,18 @@ public enum ProductType { } throw new IllegalArgumentException("Unknown product type code: " + code); } + + /** + * 获取分类代码 + */ + public String getCategoryCode() { + return category.getCode(); + } + + /** + * 获取分类描述 + */ + public String getCategoryDescription() { + return category.getDescription(); + } } \ No newline at end of file From 6dd08ac4e77358c3b03c3511d2df23c1f002e55f Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 27 Nov 2025 20:52:32 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(product):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=95=86=E5=93=81=E7=B1=BB=E5=9E=8B=E8=83=BD=E5=8A=9B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增商品类型能力配置的增删改查接口 - 实现分页查询、分类查询、状态筛选等功能 - 支持批量初始化默认配置和缓存刷新 - 提供定价模式、重复检查策略等枚举选项接口 - 实现完整的参数校验和业务逻辑处理 - 添加详细的日志记录和异常处理机制 --- .../ProductTypeCapabilityController.java | 229 +++++++++++ .../basic/product/dto/EnumOptionResponse.java | 25 ++ .../dto/ProductTypeCapabilityRequest.java | 91 +++++ .../dto/ProductTypeCapabilityResponse.java | 85 +++++ ...roductTypeCapabilityManagementService.java | 99 +++++ ...ctTypeCapabilityManagementServiceImpl.java | 355 ++++++++++++++++++ 6 files changed, 884 insertions(+) create mode 100644 src/main/java/com/ycwl/basic/product/controller/ProductTypeCapabilityController.java create mode 100644 src/main/java/com/ycwl/basic/product/dto/EnumOptionResponse.java create mode 100644 src/main/java/com/ycwl/basic/product/dto/ProductTypeCapabilityRequest.java create mode 100644 src/main/java/com/ycwl/basic/product/dto/ProductTypeCapabilityResponse.java create mode 100644 src/main/java/com/ycwl/basic/product/service/IProductTypeCapabilityManagementService.java create mode 100644 src/main/java/com/ycwl/basic/product/service/impl/ProductTypeCapabilityManagementServiceImpl.java diff --git a/src/main/java/com/ycwl/basic/product/controller/ProductTypeCapabilityController.java b/src/main/java/com/ycwl/basic/product/controller/ProductTypeCapabilityController.java new file mode 100644 index 00000000..e5ba3efb --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/controller/ProductTypeCapabilityController.java @@ -0,0 +1,229 @@ +package com.ycwl.basic.product.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.pricing.enums.ProductCategory; +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import com.ycwl.basic.product.capability.PricingMode; +import com.ycwl.basic.product.capability.ProductTypeCapability; +import com.ycwl.basic.product.dto.EnumOptionResponse; +import com.ycwl.basic.product.dto.ProductTypeCapabilityRequest; +import com.ycwl.basic.product.dto.ProductTypeCapabilityResponse; +import com.ycwl.basic.product.service.IProductTypeCapabilityManagementService; +import com.ycwl.basic.product.service.IProductTypeCapabilityService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 商品类型能力配置管理控制器 + * 提供管理端的配置管理功能 + */ +@Slf4j +@RestController +@RequestMapping("/api/product/admin/capability") +@RequiredArgsConstructor +public class ProductTypeCapabilityController { + + private final IProductTypeCapabilityManagementService managementService; + private final IProductTypeCapabilityService capabilityService; + + /** + * 分页查询商品类型能力配置 + */ + @GetMapping("/page") + public ApiResponse> queryByPage( + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(required = false) String productType, + @RequestParam(required = false) String category, + @RequestParam(required = false) Boolean isActive) { + + Page page = managementService.queryByPage( + pageNum, pageSize, productType, category, isActive); + + // 转换为响应DTO + Page responsePage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + List responseList = page.getRecords().stream() + .map(ProductTypeCapabilityResponse::fromEntity) + .collect(Collectors.toList()); + responsePage.setRecords(responseList); + + return ApiResponse.success(responsePage); + } + + /** + * 查询所有商品类型能力配置 + */ + @GetMapping("/list") + public ApiResponse> queryAll( + @RequestParam(defaultValue = "false") boolean includeInactive) { + + List capabilities = managementService.queryAll(includeInactive); + List responseList = capabilities.stream() + .map(ProductTypeCapabilityResponse::fromEntity) + .collect(Collectors.toList()); + + return ApiResponse.success(responseList); + } + + /** + * 根据分类查询商品类型能力配置 + */ + @GetMapping("/category/{category}") + public ApiResponse> queryByCategory( + @PathVariable String category) { + + List capabilities = managementService.queryByCategory(category); + List responseList = capabilities.stream() + .map(ProductTypeCapabilityResponse::fromEntity) + .collect(Collectors.toList()); + + return ApiResponse.success(responseList); + } + + /** + * 根据ID查询配置详情 + */ + @GetMapping("/{id}") + public ApiResponse getById( + @PathVariable Long id) { + + ProductTypeCapability capability = managementService.getById(id); + return ApiResponse.success(ProductTypeCapabilityResponse.fromEntity(capability)); + } + + /** + * 根据商品类型代码查询配置详情 + */ + @GetMapping("/product-type/{productType}") + public ApiResponse getByProductType( + @PathVariable String productType) { + + ProductTypeCapability capability = managementService.getByProductType(productType); + return ApiResponse.success(ProductTypeCapabilityResponse.fromEntity(capability)); + } + + /** + * 创建商品类型能力配置 + */ + @PostMapping + public ApiResponse create( + @RequestBody ProductTypeCapabilityRequest request) { + + ProductTypeCapability capability = managementService.create(request); + return ApiResponse.success(ProductTypeCapabilityResponse.fromEntity(capability)); + } + + /** + * 更新商品类型能力配置 + */ + @PutMapping("/{id}") + public ApiResponse update( + @PathVariable Long id, + @RequestBody ProductTypeCapabilityRequest request) { + + ProductTypeCapability capability = managementService.update(id, request); + return ApiResponse.success(ProductTypeCapabilityResponse.fromEntity(capability)); + } + + /** + * 删除商品类型能力配置 + */ + @DeleteMapping("/{id}") + public ApiResponse delete( + @PathVariable Long id) { + + managementService.delete(id); + return ApiResponse.success(null); + } + + /** + * 启用/禁用商品类型能力配置 + */ + @PutMapping("/{id}/status") + public ApiResponse updateStatus( + @PathVariable Long id, + @RequestParam Boolean isActive) { + + managementService.updateStatus(id, isActive); + return ApiResponse.success(null); + } + + /** + * 批量初始化商品类型能力配置 + * 为所有已定义的商品类型创建默认配置 + */ + @PostMapping("/init-defaults") + public ApiResponse initializeDefaultCapabilities() { + + int count = managementService.initializeDefaultCapabilities(); + return ApiResponse.success(count); + } + + /** + * 刷新所有缓存 + */ + @PostMapping("/refresh-cache") + public ApiResponse refreshCache() { + + capabilityService.refreshCache(); + return ApiResponse.success(null); + } + + /** + * 刷新指定商品类型的缓存 + */ + @PostMapping("/refresh-cache/{productType}") + public ApiResponse refreshCacheByProductType( + @PathVariable String productType) { + + capabilityService.refreshCache(productType); + return ApiResponse.success(null); + } + + // ========== 枚举选项接口 ========== + + /** + * 获取定价模式枚举选项 + */ + @GetMapping("/enums/pricing-modes") + public ApiResponse> getPricingModes() { + + List options = Arrays.stream(PricingMode.values()) + .map(mode -> new EnumOptionResponse(mode.getCode(), mode.getDescription())) + .collect(Collectors.toList()); + + return ApiResponse.success(options); + } + + /** + * 获取重复检查策略枚举选项 + */ + @GetMapping("/enums/duplicate-check-strategies") + public ApiResponse> getDuplicateCheckStrategies() { + + List options = Arrays.stream(DuplicateCheckStrategy.values()) + .map(strategy -> new EnumOptionResponse(strategy.getCode(), strategy.getDescription())) + .collect(Collectors.toList()); + + return ApiResponse.success(options); + } + + /** + * 获取商品分类枚举选项 + */ + @GetMapping("/enums/categories") + public ApiResponse> getCategories() { + + List options = Arrays.stream(ProductCategory.values()) + .map(category -> new EnumOptionResponse(category.getCode(), category.getDescription())) + .collect(Collectors.toList()); + + return ApiResponse.success(options); + } +} diff --git a/src/main/java/com/ycwl/basic/product/dto/EnumOptionResponse.java b/src/main/java/com/ycwl/basic/product/dto/EnumOptionResponse.java new file mode 100644 index 00000000..3a949bc7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/dto/EnumOptionResponse.java @@ -0,0 +1,25 @@ +package com.ycwl.basic.product.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 枚举选项响应DTO + * 用于前端下拉框等场景 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EnumOptionResponse { + + /** + * 枚举代码 + */ + private String code; + + /** + * 枚举描述 + */ + private String description; +} diff --git a/src/main/java/com/ycwl/basic/product/dto/ProductTypeCapabilityRequest.java b/src/main/java/com/ycwl/basic/product/dto/ProductTypeCapabilityRequest.java new file mode 100644 index 00000000..3f9a7c94 --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/dto/ProductTypeCapabilityRequest.java @@ -0,0 +1,91 @@ +package com.ycwl.basic.product.dto; + +import lombok.Data; + +import java.util.Map; + +/** + * 商品类型能力配置请求DTO + */ +@Data +public class ProductTypeCapabilityRequest { + + /** + * 商品类型代码(唯一) + * 如:VLOG_VIDEO, PHOTO_PRINT 等 + */ + private String productType; + + /** + * 显示名称 + */ + private String displayName; + + /** + * 商品分类 + * VLOG, PHOTO, VIDEO, PRINT, OTHER + */ + private String category; + + // ========== 定价相关 ========== + + /** + * 定价模式 + * FIXED: 固定价格 + * QUANTITY_BASED: 基于数量 + * TIERED: 分层定价 + */ + private String pricingMode; + + /** + * 是否支持阶梯定价 + */ + private Boolean supportsTierPricing; + + // ========== 购买限制 ========== + + /** + * 是否允许重复购买 + */ + private Boolean allowDuplicatePurchase; + + /** + * 重复购买检查策略 + * NO_CHECK, CHECK_BY_VIDEO_ID, CHECK_BY_SET_ID, CUSTOM + */ + private String duplicateCheckStrategy; + + // ========== 优惠能力 ========== + + /** + * 是否可使用优惠券 + */ + private Boolean canUseCoupon; + + /** + * 是否可使用券码 + */ + private Boolean canUseVoucher; + + /** + * 是否可使用一口价优惠 + */ + private Boolean canUseOnePrice; + + /** + * 是否可参与打包优惠 + */ + private Boolean canUseBundle; + + // ========== 扩展属性 ========== + + /** + * 扩展属性(JSON 格式) + */ + private Map metadata; + + /** + * 是否启用 + */ + private Boolean isActive; +} diff --git a/src/main/java/com/ycwl/basic/product/dto/ProductTypeCapabilityResponse.java b/src/main/java/com/ycwl/basic/product/dto/ProductTypeCapabilityResponse.java new file mode 100644 index 00000000..616d44c7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/dto/ProductTypeCapabilityResponse.java @@ -0,0 +1,85 @@ +package com.ycwl.basic.product.dto; + +import com.ycwl.basic.product.capability.ProductTypeCapability; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 商品类型能力配置响应DTO + */ +@Data +public class ProductTypeCapabilityResponse { + + private Long id; + private String productType; + private String displayName; + private String category; + private String categoryDescription; + + // 定价相关 + private String pricingMode; + private String pricingModeDescription; + private Boolean supportsTierPricing; + + // 购买限制 + private Boolean allowDuplicatePurchase; + private String duplicateCheckStrategy; + private String duplicateCheckStrategyDescription; + + // 优惠能力 + private Boolean canUseCoupon; + private Boolean canUseVoucher; + private Boolean canUseOnePrice; + private Boolean canUseBundle; + + // 扩展属性 + private Map metadata; + private Boolean isActive; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + /** + * 从实体转换为响应DTO + */ + public static ProductTypeCapabilityResponse fromEntity(ProductTypeCapability entity) { + if (entity == null) { + return null; + } + + ProductTypeCapabilityResponse response = new ProductTypeCapabilityResponse(); + response.setId(entity.getId()); + response.setProductType(entity.getProductType()); + response.setDisplayName(entity.getDisplayName()); + response.setCategory(entity.getCategory()); + + // 定价相关 + response.setPricingMode(entity.getPricingMode()); + if (entity.getPricingModeEnum() != null) { + response.setPricingModeDescription(entity.getPricingModeEnum().getDescription()); + } + response.setSupportsTierPricing(entity.getSupportsTierPricing()); + + // 购买限制 + response.setAllowDuplicatePurchase(entity.getAllowDuplicatePurchase()); + response.setDuplicateCheckStrategy(entity.getDuplicateCheckStrategy()); + if (entity.getDuplicateCheckStrategyEnum() != null) { + response.setDuplicateCheckStrategyDescription(entity.getDuplicateCheckStrategyEnum().getDescription()); + } + + // 优惠能力 + response.setCanUseCoupon(entity.getCanUseCoupon()); + response.setCanUseVoucher(entity.getCanUseVoucher()); + response.setCanUseOnePrice(entity.getCanUseOnePrice()); + response.setCanUseBundle(entity.getCanUseBundle()); + + // 扩展属性 + response.setMetadata(entity.getMetadata()); + response.setIsActive(entity.getIsActive()); + response.setCreatedAt(entity.getCreatedAt()); + response.setUpdatedAt(entity.getUpdatedAt()); + + return response; + } +} diff --git a/src/main/java/com/ycwl/basic/product/service/IProductTypeCapabilityManagementService.java b/src/main/java/com/ycwl/basic/product/service/IProductTypeCapabilityManagementService.java new file mode 100644 index 00000000..a5905622 --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/service/IProductTypeCapabilityManagementService.java @@ -0,0 +1,99 @@ +package com.ycwl.basic.product.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.product.capability.ProductTypeCapability; +import com.ycwl.basic.product.dto.ProductTypeCapabilityRequest; + +import java.util.List; + +/** + * 商品类型能力配置管理服务接口 + * 用于管理端的配置管理功能 + */ +public interface IProductTypeCapabilityManagementService { + + /** + * 分页查询商品类型能力配置 + * + * @param pageNum 页码 + * @param pageSize 每页大小 + * @param productType 商品类型代码(可选,支持模糊查询) + * @param category 商品分类(可选) + * @param isActive 是否启用(可选) + * @return 分页结果 + */ + Page queryByPage(int pageNum, int pageSize, + String productType, String category, Boolean isActive); + + /** + * 查询所有商品类型能力配置 + * + * @param includeInactive 是否包含禁用的配置 + * @return 配置列表 + */ + List queryAll(boolean includeInactive); + + /** + * 根据分类查询商品类型能力配置 + * + * @param category 商品分类 + * @return 配置列表 + */ + List queryByCategory(String category); + + /** + * 根据ID查询配置详情 + * + * @param id 配置ID + * @return 配置详情 + */ + ProductTypeCapability getById(Long id); + + /** + * 根据商品类型代码查询配置详情 + * + * @param productType 商品类型代码 + * @return 配置详情 + */ + ProductTypeCapability getByProductType(String productType); + + /** + * 创建商品类型能力配置 + * + * @param request 请求参数 + * @return 创建的配置 + */ + ProductTypeCapability create(ProductTypeCapabilityRequest request); + + /** + * 更新商品类型能力配置 + * + * @param id 配置ID + * @param request 请求参数 + * @return 更新后的配置 + */ + ProductTypeCapability update(Long id, ProductTypeCapabilityRequest request); + + /** + * 删除商品类型能力配置 + * + * @param id 配置ID + */ + void delete(Long id); + + /** + * 启用/禁用商品类型能力配置 + * + * @param id 配置ID + * @param isActive 是否启用 + */ + void updateStatus(Long id, Boolean isActive); + + /** + * 批量初始化商品类型能力配置 + * 为所有已定义的商品类型创建默认配置 + * + * @return 初始化的配置数量 + */ + int initializeDefaultCapabilities(); +} diff --git a/src/main/java/com/ycwl/basic/product/service/impl/ProductTypeCapabilityManagementServiceImpl.java b/src/main/java/com/ycwl/basic/product/service/impl/ProductTypeCapabilityManagementServiceImpl.java new file mode 100644 index 00000000..57fd5fec --- /dev/null +++ b/src/main/java/com/ycwl/basic/product/service/impl/ProductTypeCapabilityManagementServiceImpl.java @@ -0,0 +1,355 @@ +package com.ycwl.basic.product.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.pricing.enums.ProductCategory; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.product.capability.DuplicateCheckStrategy; +import com.ycwl.basic.product.capability.PricingMode; +import com.ycwl.basic.product.capability.ProductTypeCapability; +import com.ycwl.basic.product.dto.ProductTypeCapabilityRequest; +import com.ycwl.basic.product.mapper.ProductTypeCapabilityMapper; +import com.ycwl.basic.product.service.IProductTypeCapabilityManagementService; +import com.ycwl.basic.product.service.IProductTypeCapabilityService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 商品类型能力配置管理服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ProductTypeCapabilityManagementServiceImpl implements IProductTypeCapabilityManagementService { + + private final ProductTypeCapabilityMapper mapper; + private final IProductTypeCapabilityService capabilityService; + + @Override + public Page queryByPage(int pageNum, int pageSize, + String productType, String category, Boolean isActive) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + if (productType != null && !productType.trim().isEmpty()) { + queryWrapper.like(ProductTypeCapability::getProductType, productType); + } + + if (category != null && !category.trim().isEmpty()) { + queryWrapper.eq(ProductTypeCapability::getCategory, category); + } + + if (isActive != null) { + queryWrapper.eq(ProductTypeCapability::getIsActive, isActive); + } + + queryWrapper.orderByDesc(ProductTypeCapability::getCreatedAt); + + return mapper.selectPage(page, queryWrapper); + } + + @Override + public List queryAll(boolean includeInactive) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + if (!includeInactive) { + queryWrapper.eq(ProductTypeCapability::getIsActive, true); + } + + queryWrapper.orderBy(true, true, ProductTypeCapability::getCategory, ProductTypeCapability::getProductType); + + return mapper.selectList(queryWrapper); + } + + @Override + public List queryByCategory(String category) { + if (category == null || category.trim().isEmpty()) { + return new ArrayList<>(); + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ProductTypeCapability::getCategory, category); + queryWrapper.eq(ProductTypeCapability::getIsActive, true); + queryWrapper.orderByAsc(ProductTypeCapability::getProductType); + + return mapper.selectList(queryWrapper); + } + + @Override + public ProductTypeCapability getById(Long id) { + if (id == null) { + throw new IllegalArgumentException("配置ID不能为空"); + } + + ProductTypeCapability capability = mapper.selectById(id); + if (capability == null) { + throw new IllegalArgumentException("配置不存在: " + id); + } + + return capability; + } + + @Override + public ProductTypeCapability getByProductType(String productType) { + if (productType == null || productType.trim().isEmpty()) { + throw new IllegalArgumentException("商品类型代码不能为空"); + } + + return mapper.selectByProductType(productType); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public ProductTypeCapability create(ProductTypeCapabilityRequest request) { + validateRequest(request, true); + + // 检查商品类型代码是否已存在 + ProductTypeCapability existing = mapper.selectByProductType(request.getProductType()); + if (existing != null) { + throw new IllegalArgumentException("商品类型代码已存在: " + request.getProductType()); + } + + ProductTypeCapability capability = convertToEntity(request); + capability.setCreatedAt(LocalDateTime.now()); + capability.setUpdatedAt(LocalDateTime.now()); + + mapper.insert(capability); + + // 刷新缓存 + capabilityService.refreshCache(capability.getProductType()); + + log.info("创建商品类型能力配置成功: {}", capability.getProductType()); + return capability; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public ProductTypeCapability update(Long id, ProductTypeCapabilityRequest request) { + validateRequest(request, false); + + ProductTypeCapability capability = getById(id); + + // 如果修改了商品类型代码,检查新代码是否已被使用 + if (request.getProductType() != null && + !request.getProductType().equals(capability.getProductType())) { + ProductTypeCapability existing = mapper.selectByProductType(request.getProductType()); + if (existing != null && !existing.getId().equals(id)) { + throw new IllegalArgumentException("商品类型代码已被使用: " + request.getProductType()); + } + } + + String oldProductType = capability.getProductType(); + updateEntityFromRequest(capability, request); + capability.setUpdatedAt(LocalDateTime.now()); + + mapper.updateById(capability); + + // 刷新缓存(如果商品类型代码改变了,需要刷新两个缓存) + capabilityService.refreshCache(capability.getProductType()); + if (!oldProductType.equals(capability.getProductType())) { + capabilityService.refreshCache(oldProductType); + } + + log.info("更新商品类型能力配置成功: {}", capability.getProductType()); + return capability; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void delete(Long id) { + ProductTypeCapability capability = getById(id); + + mapper.deleteById(id); + + // 刷新缓存 + capabilityService.refreshCache(capability.getProductType()); + + log.info("删除商品类型能力配置成功: {}", capability.getProductType()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateStatus(Long id, Boolean isActive) { + if (isActive == null) { + throw new IllegalArgumentException("状态不能为空"); + } + + ProductTypeCapability capability = getById(id); + capability.setIsActive(isActive); + capability.setUpdatedAt(LocalDateTime.now()); + + mapper.updateById(capability); + + // 刷新缓存 + capabilityService.refreshCache(capability.getProductType()); + + log.info("更新商品类型能力配置状态成功: {}, isActive={}", capability.getProductType(), isActive); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public int initializeDefaultCapabilities() { + int count = 0; + + for (ProductType productType : ProductType.values()) { + // 检查是否已存在配置 + ProductTypeCapability existing = mapper.selectByProductType(productType.getCode()); + if (existing != null) { + log.debug("商品类型 {} 已存在配置,跳过初始化", productType.getCode()); + continue; + } + + // 创建默认配置 + ProductTypeCapability capability = createDefaultCapability(productType); + mapper.insert(capability); + count++; + + log.info("初始化商品类型能力配置: {}", productType.getCode()); + } + + // 刷新所有缓存 + capabilityService.refreshCache(); + + log.info("商品类型能力配置批量初始化完成,共初始化 {} 个配置", count); + return count; + } + + /** + * 创建默认配置 + */ + private ProductTypeCapability createDefaultCapability(ProductType productType) { + ProductTypeCapability capability = new ProductTypeCapability(); + capability.setProductType(productType.getCode()); + capability.setDisplayName(productType.getDescription()); + capability.setCategory(productType.getCategoryCode()); + + // 根据分类设置默认的定价模式 + if (ProductCategory.PRINT == productType.getCategory()) { + capability.setPricingMode(PricingMode.QUANTITY_BASED.getCode()); + capability.setSupportsTierPricing(true); + capability.setAllowDuplicatePurchase(true); + capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.NO_CHECK.getCode()); + } else { + capability.setPricingMode(PricingMode.FIXED.getCode()); + capability.setSupportsTierPricing(false); + capability.setAllowDuplicatePurchase(false); + capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.CHECK_BY_SET_ID.getCode()); + } + + // 优惠能力默认全部开启 + capability.setCanUseCoupon(true); + capability.setCanUseVoucher(true); + capability.setCanUseOnePrice(true); + capability.setCanUseBundle(true); + + capability.setIsActive(true); + capability.setCreatedAt(LocalDateTime.now()); + capability.setUpdatedAt(LocalDateTime.now()); + + return capability; + } + + /** + * 验证请求参数 + */ + private void validateRequest(ProductTypeCapabilityRequest request, boolean isCreate) { + if (request == null) { + throw new IllegalArgumentException("请求参数不能为空"); + } + + if (isCreate || request.getProductType() != null) { + if (request.getProductType() == null || request.getProductType().trim().isEmpty()) { + throw new IllegalArgumentException("商品类型代码不能为空"); + } + } + + if (request.getDisplayName() != null && request.getDisplayName().trim().isEmpty()) { + throw new IllegalArgumentException("显示名称不能为空字符串"); + } + + // 验证枚举值有效性 + if (request.getPricingMode() != null) { + try { + PricingMode.fromCode(request.getPricingMode()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效的定价模式: " + request.getPricingMode()); + } + } + + if (request.getDuplicateCheckStrategy() != null) { + try { + DuplicateCheckStrategy.fromCode(request.getDuplicateCheckStrategy()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效的重复检查策略: " + request.getDuplicateCheckStrategy()); + } + } + + if (request.getCategory() != null) { + try { + ProductCategory.fromCode(request.getCategory()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效的商品分类: " + request.getCategory()); + } + } + } + + /** + * 将请求转换为实体 + */ + private ProductTypeCapability convertToEntity(ProductTypeCapabilityRequest request) { + ProductTypeCapability capability = new ProductTypeCapability(); + updateEntityFromRequest(capability, request); + return capability; + } + + /** + * 从请求更新实体 + */ + private void updateEntityFromRequest(ProductTypeCapability capability, ProductTypeCapabilityRequest request) { + if (request.getProductType() != null) { + capability.setProductType(request.getProductType()); + } + if (request.getDisplayName() != null) { + capability.setDisplayName(request.getDisplayName()); + } + if (request.getCategory() != null) { + capability.setCategory(request.getCategory()); + } + if (request.getPricingMode() != null) { + capability.setPricingMode(request.getPricingMode()); + } + if (request.getSupportsTierPricing() != null) { + capability.setSupportsTierPricing(request.getSupportsTierPricing()); + } + if (request.getAllowDuplicatePurchase() != null) { + capability.setAllowDuplicatePurchase(request.getAllowDuplicatePurchase()); + } + if (request.getDuplicateCheckStrategy() != null) { + capability.setDuplicateCheckStrategy(request.getDuplicateCheckStrategy()); + } + if (request.getCanUseCoupon() != null) { + capability.setCanUseCoupon(request.getCanUseCoupon()); + } + if (request.getCanUseVoucher() != null) { + capability.setCanUseVoucher(request.getCanUseVoucher()); + } + if (request.getCanUseOnePrice() != null) { + capability.setCanUseOnePrice(request.getCanUseOnePrice()); + } + if (request.getCanUseBundle() != null) { + capability.setCanUseBundle(request.getCanUseBundle()); + } + if (request.getMetadata() != null) { + capability.setMetadata(request.getMetadata()); + } + if (request.getIsActive() != null) { + capability.setIsActive(request.getIsActive()); + } + } +}