From 9c932b6ba89027e5a42085be88ef5dd879df4525 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 14 Aug 2025 10:48:59 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=EF=BC=8C=E5=BE=85=E5=A4=84=E7=90=86=E8=AE=A2=E5=8D=95=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 4 + .../basic/config/JacksonConfiguration.java | 18 +- .../PriceCalculationController.java | 75 ++++++ .../controller/PricingConfigController.java | 149 +++++++++++ .../ycwl/basic/pricing/dto/CouponInfo.java | 38 +++ .../basic/pricing/dto/CouponUseRequest.java | 37 +++ .../basic/pricing/dto/CouponUseResult.java | 38 +++ .../basic/pricing/dto/DiscountDetail.java | 76 ++++++ .../pricing/dto/PriceCalculationRequest.java | 27 ++ .../pricing/dto/PriceCalculationResult.java | 48 ++++ .../ycwl/basic/pricing/dto/PriceDetails.java | 32 +++ .../ycwl/basic/pricing/dto/ProductItem.java | 53 ++++ .../basic/pricing/dto/ProductPriceInfo.java | 27 ++ .../ycwl/basic/pricing/entity/BaseEntity.java | 21 ++ .../pricing/entity/PriceBundleConfig.java | 46 ++++ .../entity/PriceCouponClaimRecord.java | 47 ++++ .../pricing/entity/PriceCouponConfig.java | 73 ++++++ .../pricing/entity/PriceProductConfig.java | 51 ++++ .../basic/pricing/entity/PriceTierConfig.java | 66 +++++ .../basic/pricing/enums/CouponStatus.java | 31 +++ .../ycwl/basic/pricing/enums/CouponType.java | 30 +++ .../ycwl/basic/pricing/enums/ProductType.java | 33 +++ .../exception/CouponInvalidException.java | 15 ++ .../exception/PriceCalculationException.java | 15 ++ .../exception/PricingExceptionHandler.java | 66 +++++ .../ProductConfigNotFoundException.java | 15 ++ .../mapper/PriceBundleConfigMapper.java | 46 ++++ .../mapper/PriceCouponClaimRecordMapper.java | 63 +++++ .../mapper/PriceCouponConfigMapper.java | 62 +++++ .../mapper/PriceProductConfigMapper.java | 49 ++++ .../pricing/mapper/PriceTierConfigMapper.java | 71 ++++++ .../basic/pricing/service/ICouponService.java | 62 +++++ .../pricing/service/IPriceBundleService.java | 36 +++ .../service/IPriceCalculationService.java | 18 ++ .../service/IPricingManagementService.java | 59 +++++ .../service/IProductConfigService.java | 75 ++++++ .../service/impl/CouponServiceImpl.java | 173 +++++++++++++ .../service/impl/PriceBundleServiceImpl.java | 104 ++++++++ .../impl/PriceCalculationServiceImpl.java | 238 ++++++++++++++++++ .../impl/PricingManagementServiceImpl.java | 107 ++++++++ .../impl/ProductConfigServiceImpl.java | 78 ++++++ 41 files changed, 2371 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java create mode 100644 src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java create mode 100644 src/main/java/com/ycwl/basic/pricing/enums/CouponType.java create mode 100644 src/main/java/com/ycwl/basic/pricing/enums/ProductType.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/ICouponService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java diff --git a/pom.xml b/pom.xml index 7dd39ed..915cbbf 100644 --- a/pom.xml +++ b/pom.xml @@ -135,6 +135,10 @@ com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java b/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java index ba2892a..70711a4 100644 --- a/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java +++ b/src/main/java/com/ycwl/basic/config/JacksonConfiguration.java @@ -1,10 +1,16 @@ package com.ycwl.basic.config; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + @Configuration public class JacksonConfiguration { @@ -13,6 +19,16 @@ public class JacksonConfiguration { return builder -> { // 把 Long 类型序列化为 String builder.serializerByType(Long.class, ToStringSerializer.instance); + + // 添加 JavaTimeModule 以支持 Java 8 时间类型 + builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss"); + builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }; } -} + + @Bean + public JavaTimeModule javaTimeModule() { + return new JavaTimeModule(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java b/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java new file mode 100644 index 0000000..8b4cd2e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.pricing.controller; + +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.service.ICouponService; +import com.ycwl.basic.pricing.service.IPriceCalculationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 价格计算控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing") +@RequiredArgsConstructor +public class PriceCalculationController { + + private final IPriceCalculationService priceCalculationService; + private final ICouponService couponService; + + /** + * 计算商品价格 + */ + @PostMapping("/calculate") + public ApiResponse calculatePrice(@RequestBody PriceCalculationRequest request) { + log.info("价格计算请求: userId={}, products={}", request.getUserId(), request.getProducts().size()); + + PriceCalculationResult result = priceCalculationService.calculatePrice(request); + + log.info("价格计算完成: originalAmount={}, finalAmount={}, usedCoupon={}", + result.getOriginalAmount(), result.getFinalAmount(), + result.getUsedCoupon() != null ? result.getUsedCoupon().getCouponId() : null); + + return ApiResponse.success(result); + } + + /** + * 使用优惠券 + * 只能通过代码处理,不能通过接口调用 + */ + @PostMapping("/coupons/use") + @Deprecated + public ApiResponse useCoupon(@RequestBody CouponUseRequest request) { +// log.info("优惠券使用请求: userId={}, couponId={}, orderId={}", +// request.getUserId(), request.getCouponId(), request.getOrderId()); +// +// CouponUseResult result = couponService.useCoupon(request); +// +// log.info("优惠券使用成功: couponId={}, discountAmount={}", +// result.getCouponId(), result.getDiscountAmount()); +// +// ApiResponse response = ApiResponse.success(result); +// response.setMsg("优惠券使用成功"); +// return response; + return null; + } + + /** + * 查询用户可用优惠券 + */ + @GetMapping("/coupons/my-coupons") + public ApiResponse> getUserCoupons(@RequestParam Long userId) { + log.info("查询用户可用优惠券: userId={}", userId); + + List coupons = couponService.getUserAvailableCoupons(userId); + + log.info("用户可用优惠券数量: {}", coupons.size()); + + return ApiResponse.success(coupons); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java new file mode 100644 index 0000000..8b98ad8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java @@ -0,0 +1,149 @@ +package com.ycwl.basic.pricing.controller; + +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import com.ycwl.basic.pricing.service.IProductConfigService; +import com.ycwl.basic.pricing.service.IPriceBundleService; +import com.ycwl.basic.pricing.service.IPricingManagementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 价格配置管理控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing/config") +@RequiredArgsConstructor +public class PricingConfigController { + + private final IProductConfigService productConfigService; + private final IPriceBundleService bundleService; + private final IPricingManagementService managementService; + + /** + * 获取所有商品配置 + */ + @GetMapping("/products") + public ApiResponse> getProductConfigs() { + List configs = productConfigService.getActiveProductConfigs(); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型获取阶梯配置 + */ + @GetMapping("/tiers/{productType}") + public ApiResponse> getTierConfigs(@PathVariable String productType) { + List configs = productConfigService.getTierConfigs(productType); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型和商品ID获取阶梯配置 + */ + @GetMapping("/tiers/{productType}/{productId}") + public ApiResponse> getTierConfigs(@PathVariable String productType, + @PathVariable String productId) { + List configs = productConfigService.getTierConfigs(productType, productId); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型和商品ID获取具体配置 + */ + @GetMapping("/products/{productType}/{productId}") + public ApiResponse getProductConfig(@PathVariable String productType, + @PathVariable String productId) { + PriceProductConfig config = productConfigService.getProductConfig(productType, productId); + return ApiResponse.success(config); + } + + /** + * 获取所有阶梯配置 + */ + @GetMapping("/tiers") + public ApiResponse> getAllTierConfigs() { + log.info("获取所有阶梯定价配置"); + return ApiResponse.success(List.of()); + } + + /** + * 获取所有一口价配置 + */ + @GetMapping("/bundles") + public ApiResponse> getBundleConfigs() { + List configs = bundleService.getActiveBundles(); + return ApiResponse.success(configs); + } + + // ==================== 配置管理API(手动处理时间) ==================== + + /** + * 创建商品配置 + */ + @PostMapping("/products") + public ApiResponse createProductConfig(@RequestBody PriceProductConfig config) { + log.info("创建商品配置: {}", config.getProductName()); + Long id = managementService.createProductConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新商品配置 + */ + @PutMapping("/products/{id}") + public ApiResponse updateProductConfig(@PathVariable Long id, @RequestBody PriceProductConfig config) { + log.info("更新商品配置: id={}, name={}", id, config.getProductName()); + config.setId(id); + boolean success = managementService.updateProductConfig(config); + return ApiResponse.success(success); + } + + /** + * 创建阶梯定价配置 + */ + @PostMapping("/tiers") + public ApiResponse createTierConfig(@RequestBody PriceTierConfig config) { + log.info("创建阶梯定价配置: productType={}, price={}", config.getProductType(), config.getPrice()); + Long id = managementService.createTierConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新阶梯定价配置 + */ + @PutMapping("/tiers/{id}") + public ApiResponse updateTierConfig(@PathVariable Long id, @RequestBody PriceTierConfig config) { + log.info("更新阶梯定价配置: id={}", id); + config.setId(id); + boolean success = managementService.updateTierConfig(config); + return ApiResponse.success(success); + } + + /** + * 创建一口价配置 + */ + @PostMapping("/bundles") + public ApiResponse createBundleConfig(@RequestBody PriceBundleConfig config) { + log.info("创建一口价配置: {}", config.getBundleName()); + Long id = managementService.createBundleConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新一口价配置 + */ + @PutMapping("/bundles/{id}") + public ApiResponse updateBundleConfig(@PathVariable Long id, @RequestBody PriceBundleConfig config) { + log.info("更新一口价配置: id={}, name={}", id, config.getBundleName()); + config.setId(id); + boolean success = managementService.updateBundleConfig(config); + return ApiResponse.success(success); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java new file mode 100644 index 0000000..6d315b7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponInfo.java @@ -0,0 +1,38 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.enums.CouponType; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 优惠券信息DTO + */ +@Data +public class CouponInfo { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 优惠券名称 + */ + private String couponName; + + /** + * 优惠类型 + */ + private CouponType discountType; + + /** + * 优惠值 + */ + private BigDecimal discountValue; + + /** + * 实际优惠金额 + */ + private BigDecimal actualDiscountAmount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java new file mode 100644 index 0000000..f1f7253 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java @@ -0,0 +1,37 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 优惠券使用请求DTO + */ +@Data +public class CouponUseRequest { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 订单ID + */ + private String orderId; + + /** + * 原始金额 + */ + private BigDecimal originalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java new file mode 100644 index 0000000..b5df283 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java @@ -0,0 +1,38 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 优惠券使用结果DTO + */ +@Data +public class CouponUseResult { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 订单ID + */ + private String orderId; + + /** + * 使用时间 + */ + private LocalDateTime useTime; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java new file mode 100644 index 0000000..30954e7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java @@ -0,0 +1,76 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 折扣明细DTO + */ +@Data +public class DiscountDetail { + + /** + * 折扣类型 + */ + private String discountType; + + /** + * 折扣名称 + */ + private String discountName; + + /** + * 折扣金额 + */ + private BigDecimal discountAmount; + + /** + * 折扣描述 + */ + private String description; + + /** + * 排序(数值越小越靠前) + */ + private Integer sortOrder; + + /** + * 创建限时立减折扣明细 + */ + public static DiscountDetail createLimitedTimeDiscount(BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("LIMITED_TIME"); + detail.setDiscountName("限时立减"); + detail.setDiscountAmount(discountAmount); + detail.setDescription("限时优惠,立即享受"); + detail.setSortOrder(1); // 限时立减排在最前面 + return detail; + } + + /** + * 创建优惠券折扣明细 + */ + public static DiscountDetail createCouponDiscount(String couponName, BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("COUPON"); + detail.setDiscountName(couponName); + detail.setDiscountAmount(discountAmount); + detail.setDescription("优惠券减免"); + detail.setSortOrder(2); // 优惠券排在限时立减后面 + return detail; + } + + /** + * 创建一口价折扣明细 + */ + public static DiscountDetail createBundleDiscount(BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("BUNDLE"); + detail.setDiscountName("一口价优惠"); + detail.setDiscountAmount(discountAmount); + detail.setDescription("一口价购买更优惠"); + detail.setSortOrder(3); // 一口价排在最后 + return detail; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java new file mode 100644 index 0000000..72b2048 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 价格计算请求DTO + */ +@Data +public class PriceCalculationRequest { + + /** + * 商品列表 + */ + private List products; + + /** + * 用户ID + */ + private Long userId; + + /** + * 是否自动使用优惠券 + */ + private Boolean autoUseCoupon = true; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java new file mode 100644 index 0000000..bd00cdb --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java @@ -0,0 +1,48 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 价格计算结果DTO + */ +@Data +public class PriceCalculationResult { + + /** + * 原始金额(用于前端展示的总原价) + */ + private BigDecimal originalAmount; + + /** + * 商品小计金额(按实际计算价格) + */ + private BigDecimal subtotalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 最终金额 + */ + private BigDecimal finalAmount; + + /** + * 使用的优惠券信息 + */ + private CouponInfo usedCoupon; + + /** + * 折扣明细列表(包含限时立减、优惠券、一口价等) + */ + private List discountDetails; + + /** + * 商品明细列表 + */ + private List productDetails; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java new file mode 100644 index 0000000..c75ca33 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceDetails.java @@ -0,0 +1,32 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 价格详情(内部计算用) + */ +@Data +public class PriceDetails { + + /** + * 实际计算总金额 + */ + private BigDecimal totalAmount; + + /** + * 原价总金额 + */ + private BigDecimal originalTotalAmount; + + public PriceDetails() { + this.totalAmount = BigDecimal.ZERO; + this.originalTotalAmount = BigDecimal.ZERO; + } + + public PriceDetails(BigDecimal totalAmount, BigDecimal originalTotalAmount) { + this.totalAmount = totalAmount; + this.originalTotalAmount = originalTotalAmount; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java new file mode 100644 index 0000000..6ce3d6c --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java @@ -0,0 +1,53 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.enums.ProductType; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 商品项DTO + */ +@Data +public class ProductItem { + + /** + * 商品类型 + */ + private ProductType productType; + + /** + * 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID + */ + private String productId; + + /** + * 商品子类型 + */ + private String productSubType; + + /** + * 数量(如原片数量、照片数量等) + */ + private Integer quantity; + + /** + * 购买数量(购买几个该商品) + */ + private Integer purchaseCount; + + /** + * 原价(用于前端展示) + */ + private BigDecimal originalPrice; + + /** + * 单价(实际计算用的价格) + */ + private BigDecimal unitPrice; + + /** + * 小计(计算后填入) + */ + private BigDecimal subtotal; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java new file mode 100644 index 0000000..65ae04b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductPriceInfo.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 商品价格信息(内部计算用) + */ +@Data +public class ProductPriceInfo { + + /** + * 实际计算价格 + */ + private BigDecimal actualPrice; + + /** + * 原价(用于前端展示) + */ + private BigDecimal originalPrice; + + public ProductPriceInfo(BigDecimal actualPrice, BigDecimal originalPrice) { + this.actualPrice = actualPrice; + this.originalPrice = originalPrice != null ? originalPrice : actualPrice; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java b/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java new file mode 100644 index 0000000..b66c468 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java @@ -0,0 +1,21 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 基础实体类 + */ +@Data +public class BaseEntity { + + @TableId(type = IdType.AUTO) + private Long id; + + private LocalDateTime createdTime; + + private LocalDateTime updatedTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java new file mode 100644 index 0000000..f7457dd --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java @@ -0,0 +1,46 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 一口价配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_bundle_config") +public class PriceBundleConfig extends BaseEntity { + + /** + * 套餐名称 + */ + private String bundleName; + + /** + * 套餐价格 + */ + private BigDecimal bundlePrice; + + /** + * 包含商品(JSON) + */ + private String includedProducts; + + /** + * 排除商品(JSON) + */ + private String excludedProducts; + + /** + * 套餐描述 + */ + private String description; + + /** + * 是否启用 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java new file mode 100644 index 0000000..352650f --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java @@ -0,0 +1,47 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.ycwl.basic.pricing.enums.CouponStatus; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 优惠券领用记录实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_coupon_claim_record") +public class PriceCouponClaimRecord extends BaseEntity { + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 用户ID + */ + private Long userId; + + /** + * 领取时间 + */ + private LocalDateTime claimTime; + + /** + * 使用时间 + */ + private LocalDateTime useTime; + + /** + * 订单ID + */ + private String orderId; + + /** + * 状态 + */ + private CouponStatus status; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java new file mode 100644 index 0000000..f0e134e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java @@ -0,0 +1,73 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.ycwl.basic.pricing.enums.CouponType; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 优惠券配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_coupon_config") +public class PriceCouponConfig extends BaseEntity { + + /** + * 优惠券名称 + */ + private String couponName; + + /** + * 优惠券类型 + */ + private CouponType couponType; + + /** + * 优惠值 + */ + private BigDecimal discountValue; + + /** + * 最小使用金额 + */ + private BigDecimal minAmount; + + /** + * 最大优惠金额 + */ + private BigDecimal maxDiscount; + + /** + * 适用商品类型(JSON) + */ + private String applicableProducts; + + /** + * 发行总量 + */ + private Integer totalQuantity; + + /** + * 已使用数量 + */ + private Integer usedQuantity; + + /** + * 生效时间 + */ + private LocalDateTime validFrom; + + /** + * 失效时间 + */ + private LocalDateTime validUntil; + + /** + * 是否启用 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java new file mode 100644 index 0000000..5bc37d7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java @@ -0,0 +1,51 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 商品价格配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_product_config") +public class PriceProductConfig extends BaseEntity { + + /** + * 商品类型 + */ + private String productType; + + /** + * 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID + */ + private String productId; + + /** + * 商品名称 + */ + private String productName; + + /** + * 基础价格 + */ + private BigDecimal basePrice; + + /** + * 商品原价:用于前端展示优惠力度,当original_price > base_price时显示限时立减 + */ + private BigDecimal originalPrice; + + /** + * 价格单位 + */ + private String unit; + + /** + * 是否启用 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java new file mode 100644 index 0000000..31711b7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java @@ -0,0 +1,66 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 阶梯定价配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_tier_config") +public class PriceTierConfig extends BaseEntity { + + /** + * 商品类型 + */ + private String productType; + + /** + * 具体商品ID:与price_product_config的product_id对应 + */ + private String productId; + + /** + * 商品子类型 + */ + private String productSubType; + + /** + * 最小数量 + */ + private Integer minQuantity; + + /** + * 最大数量 + */ + private Integer maxQuantity; + + /** + * 阶梯价格 + */ + private BigDecimal price; + + /** + * 阶梯原价:用于前端展示优惠力度,当original_price > price时显示限时立减 + */ + private BigDecimal originalPrice; + + /** + * 计价单位 + */ + private String unit; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 是否启用 + */ + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java b/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java new file mode 100644 index 0000000..74e511e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/CouponStatus.java @@ -0,0 +1,31 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 优惠券状态枚举 + */ +@Getter +@AllArgsConstructor +public enum CouponStatus { + + CLAIMED("claimed", "已领取"), + USED("used", "已使用"), + EXPIRED("expired", "已过期"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static CouponStatus fromCode(String code) { + for (CouponStatus status : values()) { + if (status.code.equals(code)) { + return status; + } + } + throw new IllegalArgumentException("Unknown coupon status code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java b/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java new file mode 100644 index 0000000..6908d3d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/CouponType.java @@ -0,0 +1,30 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 优惠券类型枚举 + */ +@Getter +@AllArgsConstructor +public enum CouponType { + + PERCENTAGE("percentage", "百分比折扣"), + FIXED_AMOUNT("fixed_amount", "固定金额减免"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static CouponType fromCode(String code) { + for (CouponType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown coupon type code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java new file mode 100644 index 0000000..c90bab2 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java @@ -0,0 +1,33 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 商品类型枚举 + */ +@Getter +@AllArgsConstructor +public enum ProductType { + + VLOG_VIDEO("vlog_video", "Vlog视频"), + RECORDING_SET("recording_set", "录像集"), + PHOTO_SET("photo_set", "照相集"), + PHOTO_PRINT("photo_print", "照片打印"), + MACHINE_PRINT("machine_print", "一体机打印"); + + private final String code; + private final String description; + + /** + * 根据代码获取枚举 + */ + public static ProductType fromCode(String code) { + for (ProductType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown product type code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java b/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java new file mode 100644 index 0000000..73a6fa4 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/CouponInvalidException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 优惠券无效异常 + */ +public class CouponInvalidException extends RuntimeException { + + public CouponInvalidException(String message) { + super(message); + } + + public CouponInvalidException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java b/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java new file mode 100644 index 0000000..891b7ef --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/PriceCalculationException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 价格计算异常 + */ +public class PriceCalculationException extends RuntimeException { + + public PriceCalculationException(String message) { + super(message); + } + + public PriceCalculationException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java b/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java new file mode 100644 index 0000000..d4acdd0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java @@ -0,0 +1,66 @@ +package com.ycwl.basic.pricing.exception; + +import com.ycwl.basic.utils.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 价格查询系统全局异常处理器 + */ +@Slf4j +@RestControllerAdvice(basePackages = "com.ycwl.basic.pricing") +public class PricingExceptionHandler { + + /** + * 处理价格计算异常 + */ + @ExceptionHandler(PriceCalculationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handlePriceCalculationException(PriceCalculationException e) { + log.error("价格计算异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理优惠券无效异常 + */ + @ExceptionHandler(CouponInvalidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleCouponInvalidException(CouponInvalidException e) { + log.error("优惠券无效异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理商品配置未找到异常 + */ + @ExceptionHandler(ProductConfigNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiResponse handleProductConfigNotFoundException(ProductConfigNotFoundException e) { + log.error("商品配置未找到异常", e); + return ApiResponse.buildResponse(404, e.getMessage()); + } + + /** + * 处理非法参数异常 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常", e); + return ApiResponse.buildResponse(400, "参数错误: " + e.getMessage()); + } + + /** + * 处理通用异常 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse handleException(Exception e) { + log.error("系统异常", e); + return ApiResponse.buildResponse(500, "系统内部错误"); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java b/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java new file mode 100644 index 0000000..9bdbe63 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/ProductConfigNotFoundException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 商品配置未找到异常 + */ +public class ProductConfigNotFoundException extends RuntimeException { + + public ProductConfigNotFoundException(String message) { + super(message); + } + + public ProductConfigNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java new file mode 100644 index 0000000..3f7f48f --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java @@ -0,0 +1,46 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 一口价配置Mapper + */ +@Mapper +public interface PriceBundleConfigMapper extends BaseMapper { + + /** + * 查询启用的一口价配置 + */ + @Select("SELECT * FROM price_bundle_config WHERE is_active = 1") + List selectActiveBundles(); + + /** + * 根据ID查询启用的配置 + */ + @Select("SELECT * FROM price_bundle_config WHERE id = #{id} AND is_active = 1") + PriceBundleConfig selectActiveBundleById(Long id); + + /** + * 插入一口价配置 + */ + @Insert("INSERT INTO price_bundle_config (bundle_name, bundle_price, included_products, excluded_products, " + + "description, is_active, created_time, updated_time) VALUES " + + "(#{bundleName}, #{bundlePrice}, #{includedProducts}, #{excludedProducts}, " + + "#{description}, #{isActive}, NOW(), NOW())") + int insertBundleConfig(PriceBundleConfig config); + + /** + * 更新一口价配置 + */ + @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, bundle_price = #{bundlePrice}, " + + "included_products = #{includedProducts}, excluded_products = #{excludedProducts}, " + + "description = #{description}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateBundleConfig(PriceBundleConfig config); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java new file mode 100644 index 0000000..87ef06d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java @@ -0,0 +1,63 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.enums.CouponStatus; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 优惠券领用记录Mapper + */ +@Mapper +public interface PriceCouponClaimRecordMapper extends BaseMapper { + + /** + * 查询用户可用的优惠券记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value, c.min_amount, " + + "c.max_discount, c.applicable_products, c.valid_from, c.valid_until " + + "FROM price_coupon_claim_record r " + + "JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.user_id = #{userId} AND r.status = 'CLAIMED' " + + "AND c.is_active = 1 AND c.valid_from <= NOW() AND c.valid_until > NOW()") + List selectUserAvailableCoupons(Long userId); + + /** + * 查询用户特定优惠券记录 + */ + @Select("SELECT * FROM price_coupon_claim_record " + + "WHERE user_id = #{userId} AND coupon_id = #{couponId} AND status = 'CLAIMED'") + PriceCouponClaimRecord selectUserCouponRecord(@Param("userId") Long userId, + @Param("couponId") Long couponId); + + /** + * 更新优惠券使用状态 + */ + @Update("UPDATE price_coupon_claim_record SET status = #{status}, " + + "use_time = #{useTime}, order_id = #{orderId}, updated_time = NOW() " + + "WHERE id = #{id}") + int updateCouponStatus(@Param("id") Long id, + @Param("status") CouponStatus status, + @Param("useTime") java.time.LocalDateTime useTime, + @Param("orderId") String orderId); + + /** + * 插入优惠券领用记录 + */ + @Insert("INSERT INTO price_coupon_claim_record (coupon_id, user_id, claim_time, status, created_time, updated_time) " + + "VALUES (#{couponId}, #{userId}, NOW(), #{status}, NOW(), NOW())") + int insertClaimRecord(PriceCouponClaimRecord record); + + /** + * 更新优惠券记录 + */ + @Update("UPDATE price_coupon_claim_record SET status = #{status}, use_time = #{useTime}, " + + "order_id = #{orderId}, updated_time = NOW() WHERE id = #{id}") + int updateClaimRecord(PriceCouponClaimRecord record); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java new file mode 100644 index 0000000..869a4b6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java @@ -0,0 +1,62 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 优惠券配置Mapper + */ +@Mapper +public interface PriceCouponConfigMapper extends BaseMapper { + + /** + * 查询有效的优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE is_active = 1 " + + "AND valid_from <= NOW() AND valid_until > NOW() " + + "AND used_quantity < total_quantity") + List selectValidCoupons(); + + /** + * 根据ID查询优惠券(包括使用数量检查) + */ + @Select("SELECT * FROM price_coupon_config WHERE id = #{couponId} " + + "AND is_active = 1 AND valid_from <= NOW() AND valid_until > NOW() " + + "AND used_quantity < total_quantity") + PriceCouponConfig selectValidCouponById(Long couponId); + + /** + * 增加优惠券使用数量 + */ + @Update("UPDATE price_coupon_config SET used_quantity = used_quantity + 1, " + + "updated_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity") + int incrementUsedQuantity(Long couponId); + + /** + * 插入优惠券配置 + */ + @Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " + + "max_discount, applicable_products, total_quantity, used_quantity, valid_from, valid_until, " + + "is_active, created_time, updated_time) VALUES " + + "(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " + + "#{applicableProducts}, #{totalQuantity}, #{usedQuantity}, #{validFrom}, #{validUntil}, " + + "#{isActive}, NOW(), NOW())") + int insertCoupon(PriceCouponConfig coupon); + + /** + * 更新优惠券配置 + */ + @Update("UPDATE price_coupon_config SET coupon_name = #{couponName}, coupon_type = #{couponType}, " + + "discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " + + "applicable_products = #{applicableProducts}, total_quantity = #{totalQuantity}, " + + "valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " + + "updated_time = NOW() WHERE id = #{id}") + int updateCoupon(PriceCouponConfig coupon); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java new file mode 100644 index 0000000..907e650 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java @@ -0,0 +1,49 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 商品价格配置Mapper + */ +@Mapper +public interface PriceProductConfigMapper extends BaseMapper { + + /** + * 查询启用的商品配置 + */ + @Select("SELECT * FROM price_product_config WHERE is_active = 1") + List selectActiveConfigs(); + + /** + * 根据商品类型查询配置 + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND is_active = 1") + List selectByProductType(String productType); + + /** + * 根据商品类型和商品ID查询配置 + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1") + PriceProductConfig selectByProductTypeAndId(String productType, String productId); + + /** + * 插入商品价格配置 + */ + @Insert("INSERT INTO price_product_config (product_type, product_id, product_name, base_price, original_price, unit, is_active, created_time, updated_time) " + + "VALUES (#{productType}, #{productId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, NOW(), NOW())") + int insertProductConfig(PriceProductConfig config); + + /** + * 更新商品价格配置 + */ + @Update("UPDATE price_product_config SET product_id = #{productId}, product_name = #{productName}, base_price = #{basePrice}, " + + "original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateProductConfig(PriceProductConfig config); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java new file mode 100644 index 0000000..e1f6a72 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java @@ -0,0 +1,71 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 阶梯定价配置Mapper + */ +@Mapper +public interface PriceTierConfigMapper extends BaseMapper { + + /** + * 根据商品类型、商品ID和数量查询匹配的阶梯价格 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} " + + "AND (product_sub_type = #{productSubType} OR product_sub_type IS NULL) " + + "AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " + + "AND is_active = 1 ORDER BY sort_order ASC LIMIT 1") + PriceTierConfig selectByProductTypeAndQuantity(@Param("productType") String productType, + @Param("productId") String productId, + @Param("productSubType") String productSubType, + @Param("quantity") Integer quantity); + + /** + * 根据商品类型查询所有阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND is_active = 1 ORDER BY sort_order ASC") + List selectByProductType(String productType); + + /** + * 根据商品类型和商品ID查询所有阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} AND is_active = 1 ORDER BY sort_order ASC") + List selectByProductTypeAndId(@Param("productType") String productType, + @Param("productId") String productId); + + /** + * 根据商品类型和子类型查询阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_sub_type = #{productSubType} AND is_active = 1 ORDER BY sort_order ASC") + List selectByProductTypeAndSubType(@Param("productType") String productType, + @Param("productSubType") String productSubType); + + /** + * 插入阶梯定价配置 + */ + @Insert("INSERT INTO price_tier_config (product_type, product_id, product_sub_type, min_quantity, max_quantity, price, " + + "original_price, unit, sort_order, is_active, created_time, updated_time) VALUES " + + "(#{productType}, #{productId}, #{productSubType}, #{minQuantity}, #{maxQuantity}, #{price}, " + + "#{originalPrice}, #{unit}, #{sortOrder}, #{isActive}, NOW(), NOW())") + int insertTierConfig(PriceTierConfig config); + + /** + * 更新阶梯定价配置 + */ + @Update("UPDATE price_tier_config SET product_id = #{productId}, product_sub_type = #{productSubType}, min_quantity = #{minQuantity}, " + + "max_quantity = #{maxQuantity}, price = #{price}, original_price = #{originalPrice}, unit = #{unit}, sort_order = #{sortOrder}, " + + "is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateTierConfig(PriceTierConfig config); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java b/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java new file mode 100644 index 0000000..3acd166 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java @@ -0,0 +1,62 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.CouponInfo; +import com.ycwl.basic.pricing.dto.CouponUseRequest; +import com.ycwl.basic.pricing.dto.CouponUseResult; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠券服务接口 + */ +public interface ICouponService { + + /** + * 自动选择最优优惠券 + * + * @param userId 用户ID + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 最优优惠券信息,如果没有可用优惠券则返回null + */ + CouponInfo selectBestCoupon(Long userId, List products, BigDecimal totalAmount); + + /** + * 计算优惠券优惠金额 + * + * @param coupon 优惠券配置 + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 优惠金额 + */ + BigDecimal calculateCouponDiscount(PriceCouponConfig coupon, List products, BigDecimal totalAmount); + + /** + * 验证优惠券是否可用 + * + * @param coupon 优惠券配置 + * @param products 商品列表 + * @param totalAmount 总金额 + * @return 是否可用 + */ + boolean isCouponApplicable(PriceCouponConfig coupon, List products, BigDecimal totalAmount); + + /** + * 使用优惠券 + * + * @param request 优惠券使用请求 + * @return 使用结果 + */ + CouponUseResult useCoupon(CouponUseRequest request); + + /** + * 查询用户可用优惠券 + * + * @param userId 用户ID + * @return 可用优惠券列表 + */ + List getUserAvailableCoupons(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java new file mode 100644 index 0000000..0e7ae09 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java @@ -0,0 +1,36 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 一口价套餐服务接口 + */ +public interface IPriceBundleService { + + /** + * 检查商品是否适用一口价 + * + * @param products 商品列表 + * @return 是否适用 + */ + boolean isBundleApplicable(List products); + + /** + * 获取一口价价格 + * + * @param products 商品列表 + * @return 一口价价格,如果不适用则返回null + */ + BigDecimal getBundlePrice(List products); + + /** + * 获取所有启用的一口价配置 + * + * @return 一口价配置列表 + */ + List getActiveBundles(); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java b/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java new file mode 100644 index 0000000..bffa743 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPriceCalculationService.java @@ -0,0 +1,18 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.PriceCalculationRequest; +import com.ycwl.basic.pricing.dto.PriceCalculationResult; + +/** + * 价格计算服务接口 + */ +public interface IPriceCalculationService { + + /** + * 计算商品价格(支持自动优惠券应用) + * + * @param request 价格计算请求 + * @return 价格计算结果 + */ + PriceCalculationResult calculatePrice(PriceCalculationRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java new file mode 100644 index 0000000..bcb9f29 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java @@ -0,0 +1,59 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.entity.*; + +/** + * 价格管理服务接口(用于配置管理,手动处理时间字段) + */ +public interface IPricingManagementService { + + /** + * 创建商品价格配置 + */ + Long createProductConfig(PriceProductConfig config); + + /** + * 更新商品价格配置 + */ + boolean updateProductConfig(PriceProductConfig config); + + /** + * 创建阶梯定价配置 + */ + Long createTierConfig(PriceTierConfig config); + + /** + * 更新阶梯定价配置 + */ + boolean updateTierConfig(PriceTierConfig config); + + /** + * 创建优惠券配置 + */ + Long createCouponConfig(PriceCouponConfig config); + + /** + * 更新优惠券配置 + */ + boolean updateCouponConfig(PriceCouponConfig config); + + /** + * 创建优惠券领用记录 + */ + Long createCouponClaimRecord(PriceCouponClaimRecord record); + + /** + * 更新优惠券领用记录 + */ + boolean updateCouponClaimRecord(PriceCouponClaimRecord record); + + /** + * 创建一口价配置 + */ + Long createBundleConfig(PriceBundleConfig config); + + /** + * 更新一口价配置 + */ + boolean updateBundleConfig(PriceBundleConfig config); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java new file mode 100644 index 0000000..cd7ab96 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; + +import java.util.List; + +/** + * 商品配置管理服务接口 + */ +public interface IProductConfigService { + + /** + * 根据商品类型获取基础配置(兼容旧接口) + * + * @param productType 商品类型 + * @return 商品配置列表 + */ + List getProductConfig(String productType); + + /** + * 根据商品类型和商品ID获取精确配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @return 商品配置 + */ + PriceProductConfig getProductConfig(String productType, String productId); + + /** + * 根据商品类型、商品ID和数量获取阶梯价格配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @param productSubType 商品子类型 + * @param quantity 数量 + * @return 阶梯价格配置 + */ + PriceTierConfig getTierConfig(String productType, String productId, String productSubType, Integer quantity); + + /** + * 根据商品类型和数量获取阶梯价格配置(兼容旧接口) + * + * @param productType 商品类型 + * @param productSubType 商品子类型 + * @param quantity 数量 + * @return 阶梯价格配置 + */ + @Deprecated + PriceTierConfig getTierConfig(String productType, String productSubType, Integer quantity); + + /** + * 获取所有启用的商品配置 + * + * @return 商品配置列表 + */ + List getActiveProductConfigs(); + + /** + * 根据商品类型获取所有阶梯配置 + * + * @param productType 商品类型 + * @return 阶梯配置列表 + */ + List getTierConfigs(String productType); + + /** + * 根据商品类型和商品ID获取所有阶梯配置 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @return 阶梯配置列表 + */ + List getTierConfigs(String productType, String productId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java new file mode 100644 index 0000000..4394dd5 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java @@ -0,0 +1,173 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; +import com.ycwl.basic.pricing.enums.CouponType; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.exception.CouponInvalidException; +import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper; +import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper; +import com.ycwl.basic.pricing.service.ICouponService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 优惠券服务实现 + */ +@Slf4j +@Service("pricingCouponServiceImpl") +@RequiredArgsConstructor +public class CouponServiceImpl implements ICouponService { + + private final PriceCouponConfigMapper couponConfigMapper; + private final PriceCouponClaimRecordMapper couponClaimRecordMapper; + private final ObjectMapper objectMapper; + + @Override + public CouponInfo selectBestCoupon(Long userId, List products, BigDecimal totalAmount) { + List userCoupons = couponClaimRecordMapper.selectUserAvailableCoupons(userId); + if (userCoupons.isEmpty()) { + return null; + } + + CouponInfo bestCoupon = null; + BigDecimal maxDiscount = BigDecimal.ZERO; + + for (PriceCouponClaimRecord record : userCoupons) { + PriceCouponConfig coupon = couponConfigMapper.selectById(record.getCouponId()); + if (coupon == null || !isCouponApplicable(coupon, products, totalAmount)) { + continue; + } + + BigDecimal discount = calculateCouponDiscount(coupon, products, totalAmount); + if (discount.compareTo(maxDiscount) > 0) { + maxDiscount = discount; + bestCoupon = buildCouponInfo(coupon, discount); + } + } + + return bestCoupon; + } + + @Override + public BigDecimal calculateCouponDiscount(PriceCouponConfig coupon, List products, BigDecimal totalAmount) { + if (!isCouponApplicable(coupon, products, totalAmount)) { + return BigDecimal.ZERO; + } + + BigDecimal discount; + if (coupon.getCouponType() == CouponType.PERCENTAGE) { + discount = totalAmount.multiply(coupon.getDiscountValue().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)); + if (coupon.getMaxDiscount() != null && discount.compareTo(coupon.getMaxDiscount()) > 0) { + discount = coupon.getMaxDiscount(); + } + } else { + discount = coupon.getDiscountValue(); + } + + return discount.setScale(2, RoundingMode.HALF_UP); + } + + @Override + public boolean isCouponApplicable(PriceCouponConfig coupon, List products, BigDecimal totalAmount) { + if (totalAmount.compareTo(coupon.getMinAmount()) < 0) { + return false; + } + + if (coupon.getApplicableProducts() == null || coupon.getApplicableProducts().isEmpty()) { + return true; + } + + try { + List applicableProductTypes = objectMapper.readValue( + coupon.getApplicableProducts(), new TypeReference>() {}); + + for (ProductItem product : products) { + if (applicableProductTypes.contains(product.getProductType().getCode())) { + return true; + } + } + return false; + } catch (Exception e) { + log.error("解析适用商品类型失败", e); + return false; + } + } + + @Override + @Transactional + public CouponUseResult useCoupon(CouponUseRequest request) { + PriceCouponClaimRecord record = couponClaimRecordMapper.selectUserCouponRecord( + request.getUserId(), request.getCouponId()); + + if (record == null) { + throw new CouponInvalidException("用户未拥有该优惠券"); + } + + if (record.getStatus() != CouponStatus.CLAIMED) { + throw new CouponInvalidException("优惠券状态无效: " + record.getStatus()); + } + + int updateCount = couponConfigMapper.incrementUsedQuantity(request.getCouponId()); + if (updateCount == 0) { + throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限"); + } + + LocalDateTime useTime = LocalDateTime.now(); + + // 设置使用时间和订单信息 + record.setStatus(CouponStatus.USED); + record.setUseTime(useTime); + record.setOrderId(request.getOrderId()); + record.setUpdatedTime(LocalDateTime.now()); + + couponClaimRecordMapper.updateCouponStatus( + record.getId(), CouponStatus.USED, useTime, request.getOrderId()); + + CouponUseResult result = new CouponUseResult(); + result.setCouponId(request.getCouponId()); + result.setUserId(request.getUserId()); + result.setOrderId(request.getOrderId()); + result.setUseTime(useTime); + result.setDiscountAmount(request.getDiscountAmount()); + + return result; + } + + @Override + public List getUserAvailableCoupons(Long userId) { + List records = couponClaimRecordMapper.selectUserAvailableCoupons(userId); + List coupons = new ArrayList<>(); + + for (PriceCouponClaimRecord record : records) { + PriceCouponConfig config = couponConfigMapper.selectById(record.getCouponId()); + if (config != null) { + coupons.add(buildCouponInfo(config, null)); + } + } + + return coupons; + } + + private CouponInfo buildCouponInfo(PriceCouponConfig coupon, BigDecimal actualDiscountAmount) { + CouponInfo info = new CouponInfo(); + info.setCouponId(coupon.getId()); + info.setCouponName(coupon.getCouponName()); + info.setDiscountType(coupon.getCouponType()); + info.setDiscountValue(coupon.getDiscountValue()); + info.setActualDiscountAmount(actualDiscountAmount); + return info; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java new file mode 100644 index 0000000..2baa997 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java @@ -0,0 +1,104 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.mapper.PriceBundleConfigMapper; +import com.ycwl.basic.pricing.service.IPriceBundleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 一口价套餐服务实现 + */ +@Slf4j +@Service("pricingBundleServiceImpl") +@RequiredArgsConstructor +public class PriceBundleServiceImpl implements IPriceBundleService { + + private final PriceBundleConfigMapper bundleConfigMapper; + private final ObjectMapper objectMapper; + + @Override + public boolean isBundleApplicable(List products) { + List bundles = getActiveBundles(); + if (bundles.isEmpty()) { + return false; + } + + Set productTypes = new HashSet<>(); + for (ProductItem product : products) { + productTypes.add(product.getProductType().getCode()); + } + + for (PriceBundleConfig bundle : bundles) { + if (isProductsMatchBundle(productTypes, bundle)) { + return true; + } + } + + return false; + } + + @Override + public BigDecimal getBundlePrice(List products) { + if (!isBundleApplicable(products)) { + return null; + } + + List bundles = getActiveBundles(); + Set productTypes = new HashSet<>(); + for (ProductItem product : products) { + productTypes.add(product.getProductType().getCode()); + } + + for (PriceBundleConfig bundle : bundles) { + if (isProductsMatchBundle(productTypes, bundle)) { + return bundle.getBundlePrice(); + } + } + + return null; + } + + @Override + @Cacheable(value = "active-bundles") + public List getActiveBundles() { + return bundleConfigMapper.selectActiveBundles(); + } + + private boolean isProductsMatchBundle(Set productTypes, PriceBundleConfig bundle) { + try { + List includedProducts = objectMapper.readValue( + bundle.getIncludedProducts(), new TypeReference>() {}); + + Set requiredProducts = new HashSet<>(includedProducts); + + if (bundle.getExcludedProducts() != null && !bundle.getExcludedProducts().isEmpty()) { + List excludedProducts = objectMapper.readValue( + bundle.getExcludedProducts(), new TypeReference>() {}); + + for (String excludedProduct : excludedProducts) { + if (productTypes.contains(excludedProduct)) { + return false; + } + } + } + + return productTypes.containsAll(requiredProducts); + + } catch (Exception e) { + log.error("解析一口价配置失败: bundleId={}", bundle.getId(), e); + return false; + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..b1783ce --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java @@ -0,0 +1,238 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.exception.PriceCalculationException; +import com.ycwl.basic.pricing.service.ICouponService; +import com.ycwl.basic.pricing.service.IPriceBundleService; +import com.ycwl.basic.pricing.service.IPriceCalculationService; +import com.ycwl.basic.pricing.service.IProductConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 价格计算服务实现 + */ +@Slf4j +@Service("pricingCalculationServiceImpl") +@RequiredArgsConstructor +public class PriceCalculationServiceImpl implements IPriceCalculationService { + + private final IProductConfigService productConfigService; + private final ICouponService couponService; + private final IPriceBundleService bundleService; + + @Override + public PriceCalculationResult calculatePrice(PriceCalculationRequest request) { + if (request.getProducts() == null || request.getProducts().isEmpty()) { + throw new PriceCalculationException("商品列表不能为空"); + } + + // 计算商品价格和原价 + PriceDetails priceDetails = calculateProductsPriceWithOriginal(request.getProducts()); + BigDecimal totalAmount = priceDetails.getTotalAmount(); + BigDecimal originalTotalAmount = priceDetails.getOriginalTotalAmount(); + + List discountDetails = new ArrayList<>(); + + // 添加限时立减折扣(如果原价 > 实际价格) + BigDecimal limitedTimeDiscount = originalTotalAmount.subtract(totalAmount); + if (limitedTimeDiscount.compareTo(BigDecimal.ZERO) > 0) { + discountDetails.add(DiscountDetail.createLimitedTimeDiscount(limitedTimeDiscount)); + } + + // 检查一口价优惠 + BigDecimal bundlePrice = bundleService.getBundlePrice(request.getProducts()); + if (bundlePrice != null && bundlePrice.compareTo(totalAmount) < 0) { + BigDecimal bundleDiscount = totalAmount.subtract(bundlePrice); + discountDetails.add(DiscountDetail.createBundleDiscount(bundleDiscount)); + totalAmount = bundlePrice; + log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount); + } + + PriceCalculationResult result = new PriceCalculationResult(); + result.setOriginalAmount(originalTotalAmount); // 原总价 + result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计 + result.setProductDetails(request.getProducts()); + + // 处理优惠券 + BigDecimal couponDiscountAmount = BigDecimal.ZERO; + if (Boolean.TRUE.equals(request.getAutoUseCoupon()) && request.getUserId() != null) { + CouponInfo bestCoupon = couponService.selectBestCoupon( + request.getUserId(), request.getProducts(), totalAmount); + + if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + result.setUsedCoupon(bestCoupon); + couponDiscountAmount = bestCoupon.getActualDiscountAmount(); + discountDetails.add(DiscountDetail.createCouponDiscount(bestCoupon.getCouponName(), couponDiscountAmount)); + } + } + + // 计算总优惠金额 + BigDecimal totalDiscountAmount = discountDetails.stream() + .map(DiscountDetail::getDiscountAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 按排序排列折扣明细 + discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder)); + + result.setDiscountAmount(totalDiscountAmount); + result.setDiscountDetails(discountDetails); + result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount)); + + return result; + } + + private BigDecimal calculateProductsPrice(List products) { + BigDecimal totalAmount = BigDecimal.ZERO; + + for (ProductItem product : products) { + BigDecimal itemPrice = calculateSingleProductPrice(product); + product.setUnitPrice(itemPrice); + + BigDecimal subtotal = itemPrice.multiply(BigDecimal.valueOf(product.getPurchaseCount())); + product.setSubtotal(subtotal); + + totalAmount = totalAmount.add(subtotal); + } + + return totalAmount.setScale(2, RoundingMode.HALF_UP); + } + + private PriceDetails calculateProductsPriceWithOriginal(List products) { + BigDecimal totalAmount = BigDecimal.ZERO; + BigDecimal originalTotalAmount = BigDecimal.ZERO; + + for (ProductItem product : products) { + // 计算实际价格和原价 + ProductPriceInfo priceInfo = calculateSingleProductPriceWithOriginal(product); + + product.setUnitPrice(priceInfo.getActualPrice()); + product.setOriginalPrice(priceInfo.getOriginalPrice()); + + BigDecimal subtotal = priceInfo.getActualPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount())); + BigDecimal originalSubtotal = priceInfo.getOriginalPrice().multiply(BigDecimal.valueOf(product.getPurchaseCount())); + + product.setSubtotal(subtotal); + + totalAmount = totalAmount.add(subtotal); + originalTotalAmount = originalTotalAmount.add(originalSubtotal); + } + + return new PriceDetails( + totalAmount.setScale(2, RoundingMode.HALF_UP), + originalTotalAmount.setScale(2, RoundingMode.HALF_UP) + ); + } + + private BigDecimal calculateSingleProductPrice(ProductItem product) { + ProductType productType = product.getProductType(); + String productId = product.getProductId() != null ? product.getProductId() : "default"; + + // 优先使用基于product_id的阶梯定价 + PriceTierConfig tierConfig = productConfigService.getTierConfig( + productType.getCode(), productId, product.getProductSubType(), product.getQuantity()); + + if (tierConfig != null) { + log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}", + productType.getCode(), productId, product.getQuantity(), tierConfig.getPrice()); + return tierConfig.getPrice(); + } + + // 使用基于product_id的基础配置 + try { + PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId); + if (baseConfig != null) { + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); + } else { + return baseConfig.getBasePrice(); + } + } + } catch (Exception e) { + log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置", + productType, productId); + } + + // 兜底:使用通用配置(向后兼容) + List configs = productConfigService.getProductConfig(productType.getCode()); + if (!configs.isEmpty()) { + PriceProductConfig baseConfig = configs.get(0); // 使用第一个配置作为默认 + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + return baseConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); + } else { + return baseConfig.getBasePrice(); + } + } + + throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); + } + + private ProductPriceInfo calculateSingleProductPriceWithOriginal(ProductItem product) { + ProductType productType = product.getProductType(); + String productId = product.getProductId() != null ? product.getProductId() : "default"; + + BigDecimal actualPrice; + BigDecimal originalPrice = null; + + // 优先使用基于product_id的阶梯定价 + PriceTierConfig tierConfig = productConfigService.getTierConfig( + productType.getCode(), productId, product.getProductSubType(), product.getQuantity()); + + if (tierConfig != null) { + actualPrice = tierConfig.getPrice(); + originalPrice = tierConfig.getOriginalPrice(); + log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}, originalPrice={}", + productType.getCode(), productId, product.getQuantity(), actualPrice, originalPrice); + } else { + // 使用基于product_id的基础配置 + try { + PriceProductConfig baseConfig = productConfigService.getProductConfig(productType.getCode(), productId); + if (baseConfig != null) { + actualPrice = baseConfig.getBasePrice(); + originalPrice = baseConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } else { + throw new PriceCalculationException("无法找到具体商品配置"); + } + } catch (Exception e) { + log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置", + productType, productId); + + // 兜底:使用通用配置(向后兼容) + List configs = productConfigService.getProductConfig(productType.getCode()); + if (!configs.isEmpty()) { + PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认 + actualPrice = baseConfig.getBasePrice(); + originalPrice = baseConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } else { + throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); + } + } + } + + return new ProductPriceInfo(actualPrice, originalPrice); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java new file mode 100644 index 0000000..948f908 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java @@ -0,0 +1,107 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.entity.*; +import com.ycwl.basic.pricing.mapper.*; +import com.ycwl.basic.pricing.service.IPricingManagementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 价格管理服务实现(用于配置管理,手动处理时间字段) + */ +@Slf4j +@Service("pricingManagementServiceImpl") +@RequiredArgsConstructor +public class PricingManagementServiceImpl implements IPricingManagementService { + + private final PriceProductConfigMapper productConfigMapper; + private final PriceTierConfigMapper tierConfigMapper; + private final PriceCouponConfigMapper couponConfigMapper; + private final PriceCouponClaimRecordMapper couponClaimRecordMapper; + private final PriceBundleConfigMapper bundleConfigMapper; + + @Override + @Transactional + public Long createProductConfig(PriceProductConfig config) { + config.setCreatedTime(LocalDateTime.now()); + config.setUpdatedTime(LocalDateTime.now()); + productConfigMapper.insertProductConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateProductConfig(PriceProductConfig config) { + config.setUpdatedTime(LocalDateTime.now()); + return productConfigMapper.updateProductConfig(config) > 0; + } + + @Override + @Transactional + public Long createTierConfig(PriceTierConfig config) { + config.setCreatedTime(LocalDateTime.now()); + config.setUpdatedTime(LocalDateTime.now()); + tierConfigMapper.insertTierConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateTierConfig(PriceTierConfig config) { + config.setUpdatedTime(LocalDateTime.now()); + return tierConfigMapper.updateTierConfig(config) > 0; + } + + @Override + @Transactional + public Long createCouponConfig(PriceCouponConfig config) { + config.setCreatedTime(LocalDateTime.now()); + config.setUpdatedTime(LocalDateTime.now()); + couponConfigMapper.insertCoupon(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateCouponConfig(PriceCouponConfig config) { + config.setUpdatedTime(LocalDateTime.now()); + return couponConfigMapper.updateCoupon(config) > 0; + } + + @Override + @Transactional + public Long createCouponClaimRecord(PriceCouponClaimRecord record) { + record.setClaimTime(LocalDateTime.now()); + record.setCreatedTime(LocalDateTime.now()); + record.setUpdatedTime(LocalDateTime.now()); + couponClaimRecordMapper.insertClaimRecord(record); + return record.getId(); + } + + @Override + @Transactional + public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) { + record.setUpdatedTime(LocalDateTime.now()); + return couponClaimRecordMapper.updateClaimRecord(record) > 0; + } + + @Override + @Transactional + public Long createBundleConfig(PriceBundleConfig config) { + config.setCreatedTime(LocalDateTime.now()); + config.setUpdatedTime(LocalDateTime.now()); + bundleConfigMapper.insertBundleConfig(config); + return config.getId(); + } + + @Override + @Transactional + public boolean updateBundleConfig(PriceBundleConfig config) { + config.setUpdatedTime(LocalDateTime.now()); + return bundleConfigMapper.updateBundleConfig(config) > 0; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..acf74ef --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/ProductConfigServiceImpl.java @@ -0,0 +1,78 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.exception.ProductConfigNotFoundException; +import com.ycwl.basic.pricing.mapper.PriceProductConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceTierConfigMapper; +import com.ycwl.basic.pricing.service.IProductConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 商品配置管理服务实现 + */ +@Slf4j +@Service("pricingProductConfigServiceImpl") +@RequiredArgsConstructor +public class ProductConfigServiceImpl implements IProductConfigService { + + private final PriceProductConfigMapper productConfigMapper; + private final PriceTierConfigMapper tierConfigMapper; + + @Override + @Cacheable(value = "product-config", key = "#productType") + public List getProductConfig(String productType) { + return productConfigMapper.selectByProductType(productType); + } + + @Override + @Cacheable(value = "product-config", key = "#productType + '_' + #productId") + public PriceProductConfig getProductConfig(String productType, String productId) { + PriceProductConfig config = productConfigMapper.selectByProductTypeAndId(productType, productId); + if (config == null) { + throw new ProductConfigNotFoundException("商品配置未找到: " + productType + ", productId: " + productId); + } + return config; + } + + @Override + @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + (#productSubType ?: 'default') + '_' + #quantity") + public PriceTierConfig getTierConfig(String productType, String productId, String productSubType, Integer quantity) { + PriceTierConfig config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, productSubType, quantity); + if (config == null) { + log.warn("阶梯定价配置未找到: productType={}, productId={}, productSubType={}, quantity={}", + productType, productId, productSubType, quantity); + } + return config; + } + + @Override + @Deprecated + public PriceTierConfig getTierConfig(String productType, String productSubType, Integer quantity) { + // 兼容旧接口,使用默认productId + return getTierConfig(productType, "default", productSubType, quantity); + } + + @Override + @Cacheable(value = "active-product-configs") + public List getActiveProductConfigs() { + return productConfigMapper.selectActiveConfigs(); + } + + @Override + @Cacheable(value = "tier-configs", key = "#productType") + public List getTierConfigs(String productType) { + return tierConfigMapper.selectByProductType(productType); + } + + @Override + @Cacheable(value = "tier-configs", key = "#productType + '_' + #productId") + public List getTierConfigs(String productType, String productId) { + return tierConfigMapper.selectByProductTypeAndId(productType, productId); + } +} \ No newline at end of file From af5c59dc6759dc3e269e9440d45b08e29af665f2 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 15 Aug 2025 13:33:51 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=99=AF=E5=8C=BA?= =?UTF-8?q?=E3=80=81=E5=88=A0=E9=99=A4=E5=AD=90=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PricingConfigController.java | 172 ++++++++++++------ .../pricing/entity/PriceBundleConfig.java | 5 + .../pricing/entity/PriceProductConfig.java | 5 + .../basic/pricing/entity/PriceTierConfig.java | 4 +- .../ycwl/basic/pricing/enums/ProductType.java | 10 +- .../mapper/PriceBundleConfigMapper.java | 21 ++- .../mapper/PriceProductConfigMapper.java | 27 ++- .../pricing/mapper/PriceTierConfigMapper.java | 44 ++++- .../pricing/service/IPriceBundleService.java | 9 + .../service/IPricingManagementService.java | 34 ++++ .../service/IProductConfigService.java | 54 ++++-- .../service/impl/PriceBundleServiceImpl.java | 11 +- .../impl/PriceCalculationServiceImpl.java | 4 +- .../impl/PricingManagementServiceImpl.java | 46 +++++ .../impl/ProductConfigServiceImpl.java | 57 ++++-- 15 files changed, 391 insertions(+), 112 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java index 8b98ad8..35d9d6b 100644 --- a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java @@ -26,61 +26,6 @@ public class PricingConfigController { private final IPriceBundleService bundleService; private final IPricingManagementService managementService; - /** - * 获取所有商品配置 - */ - @GetMapping("/products") - public ApiResponse> getProductConfigs() { - List configs = productConfigService.getActiveProductConfigs(); - return ApiResponse.success(configs); - } - - /** - * 根据商品类型获取阶梯配置 - */ - @GetMapping("/tiers/{productType}") - public ApiResponse> getTierConfigs(@PathVariable String productType) { - List configs = productConfigService.getTierConfigs(productType); - return ApiResponse.success(configs); - } - - /** - * 根据商品类型和商品ID获取阶梯配置 - */ - @GetMapping("/tiers/{productType}/{productId}") - public ApiResponse> getTierConfigs(@PathVariable String productType, - @PathVariable String productId) { - List configs = productConfigService.getTierConfigs(productType, productId); - return ApiResponse.success(configs); - } - - /** - * 根据商品类型和商品ID获取具体配置 - */ - @GetMapping("/products/{productType}/{productId}") - public ApiResponse getProductConfig(@PathVariable String productType, - @PathVariable String productId) { - PriceProductConfig config = productConfigService.getProductConfig(productType, productId); - return ApiResponse.success(config); - } - - /** - * 获取所有阶梯配置 - */ - @GetMapping("/tiers") - public ApiResponse> getAllTierConfigs() { - log.info("获取所有阶梯定价配置"); - return ApiResponse.success(List.of()); - } - - /** - * 获取所有一口价配置 - */ - @GetMapping("/bundles") - public ApiResponse> getBundleConfigs() { - List configs = bundleService.getActiveBundles(); - return ApiResponse.success(configs); - } // ==================== 配置管理API(手动处理时间) ==================== @@ -146,4 +91,121 @@ public class PricingConfigController { boolean success = managementService.updateBundleConfig(config); return ApiResponse.success(success); } + + // ==================== 启用/禁用API ==================== + + /** + * 启用/禁用商品配置 + */ + @PutMapping("/products/{id}/status") + public ApiResponse updateProductConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("修改商品配置状态: id={}, isActive={}", id, isActive); + boolean success = managementService.updateProductConfigStatus(id, isActive); + return ApiResponse.success(success); + } + + /** + * 启用/禁用阶梯配置 + */ + @PutMapping("/tiers/{id}/status") + public ApiResponse updateTierConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("修改阶梯配置状态: id={}, isActive={}", id, isActive); + boolean success = managementService.updateTierConfigStatus(id, isActive); + return ApiResponse.success(success); + } + + /** + * 启用/禁用一口价配置 + */ + @PutMapping("/bundles/{id}/status") + public ApiResponse updateBundleConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("修改一口价配置状态: id={}, isActive={}", id, isActive); + boolean success = managementService.updateBundleConfigStatus(id, isActive); + return ApiResponse.success(success); + } + + // ==================== 删除API ==================== + + /** + * 删除商品配置 + */ + @DeleteMapping("/products/{id}") + public ApiResponse deleteProductConfig(@PathVariable Long id) { + log.info("删除商品配置: id={}", id); + boolean success = managementService.deleteProductConfig(id); + return ApiResponse.success(success); + } + + /** + * 删除阶梯配置 + */ + @DeleteMapping("/tiers/{id}") + public ApiResponse deleteTierConfig(@PathVariable Long id) { + log.info("删除阶梯配置: id={}", id); + boolean success = managementService.deleteTierConfig(id); + return ApiResponse.success(success); + } + + /** + * 删除一口价配置 + */ + @DeleteMapping("/bundles/{id}") + public ApiResponse deleteBundleConfig(@PathVariable Long id) { + log.info("删除一口价配置: id={}", id); + boolean success = managementService.deleteBundleConfig(id); + return ApiResponse.success(success); + } + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 管理端:获取所有商品配置(包含禁用的) + */ + @GetMapping("/admin/products") + public ApiResponse> getAllProductConfigsForAdmin() { + log.info("管理端获取所有商品配置"); + List configs = productConfigService.getAllProductConfigsForAdmin(); + return ApiResponse.success(configs); + } + + /** + * 管理端:获取所有阶梯配置(包含禁用的) + */ + @GetMapping("/admin/tiers") + public ApiResponse> getAllTierConfigsForAdmin() { + log.info("管理端获取所有阶梯配置"); + List configs = productConfigService.getAllTierConfigsForAdmin(); + return ApiResponse.success(configs); + } + + /** + * 管理端:根据商品类型获取阶梯配置(包含禁用的) + */ + @GetMapping("/admin/tiers/{productType}") + public ApiResponse> getTierConfigsForAdmin(@PathVariable String productType) { + log.info("管理端根据商品类型获取阶梯配置: {}", productType); + List configs = productConfigService.getTierConfigsForAdmin(productType); + return ApiResponse.success(configs); + } + + /** + * 管理端:根据商品类型和商品ID获取阶梯配置(包含禁用的) + */ + @GetMapping("/admin/tiers/{productType}/{productId}") + public ApiResponse> getTierConfigsForAdmin(@PathVariable String productType, + @PathVariable String productId) { + log.info("管理端根据商品类型和ID获取阶梯配置: {}, {}", productType, productId); + List configs = productConfigService.getTierConfigsForAdmin(productType, productId); + return ApiResponse.success(configs); + } + + /** + * 管理端:获取所有一口价配置(包含禁用的) + */ + @GetMapping("/admin/bundles") + public ApiResponse> getAllBundleConfigsForAdmin() { + log.info("管理端获取所有一口价配置"); + List configs = bundleService.getAllBundlesForAdmin(); + return ApiResponse.success(configs); + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java index f7457dd..0d1ec9f 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java @@ -19,6 +19,11 @@ public class PriceBundleConfig extends BaseEntity { */ private String bundleName; + /** + * 景区ID + */ + private String scenicId; + /** * 套餐价格 */ diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java index 5bc37d7..8fec5d8 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java @@ -24,6 +24,11 @@ public class PriceProductConfig extends BaseEntity { */ private String productId; + /** + * 景区ID:用于前端搜索和编辑时正确显示景区内容 + */ + private String scenicId; + /** * 商品名称 */ diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java index 31711b7..4c76593 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java @@ -25,9 +25,9 @@ public class PriceTierConfig extends BaseEntity { private String productId; /** - * 商品子类型 + * 景区ID:用于前端搜索和编辑时正确显示景区内容 */ - private String productSubType; + private String scenicId; /** * 最小数量 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 c90bab2..bc3aa78 100644 --- a/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java +++ b/src/main/java/com/ycwl/basic/pricing/enums/ProductType.java @@ -10,11 +10,11 @@ import lombok.Getter; @AllArgsConstructor public enum ProductType { - VLOG_VIDEO("vlog_video", "Vlog视频"), - RECORDING_SET("recording_set", "录像集"), - PHOTO_SET("photo_set", "照相集"), - PHOTO_PRINT("photo_print", "照片打印"), - MACHINE_PRINT("machine_print", "一体机打印"); + VLOG_VIDEO("VLOG_VIDEO", "Vlog视频"), + RECORDING_SET("RECORDING_SET", "录像集"), + PHOTO_SET("PHOTO_SET", "照相集"), + PHOTO_PRINT("PHOTO_PRINT", "照片打印"), + MACHINE_PRINT("MACHINE_PRINT", "一体机打印"); private final String code; private final String description; diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java index 3f7f48f..08a7199 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ycwl.basic.pricing.entity.PriceBundleConfig; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; @@ -27,20 +28,34 @@ public interface PriceBundleConfigMapper extends BaseMapper { @Select("SELECT * FROM price_bundle_config WHERE id = #{id} AND is_active = 1") PriceBundleConfig selectActiveBundleById(Long id); + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 查询所有一口价配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_bundle_config ORDER BY is_active DESC, bundle_name ASC") + List selectAllBundlesForAdmin(); + /** * 插入一口价配置 */ - @Insert("INSERT INTO price_bundle_config (bundle_name, bundle_price, included_products, excluded_products, " + + @Insert("INSERT INTO price_bundle_config (bundle_name, scenic_id, bundle_price, included_products, excluded_products, " + "description, is_active, created_time, updated_time) VALUES " + - "(#{bundleName}, #{bundlePrice}, #{includedProducts}, #{excludedProducts}, " + + "(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts}, #{excludedProducts}, " + "#{description}, #{isActive}, NOW(), NOW())") int insertBundleConfig(PriceBundleConfig config); /** * 更新一口价配置 */ - @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, bundle_price = #{bundlePrice}, " + + @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, scenic_id = #{scenicId}, bundle_price = #{bundlePrice}, " + "included_products = #{includedProducts}, excluded_products = #{excludedProducts}, " + "description = #{description}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") int updateBundleConfig(PriceBundleConfig config); + + /** + * 更新一口价配置状态 + */ + @Update("UPDATE price_bundle_config SET is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateBundleConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); } \ No newline at end of file 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 907e650..0f77fcb 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ycwl.basic.pricing.entity.PriceProductConfig; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; @@ -33,17 +34,37 @@ 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); + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 查询所有商品配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_product_config ORDER BY product_type ASC, is_active DESC, product_id ASC") + List selectAllConfigsForAdmin(); + + /** + * 根据商品类型查询所有配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_product_config WHERE product_type = #{productType} ORDER BY is_active DESC, product_id ASC") + List selectByProductTypeForAdmin(@Param("productType") String productType); + /** * 插入商品价格配置 */ - @Insert("INSERT INTO price_product_config (product_type, product_id, product_name, base_price, original_price, unit, is_active, created_time, updated_time) " + - "VALUES (#{productType}, #{productId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, NOW(), NOW())") + @Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, created_time, updated_time) " + + "VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, NOW(), NOW())") int insertProductConfig(PriceProductConfig config); /** * 更新商品价格配置 */ - @Update("UPDATE price_product_config SET product_id = #{productId}, product_name = #{productName}, base_price = #{basePrice}, " + + @Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " + "original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") int updateProductConfig(PriceProductConfig config); + + /** + * 更新商品配置状态 + */ + @Update("UPDATE price_product_config SET is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateProductConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); } \ No newline at end of file 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 e1f6a72..5421f65 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java @@ -21,12 +21,10 @@ public interface PriceTierConfigMapper extends BaseMapper { */ @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + "AND product_id = #{productId} " + - "AND (product_sub_type = #{productSubType} OR product_sub_type IS NULL) " + "AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " + "AND is_active = 1 ORDER BY sort_order ASC LIMIT 1") PriceTierConfig selectByProductTypeAndQuantity(@Param("productType") String productType, @Param("productId") String productId, - @Param("productSubType") String productSubType, @Param("quantity") Integer quantity); /** @@ -44,28 +42,56 @@ public interface PriceTierConfigMapper extends BaseMapper { List selectByProductTypeAndId(@Param("productType") String productType, @Param("productId") String productId); + /** - * 根据商品类型和子类型查询阶梯配置 + * 查询所有启用的阶梯配置 + */ + @Select("SELECT * FROM price_tier_config WHERE is_active = 1 ORDER BY product_type ASC, sort_order ASC") + List selectAllActiveConfigs(); + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 查询所有阶梯配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_tier_config ORDER BY product_type ASC, is_active DESC, sort_order ASC") + List selectAllConfigsForAdmin(); + + /** + * 根据商品类型查询所有阶梯配置(包含禁用的)- 管理端使用 */ @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + - "AND product_sub_type = #{productSubType} AND is_active = 1 ORDER BY sort_order ASC") - List selectByProductTypeAndSubType(@Param("productType") String productType, - @Param("productSubType") String productSubType); + "ORDER BY is_active DESC, sort_order ASC") + List selectByProductTypeForAdmin(@Param("productType") String productType); + + /** + * 根据商品类型和商品ID查询所有阶梯配置(包含禁用的)- 管理端使用 + */ + @Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " + + "AND product_id = #{productId} ORDER BY is_active DESC, sort_order ASC") + List selectByProductTypeAndIdForAdmin(@Param("productType") String productType, + @Param("productId") String productId); /** * 插入阶梯定价配置 */ - @Insert("INSERT INTO price_tier_config (product_type, product_id, product_sub_type, min_quantity, max_quantity, price, " + + @Insert("INSERT INTO price_tier_config (product_type, product_id, scenic_id, min_quantity, max_quantity, price, " + "original_price, unit, sort_order, is_active, created_time, updated_time) VALUES " + - "(#{productType}, #{productId}, #{productSubType}, #{minQuantity}, #{maxQuantity}, #{price}, " + + "(#{productType}, #{productId}, #{scenicId}, #{minQuantity}, #{maxQuantity}, #{price}, " + "#{originalPrice}, #{unit}, #{sortOrder}, #{isActive}, NOW(), NOW())") int insertTierConfig(PriceTierConfig config); /** * 更新阶梯定价配置 */ - @Update("UPDATE price_tier_config SET product_id = #{productId}, product_sub_type = #{productSubType}, min_quantity = #{minQuantity}, " + + @Update("UPDATE price_tier_config SET product_id = #{productId}, scenic_id = #{scenicId}, min_quantity = #{minQuantity}, " + "max_quantity = #{maxQuantity}, price = #{price}, original_price = #{originalPrice}, unit = #{unit}, sort_order = #{sortOrder}, " + "is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") int updateTierConfig(PriceTierConfig config); + + /** + * 更新阶梯配置状态 + */ + @Update("UPDATE price_tier_config SET is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateTierConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java index 0e7ae09..2e66341 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java @@ -33,4 +33,13 @@ public interface IPriceBundleService { * @return 一口价配置列表 */ List getActiveBundles(); + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 获取所有一口价配置(包含禁用的)- 管理端使用 + * + * @return 一口价配置列表 + */ + List getAllBundlesForAdmin(); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java index bcb9f29..a37de38 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java @@ -56,4 +56,38 @@ public interface IPricingManagementService { * 更新一口价配置 */ boolean updateBundleConfig(PriceBundleConfig config); + + // ==================== 状态管理 ==================== + + /** + * 更新商品配置状态 + */ + boolean updateProductConfigStatus(Long id, Boolean isActive); + + /** + * 更新阶梯配置状态 + */ + boolean updateTierConfigStatus(Long id, Boolean isActive); + + /** + * 更新一口价配置状态 + */ + boolean updateBundleConfigStatus(Long id, Boolean isActive); + + // ==================== 删除操作 ==================== + + /** + * 删除商品配置 + */ + boolean deleteProductConfig(Long id); + + /** + * 删除阶梯配置 + */ + boolean deleteTierConfig(Long id); + + /** + * 删除一口价配置 + */ + boolean deleteBundleConfig(Long id); } \ No newline at end of file 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 cd7ab96..11a31b8 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java @@ -32,22 +32,10 @@ public interface IProductConfigService { * * @param productType 商品类型 * @param productId 具体商品ID - * @param productSubType 商品子类型 * @param quantity 数量 * @return 阶梯价格配置 */ - PriceTierConfig getTierConfig(String productType, String productId, String productSubType, Integer quantity); - - /** - * 根据商品类型和数量获取阶梯价格配置(兼容旧接口) - * - * @param productType 商品类型 - * @param productSubType 商品子类型 - * @param quantity 数量 - * @return 阶梯价格配置 - */ - @Deprecated - PriceTierConfig getTierConfig(String productType, String productSubType, Integer quantity); + PriceTierConfig getTierConfig(String productType, String productId, Integer quantity); /** * 获取所有启用的商品配置 @@ -72,4 +60,44 @@ public interface IProductConfigService { * @return 阶梯配置列表 */ List getTierConfigs(String productType, String productId); + + /** + * 获取所有启用的阶梯配置 + * + * @return 阶梯配置列表 + */ + List getAllTierConfigs(); + + // ==================== 管理端接口(包含禁用的配置) ==================== + + /** + * 获取所有商品配置(包含禁用的)- 管理端使用 + * + * @return 商品配置列表 + */ + List getAllProductConfigsForAdmin(); + + /** + * 获取所有阶梯配置(包含禁用的)- 管理端使用 + * + * @return 阶梯配置列表 + */ + List getAllTierConfigsForAdmin(); + + /** + * 根据商品类型获取所有阶梯配置(包含禁用的)- 管理端使用 + * + * @param productType 商品类型 + * @return 阶梯配置列表 + */ + List getTierConfigsForAdmin(String productType); + + /** + * 根据商品类型和商品ID获取所有阶梯配置(包含禁用的)- 管理端使用 + * + * @param productType 商品类型 + * @param productId 具体商品ID + * @return 阶梯配置列表 + */ + List getTierConfigsForAdmin(String productType, String productId); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java index 2baa997..f0ba3eb 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java @@ -9,7 +9,7 @@ import com.ycwl.basic.pricing.mapper.PriceBundleConfigMapper; import com.ycwl.basic.pricing.service.IPriceBundleService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.Cacheable; +//import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -71,11 +71,18 @@ public class PriceBundleServiceImpl implements IPriceBundleService { } @Override - @Cacheable(value = "active-bundles") +// @Cacheable(value = "active-bundles") public List getActiveBundles() { return bundleConfigMapper.selectActiveBundles(); } + // ==================== 管理端接口(包含禁用的配置) ==================== + + @Override + public List getAllBundlesForAdmin() { + return bundleConfigMapper.selectAllBundlesForAdmin(); + } + private boolean isProductsMatchBundle(Set productTypes, PriceBundleConfig bundle) { try { List includedProducts = objectMapper.readValue( 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 b1783ce..1cf1490 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 @@ -140,7 +140,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { // 优先使用基于product_id的阶梯定价 PriceTierConfig tierConfig = productConfigService.getTierConfig( - productType.getCode(), productId, product.getProductSubType(), product.getQuantity()); + productType.getCode(), productId, product.getQuantity()); if (tierConfig != null) { log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}", @@ -186,7 +186,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { // 优先使用基于product_id的阶梯定价 PriceTierConfig tierConfig = productConfigService.getTierConfig( - productType.getCode(), productId, product.getProductSubType(), product.getQuantity()); + productType.getCode(), productId, product.getQuantity()); if (tierConfig != null) { actualPrice = tierConfig.getPrice(); diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java index 948f908..0a70889 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java @@ -104,4 +104,50 @@ public class PricingManagementServiceImpl implements IPricingManagementService { config.setUpdatedTime(LocalDateTime.now()); return bundleConfigMapper.updateBundleConfig(config) > 0; } + + // ==================== 状态管理 ==================== + + @Override + @Transactional + public boolean updateProductConfigStatus(Long id, Boolean isActive) { + log.info("更新商品配置状态: id={}, isActive={}", id, isActive); + return productConfigMapper.updateProductConfigStatus(id, isActive) > 0; + } + + @Override + @Transactional + public boolean updateTierConfigStatus(Long id, Boolean isActive) { + log.info("更新阶梯配置状态: id={}, isActive={}", id, isActive); + return tierConfigMapper.updateTierConfigStatus(id, isActive) > 0; + } + + @Override + @Transactional + public boolean updateBundleConfigStatus(Long id, Boolean isActive) { + log.info("更新一口价配置状态: id={}, isActive={}", id, isActive); + return bundleConfigMapper.updateBundleConfigStatus(id, isActive) > 0; + } + + // ==================== 删除操作 ==================== + + @Override + @Transactional + public boolean deleteProductConfig(Long id) { + log.info("删除商品配置: id={}", id); + return productConfigMapper.deleteById(id) > 0; + } + + @Override + @Transactional + public boolean deleteTierConfig(Long id) { + log.info("删除阶梯配置: id={}", id); + return tierConfigMapper.deleteById(id) > 0; + } + + @Override + @Transactional + public boolean deleteBundleConfig(Long id) { + log.info("删除一口价配置: id={}", id); + return bundleConfigMapper.deleteById(id) > 0; + } } \ No newline at end of file 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 acf74ef..9103e2b 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 @@ -8,7 +8,7 @@ import com.ycwl.basic.pricing.mapper.PriceTierConfigMapper; import com.ycwl.basic.pricing.service.IProductConfigService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.Cacheable; +//import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.List; @@ -25,13 +25,13 @@ public class ProductConfigServiceImpl implements IProductConfigService { private final PriceTierConfigMapper tierConfigMapper; @Override - @Cacheable(value = "product-config", key = "#productType") +// @Cacheable(value = "product-config", key = "#productType") public List getProductConfig(String productType) { return productConfigMapper.selectByProductType(productType); } @Override - @Cacheable(value = "product-config", key = "#productType + '_' + #productId") +// @Cacheable(value = "product-config", key = "#productType + '_' + #productId") public PriceProductConfig getProductConfig(String productType, String productId) { PriceProductConfig config = productConfigMapper.selectByProductTypeAndId(productType, productId); if (config == null) { @@ -41,38 +41,59 @@ public class ProductConfigServiceImpl implements IProductConfigService { } @Override - @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + (#productSubType ?: 'default') + '_' + #quantity") - public PriceTierConfig getTierConfig(String productType, String productId, String productSubType, Integer quantity) { - PriceTierConfig config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, productSubType, quantity); +// @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + #quantity") + public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity) { + PriceTierConfig config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity); if (config == null) { - log.warn("阶梯定价配置未找到: productType={}, productId={}, productSubType={}, quantity={}", - productType, productId, productSubType, quantity); + log.warn("阶梯定价配置未找到: productType={}, productId={}, quantity={}", + productType, productId, quantity); } return config; } @Override - @Deprecated - public PriceTierConfig getTierConfig(String productType, String productSubType, Integer quantity) { - // 兼容旧接口,使用默认productId - return getTierConfig(productType, "default", productSubType, quantity); - } - - @Override - @Cacheable(value = "active-product-configs") +// @Cacheable(value = "active-product-configs") public List getActiveProductConfigs() { return productConfigMapper.selectActiveConfigs(); } @Override - @Cacheable(value = "tier-configs", key = "#productType") +// @Cacheable(value = "tier-configs", key = "#productType") public List getTierConfigs(String productType) { return tierConfigMapper.selectByProductType(productType); } @Override - @Cacheable(value = "tier-configs", key = "#productType + '_' + #productId") +// @Cacheable(value = "tier-configs", key = "#productType + '_' + #productId") public List getTierConfigs(String productType, String productId) { return tierConfigMapper.selectByProductTypeAndId(productType, productId); } + + @Override +// @Cacheable(value = "all-tier-configs") + public List getAllTierConfigs() { + return tierConfigMapper.selectAllActiveConfigs(); + } + + // ==================== 管理端接口(包含禁用的配置) ==================== + + @Override + public List getAllProductConfigsForAdmin() { + return productConfigMapper.selectAllConfigsForAdmin(); + } + + @Override + public List getAllTierConfigsForAdmin() { + return tierConfigMapper.selectAllConfigsForAdmin(); + } + + @Override + public List getTierConfigsForAdmin(String productType) { + return tierConfigMapper.selectByProductTypeForAdmin(productType); + } + + @Override + public List getTierConfigsForAdmin(String productType, String productId) { + return tierConfigMapper.selectByProductTypeAndIdForAdmin(productType, productId); + } } \ No newline at end of file From 688459d2dacccb4f3c3da428831eb5ba1523c8c1 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 15 Aug 2025 14:54:31 +0800 Subject: [PATCH 03/18] =?UTF-8?q?feat(pricing):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增多个查询接口,包括商品配置、阶梯配置和一口价配置的查询- 优化配置管理逻辑,支持 default 配置的创建和使用 - 重构部分代码,提高可维护性和可扩展性 --- .../controller/PricingConfigController.java | 65 +++++++++++++++++++ .../basic/pricing/dto/BundleProductItem.java | 30 +++++++++ .../ycwl/basic/pricing/dto/ProductItem.java | 7 +- .../pricing/entity/PriceBundleConfig.java | 14 ++-- .../mapper/PriceBundleConfigMapper.java | 4 +- .../mapper/PriceProductConfigMapper.java | 6 ++ .../pricing/mapper/PriceTierConfigMapper.java | 6 ++ .../pricing/service/IPriceBundleService.java | 7 ++ .../service/IProductConfigService.java | 7 ++ .../service/impl/PriceBundleServiceImpl.java | 31 ++++++--- .../impl/PriceCalculationServiceImpl.java | 62 ++++++++++++++---- .../impl/PricingManagementServiceImpl.java | 15 +++++ .../impl/ProductConfigServiceImpl.java | 18 +++++ 13 files changed, 236 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/BundleProductItem.java diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java index 35d9d6b..74125f0 100644 --- a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java @@ -27,6 +27,71 @@ public class PricingConfigController { private final IPricingManagementService managementService; + // ==================== 查询API ==================== + + /** + * 获取所有商品配置 + */ + @GetMapping("/products") + public ApiResponse> getAllProductConfigs() { + log.info("获取所有商品配置"); + List configs = productConfigService.getAllProductConfigs(); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型获取阶梯配置 + */ + @GetMapping("/tiers/{productType}") + public ApiResponse> getTierConfigs(@PathVariable String productType) { + log.info("根据商品类型获取阶梯配置: {}", productType); + List configs = productConfigService.getTierConfigs(productType); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型和商品ID获取阶梯配置 + */ + @GetMapping("/tiers/{productType}/{productId}") + public ApiResponse> getTierConfigs(@PathVariable String productType, + @PathVariable String productId) { + log.info("根据商品类型和ID获取阶梯配置: {}, {}", productType, productId); + List configs = productConfigService.getTierConfigs(productType, productId); + return ApiResponse.success(configs); + } + + /** + * 根据商品类型和商品ID获取具体配置 + */ + @GetMapping("/products/{productType}/{productId}") + public ApiResponse getProductConfig(@PathVariable String productType, + @PathVariable String productId) { + log.info("根据商品类型和ID获取商品配置: {}, {}", productType, productId); + PriceProductConfig config = productConfigService.getProductConfig(productType, productId); + return ApiResponse.success(config); + } + + /** + * 获取所有阶梯配置 + */ + @GetMapping("/tiers") + public ApiResponse> getAllTierConfigs() { + log.info("获取所有阶梯配置"); + List configs = productConfigService.getAllTierConfigs(); + return ApiResponse.success(configs); + } + + /** + * 获取所有一口价配置 + */ + @GetMapping("/bundles") + public ApiResponse> getAllBundleConfigs() { + log.info("获取所有一口价配置"); + List configs = bundleService.getAllBundles(); + return ApiResponse.success(configs); + } + + // ==================== 配置管理API(手动处理时间) ==================== /** diff --git a/src/main/java/com/ycwl/basic/pricing/dto/BundleProductItem.java b/src/main/java/com/ycwl/basic/pricing/dto/BundleProductItem.java new file mode 100644 index 0000000..fe5a9fe --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/BundleProductItem.java @@ -0,0 +1,30 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +/** + * 一口价套餐商品项DTO + */ +@Data +public class BundleProductItem { + + /** + * 商品类型 + */ + private String type; + + /** + * 商品子类型(可选) + */ + private String subType; + + /** + * 商品ID(可选) + */ + private String productId; + + /** + * 数量 + */ + private Integer quantity; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java index 6ce3d6c..5bb0444 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java @@ -20,12 +20,7 @@ public class ProductItem { * 具体商品ID:vlog视频为具体视频ID,录像集/照相集为景区ID,打印为景区ID */ private String productId; - - /** - * 商品子类型 - */ - private String productSubType; - + /** * 数量(如原片数量、照片数量等) */ diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java index 0d1ec9f..9be8864 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java @@ -1,10 +1,14 @@ package com.ycwl.basic.pricing.entity; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.baomidou.mybatisplus.annotation.TableField; +import com.ycwl.basic.pricing.dto.BundleProductItem; import lombok.Data; import lombok.EqualsAndHashCode; import java.math.BigDecimal; +import java.util.List; /** * 一口价配置实体 @@ -30,14 +34,16 @@ public class PriceBundleConfig extends BaseEntity { private BigDecimal bundlePrice; /** - * 包含商品(JSON) + * 包含商品 */ - private String includedProducts; + @TableField(typeHandler = JacksonTypeHandler.class) + private List includedProducts; /** - * 排除商品(JSON) + * 排除商品 */ - private String excludedProducts; + @TableField(typeHandler = JacksonTypeHandler.class) + private List excludedProducts; /** * 套餐描述 diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java index 08a7199..a0dfaf8 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java @@ -41,7 +41,7 @@ public interface PriceBundleConfigMapper extends BaseMapper { */ @Insert("INSERT INTO price_bundle_config (bundle_name, scenic_id, bundle_price, included_products, excluded_products, " + "description, is_active, created_time, updated_time) VALUES " + - "(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts}, #{excludedProducts}, " + + "(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, #{excludedProducts,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, " + "#{description}, #{isActive}, NOW(), NOW())") int insertBundleConfig(PriceBundleConfig config); @@ -49,7 +49,7 @@ public interface PriceBundleConfigMapper extends BaseMapper { * 更新一口价配置 */ @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, scenic_id = #{scenicId}, bundle_price = #{bundlePrice}, " + - "included_products = #{includedProducts}, excluded_products = #{excludedProducts}, " + + "included_products = #{includedProducts,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, excluded_products = #{excludedProducts,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, " + "description = #{description}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") int updateBundleConfig(PriceBundleConfig config); 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 0f77fcb..d200921 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java @@ -34,6 +34,12 @@ 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); + /** + * 检查是否存在default配置(包含禁用的) + */ + @Select("SELECT COUNT(*) FROM price_product_config WHERE product_type = #{productType} AND product_id = 'default'") + int countDefaultConfigsByProductType(@Param("productType") String productType); + // ==================== 管理端接口(包含禁用的配置) ==================== /** 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 5421f65..8921078 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java @@ -49,6 +49,12 @@ public interface PriceTierConfigMapper extends BaseMapper { @Select("SELECT * FROM price_tier_config WHERE is_active = 1 ORDER BY product_type ASC, sort_order ASC") List selectAllActiveConfigs(); + /** + * 检查是否存在default阶梯配置(包含禁用的) + */ + @Select("SELECT COUNT(*) FROM price_tier_config WHERE product_type = #{productType} AND product_id = 'default'") + int countDefaultTierConfigsByProductType(@Param("productType") String productType); + // ==================== 管理端接口(包含禁用的配置) ==================== /** diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java index 2e66341..7ff94e7 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IPriceBundleService.java @@ -34,6 +34,13 @@ public interface IPriceBundleService { */ List getActiveBundles(); + /** + * 获取所有一口价配置(仅启用的) + * + * @return 一口价配置列表 + */ + List getAllBundles(); + // ==================== 管理端接口(包含禁用的配置) ==================== /** 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 11a31b8..c1d27cb 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IProductConfigService.java @@ -44,6 +44,13 @@ public interface IProductConfigService { */ List getActiveProductConfigs(); + /** + * 获取所有商品配置(仅启用的) + * + * @return 商品配置列表 + */ + List getAllProductConfigs(); + /** * 根据商品类型获取所有阶梯配置 * diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java index f0ba3eb..5101323 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceBundleServiceImpl.java @@ -2,6 +2,7 @@ package com.ycwl.basic.pricing.service.impl; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.BundleProductItem; import com.ycwl.basic.pricing.dto.ProductItem; import com.ycwl.basic.pricing.entity.PriceBundleConfig; import com.ycwl.basic.pricing.enums.ProductType; @@ -76,6 +77,12 @@ public class PriceBundleServiceImpl implements IPriceBundleService { return bundleConfigMapper.selectActiveBundles(); } + @Override +// @Cacheable(value = "all-bundles") + public List getAllBundles() { + return bundleConfigMapper.selectActiveBundles(); + } + // ==================== 管理端接口(包含禁用的配置) ==================== @Override @@ -85,23 +92,27 @@ public class PriceBundleServiceImpl implements IPriceBundleService { private boolean isProductsMatchBundle(Set productTypes, PriceBundleConfig bundle) { try { - List includedProducts = objectMapper.readValue( - bundle.getIncludedProducts(), new TypeReference>() {}); - - Set requiredProducts = new HashSet<>(includedProducts); + // 检查包含的商品 + if (bundle.getIncludedProducts() != null && !bundle.getIncludedProducts().isEmpty()) { + Set requiredProducts = new HashSet<>(); + for (BundleProductItem item : bundle.getIncludedProducts()) { + requiredProducts.add(item.getType()); + } + if (!productTypes.containsAll(requiredProducts)) { + return false; + } + } + // 检查排除的商品 if (bundle.getExcludedProducts() != null && !bundle.getExcludedProducts().isEmpty()) { - List excludedProducts = objectMapper.readValue( - bundle.getExcludedProducts(), new TypeReference>() {}); - - for (String excludedProduct : excludedProducts) { - if (productTypes.contains(excludedProduct)) { + for (BundleProductItem item : bundle.getExcludedProducts()) { + if (productTypes.contains(item.getType())) { return false; } } } - return productTypes.containsAll(requiredProducts); + return true; } catch (Exception e) { log.error("解析一口价配置失败: bundleId={}", bundle.getId(), 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 1cf1490..59795ee 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 @@ -163,7 +163,21 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { productType, productId); } - // 兜底:使用通用配置(向后兼容) + // 兜底:使用default配置 + try { + PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default"); + if (defaultConfig != null) { + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + return defaultConfig.getBasePrice().multiply(BigDecimal.valueOf(product.getQuantity())); + } else { + return defaultConfig.getBasePrice(); + } + } + } catch (Exception e) { + log.warn("未找到default配置: productType={}", productType.getCode()); + } + + // 最后兜底:使用通用配置(向后兼容) List configs = productConfigService.getProductConfig(productType.getCode()); if (!configs.isEmpty()) { PriceProductConfig baseConfig = configs.get(0); // 使用第一个配置作为默认 @@ -214,21 +228,41 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { log.warn("未找到具体商品配置: productType={}, productId={}, 尝试使用通用配置", productType, productId); - // 兜底:使用通用配置(向后兼容) - List configs = productConfigService.getProductConfig(productType.getCode()); - if (!configs.isEmpty()) { - PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认 - actualPrice = baseConfig.getBasePrice(); - originalPrice = baseConfig.getOriginalPrice(); - - if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { - actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); - if (originalPrice != null) { - originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + // 兜底:使用default配置 + try { + PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default"); + if (defaultConfig != null) { + actualPrice = defaultConfig.getBasePrice(); + originalPrice = defaultConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } } + } else { + throw new PriceCalculationException("无法找到default配置"); + } + } catch (Exception defaultEx) { + log.warn("未找到default配置: productType={}", productType.getCode()); + + // 最后兜底:使用通用配置(向后兼容) + List configs = productConfigService.getProductConfig(productType.getCode()); + if (!configs.isEmpty()) { + PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认 + actualPrice = baseConfig.getBasePrice(); + originalPrice = baseConfig.getOriginalPrice(); + + if (productType == ProductType.PHOTO_PRINT || productType == ProductType.MACHINE_PRINT) { + actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } else { + throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); } - } else { - throw new PriceCalculationException("无法计算商品价格: " + productType.getDescription() + ", productId: " + productId); } } } diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java index 0a70889..12bfec9 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java @@ -27,6 +27,14 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public Long createProductConfig(PriceProductConfig config) { + // 校验:如果是default配置,确保该商品类型只能有一个default配置 + if ("default".equals(config.getProductId())) { + int existingCount = productConfigMapper.countDefaultConfigsByProductType(config.getProductType()); + if (existingCount > 0) { + throw new IllegalArgumentException("商品类型 " + config.getProductType() + " 的default配置已存在,每种商品类型只能有一个default配置"); + } + } + config.setCreatedTime(LocalDateTime.now()); config.setUpdatedTime(LocalDateTime.now()); productConfigMapper.insertProductConfig(config); @@ -43,6 +51,13 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public Long createTierConfig(PriceTierConfig config) { + // 校验:如果是default配置,检查是否可以创建 + if ("default".equals(config.getProductId())) { + // 对于阶梯配置,可以有多个default配置(不同数量区间),不需要限制 + log.info("创建default阶梯配置: productType={}, quantity range: {}-{}", + config.getProductType(), config.getMinQuantity(), config.getMaxQuantity()); + } + config.setCreatedTime(LocalDateTime.now()); config.setUpdatedTime(LocalDateTime.now()); tierConfigMapper.insertTierConfig(config); 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 9103e2b..86fb376 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 @@ -44,6 +44,18 @@ public class ProductConfigServiceImpl implements IProductConfigService { // @Cacheable(value = "tier-config", key = "#productType + '_' + #productId + '_' + #quantity") public PriceTierConfig getTierConfig(String productType, String productId, Integer quantity) { PriceTierConfig config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity); + + // 如果没有找到特定商品的阶梯配置,尝试使用default配置 + if (config == null && !"default".equals(productId)) { + log.warn("阶梯定价配置未找到: productType={}, productId={}, quantity={}, 尝试使用default配置", + productType, productId, quantity); + config = tierConfigMapper.selectByProductTypeAndQuantity(productType, "default", quantity); + if (config != null) { + log.debug("使用default阶梯配置: productType={}, quantity={}, price={}", + productType, quantity, config.getPrice()); + } + } + if (config == null) { log.warn("阶梯定价配置未找到: productType={}, productId={}, quantity={}", productType, productId, quantity); @@ -57,6 +69,12 @@ public class ProductConfigServiceImpl implements IProductConfigService { return productConfigMapper.selectActiveConfigs(); } + @Override +// @Cacheable(value = "all-product-configs") + public List getAllProductConfigs() { + return productConfigMapper.selectActiveConfigs(); + } + @Override // @Cacheable(value = "tier-configs", key = "#productType") public List getTierConfigs(String productType) { From 16e07ee9ef3e2339e579527345808932f6d9a7d9 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 15 Aug 2025 15:28:43 +0800 Subject: [PATCH 04/18] =?UTF-8?q?feat(pricing):=20=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=A4=84=E7=90=86=20BundleProductItem=20=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用自定义的 BundleProductListTypeHandler 替代 JacksonTypeHandler - 在 PriceBundleConfigMapper 中添加 @Results 注解,指定自定义处理器 - 更新 insert 和 update 方法,使用新的 BundleProductListTypeHandler --- .../pricing/entity/PriceBundleConfig.java | 7 +-- .../mapper/PriceBundleConfigMapper.java | 43 ++++++++++++++----- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java index 9be8864..bd2e705 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java @@ -1,11 +1,12 @@ package com.ycwl.basic.pricing.entity; import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.annotation.TableField; import com.ycwl.basic.pricing.dto.BundleProductItem; +import com.ycwl.basic.pricing.handler.BundleProductListTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; +import org.apache.ibatis.type.JdbcType; import java.math.BigDecimal; import java.util.List; @@ -36,13 +37,13 @@ public class PriceBundleConfig extends BaseEntity { /** * 包含商品 */ - @TableField(typeHandler = JacksonTypeHandler.class) + @TableField(typeHandler = BundleProductListTypeHandler.class, jdbcType = JdbcType.VARCHAR) private List includedProducts; /** * 排除商品 */ - @TableField(typeHandler = JacksonTypeHandler.class) + @TableField(typeHandler = BundleProductListTypeHandler.class, jdbcType = JdbcType.VARCHAR) private List excludedProducts; /** diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java index a0dfaf8..0bc1ce3 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java @@ -2,11 +2,7 @@ package com.ycwl.basic.pricing.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ycwl.basic.pricing.entity.PriceBundleConfig; -import org.apache.ibatis.annotations.Insert; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; -import org.apache.ibatis.annotations.Update; +import org.apache.ibatis.annotations.*; import java.util.List; @@ -19,13 +15,31 @@ public interface PriceBundleConfigMapper extends BaseMapper { /** * 查询启用的一口价配置 */ - @Select("SELECT * FROM price_bundle_config WHERE is_active = 1") + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + + "included_products, excluded_products, " + + "description, is_active, created_time, updated_time " + + "FROM price_bundle_config WHERE is_active = 1") + @Results({ + @Result(column = "included_products", property = "includedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class), + @Result(column = "excluded_products", property = "excludedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class) + }) List selectActiveBundles(); /** * 根据ID查询启用的配置 */ - @Select("SELECT * FROM price_bundle_config WHERE id = #{id} AND is_active = 1") + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + + "included_products, excluded_products, " + + "description, is_active, created_time, updated_time " + + "FROM price_bundle_config WHERE id = #{id} AND is_active = 1") + @Results({ + @Result(column = "included_products", property = "includedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class), + @Result(column = "excluded_products", property = "excludedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class) + }) PriceBundleConfig selectActiveBundleById(Long id); // ==================== 管理端接口(包含禁用的配置) ==================== @@ -33,7 +47,16 @@ public interface PriceBundleConfigMapper extends BaseMapper { /** * 查询所有一口价配置(包含禁用的)- 管理端使用 */ - @Select("SELECT * FROM price_bundle_config ORDER BY is_active DESC, bundle_name ASC") + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + + "included_products, excluded_products, " + + "description, is_active, created_time, updated_time " + + "FROM price_bundle_config ORDER BY is_active DESC, bundle_name ASC") + @Results({ + @Result(column = "included_products", property = "includedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class), + @Result(column = "excluded_products", property = "excludedProducts", + typeHandler = com.ycwl.basic.pricing.handler.BundleProductListTypeHandler.class) + }) List selectAllBundlesForAdmin(); /** @@ -41,7 +64,7 @@ public interface PriceBundleConfigMapper extends BaseMapper { */ @Insert("INSERT INTO price_bundle_config (bundle_name, scenic_id, bundle_price, included_products, excluded_products, " + "description, is_active, created_time, updated_time) VALUES " + - "(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, #{excludedProducts,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, " + + "(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " + "#{description}, #{isActive}, NOW(), NOW())") int insertBundleConfig(PriceBundleConfig config); @@ -49,7 +72,7 @@ public interface PriceBundleConfigMapper extends BaseMapper { * 更新一口价配置 */ @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, scenic_id = #{scenicId}, bundle_price = #{bundlePrice}, " + - "included_products = #{includedProducts,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, excluded_products = #{excludedProducts,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, " + + "included_products = #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, excluded_products = #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " + "description = #{description}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") int updateBundleConfig(PriceBundleConfig config); From 4787efd32827b783be7f958ede321894d0943e91 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 18 Aug 2025 04:33:58 +0800 Subject: [PATCH 05/18] =?UTF-8?q?feat(pricing):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=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 - 新增 CouponManagementController 控制器,实现优惠券配置和领取记录的管理 - 新增 ICouponManagementService 接口和 CouponManagementServiceImpl 实现类,提供优惠券管理服务 - 在 PricingConfigController 中添加获取所有优惠券配置和领取记录的接口 - 新增 BundleProductListTypeHandler 类,用于处理一口价商品列表的序列化和反序列化 - 更新 PriceCouponClaimRecordMapper 和 PriceCouponConfigMapper,添加管理端所需的查询接口 --- .../CouponManagementController.java | 220 ++++++++++++++++ .../controller/PricingConfigController.java | 24 ++ .../handler/BundleProductListTypeHandler.java | 72 ++++++ .../mapper/PriceCouponClaimRecordMapper.java | 112 +++++++++ .../mapper/PriceCouponConfigMapper.java | 44 ++++ .../service/ICouponManagementService.java | 107 ++++++++ .../impl/CouponManagementServiceImpl.java | 234 ++++++++++++++++++ 7 files changed, 813 insertions(+) create mode 100644 src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java create mode 100644 src/main/java/com/ycwl/basic/pricing/handler/BundleProductListTypeHandler.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java diff --git a/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java b/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java new file mode 100644 index 0000000..46d66ca --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java @@ -0,0 +1,220 @@ +package com.ycwl.basic.pricing.controller; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; +import com.ycwl.basic.pricing.service.ICouponManagementService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 优惠券管理控制器(管理端) + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing/admin/coupons") +@RequiredArgsConstructor +public class CouponManagementController { + + private final ICouponManagementService couponManagementService; + + // ==================== 优惠券配置管理 ==================== + + /** + * 创建优惠券配置 + */ + @PostMapping("/configs") + public ApiResponse createCouponConfig(@RequestBody PriceCouponConfig config) { + log.info("创建优惠券配置: {}", config.getCouponName()); + Long id = couponManagementService.createCouponConfig(config); + return ApiResponse.success(id); + } + + /** + * 更新优惠券配置 + */ + @PutMapping("/configs/{id}") + public ApiResponse updateCouponConfig(@PathVariable Long id, @RequestBody PriceCouponConfig config) { + log.info("更新优惠券配置: id={}, name={}", id, config.getCouponName()); + config.setId(id); + boolean success = couponManagementService.updateCouponConfig(config); + return ApiResponse.success(success); + } + + /** + * 删除优惠券配置 + */ + @DeleteMapping("/configs/{id}") + public ApiResponse deleteCouponConfig(@PathVariable Long id) { + log.info("删除优惠券配置: id={}", id); + boolean success = couponManagementService.deleteCouponConfig(id); + return ApiResponse.success(success); + } + + /** + * 启用/禁用优惠券配置 + */ + @PutMapping("/configs/{id}/status") + public ApiResponse updateCouponConfigStatus(@PathVariable Long id, @RequestParam Boolean isActive) { + log.info("修改优惠券配置状态: id={}, isActive={}", id, isActive); + boolean success = couponManagementService.updateCouponConfigStatus(id, isActive); + return ApiResponse.success(success); + } + + /** + * 查询所有优惠券配置(包含禁用的) + */ + @GetMapping("/configs") + public ApiResponse> getAllCouponConfigs() { + log.info("管理端获取所有优惠券配置"); + List configs = couponManagementService.getAllCouponConfigs(); + return ApiResponse.success(configs); + } + + /** + * 分页查询优惠券配置 + */ + @GetMapping("/configs/page") + public ApiResponse> getCouponConfigsPage( + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Boolean isActive, + @RequestParam(required = false) String couponName) { + log.info("分页查询优惠券配置: pageNum={}, pageSize={}, isActive={}, couponName={}", + pageNum, pageSize, isActive, couponName); + PageInfo pageInfo = couponManagementService.getCouponConfigsPage( + pageNum, pageSize, isActive, couponName); + return ApiResponse.success(pageInfo); + } + + /** + * 根据状态查询优惠券配置 + */ + @GetMapping("/configs/status/{isActive}") + public ApiResponse> getCouponConfigsByStatus(@PathVariable Boolean isActive) { + log.info("根据状态查询优惠券配置: {}", isActive); + List configs = couponManagementService.getCouponConfigsByStatus(isActive); + return ApiResponse.success(configs); + } + + /** + * 根据ID查询优惠券配置 + */ + @GetMapping("/configs/{id}") + public ApiResponse getCouponConfigById(@PathVariable Long id) { + log.info("根据ID查询优惠券配置: {}", id); + PriceCouponConfig config = couponManagementService.getCouponConfigById(id); + return ApiResponse.success(config); + } + + // ==================== 优惠券领取记录查询 ==================== + + /** + * 查询所有优惠券领取记录 + */ + @GetMapping("/claim-records") + public ApiResponse> getAllClaimRecords() { + log.info("查询所有优惠券领取记录"); + List records = couponManagementService.getAllClaimRecords(); + return ApiResponse.success(records); + } + + /** + * 分页查询优惠券领取记录 + */ + @GetMapping("/claim-records/page") + public ApiResponse> getClaimRecordsPage( + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Long userId, + @RequestParam(required = false) Long couponId, + @RequestParam(required = false) CouponStatus status, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime) { + log.info("分页查询优惠券领取记录: pageNum={}, pageSize={}, userId={}, couponId={}, status={}, startTime={}, endTime={}", + pageNum, pageSize, userId, couponId, status, startTime, endTime); + PageInfo pageInfo = couponManagementService.getClaimRecordsPage( + pageNum, pageSize, userId, couponId, status, startTime, endTime); + return ApiResponse.success(pageInfo); + } + + /** + * 根据用户ID查询优惠券领取记录 + */ + @GetMapping("/claim-records/user/{userId}") + public ApiResponse> getClaimRecordsByUserId(@PathVariable Long userId) { + log.info("根据用户ID查询优惠券领取记录: {}", userId); + List records = couponManagementService.getClaimRecordsByUserId(userId); + return ApiResponse.success(records); + } + + /** + * 根据优惠券ID查询领取记录 + */ + @GetMapping("/claim-records/coupon/{couponId}") + public ApiResponse> getClaimRecordsByCouponId(@PathVariable Long couponId) { + log.info("根据优惠券ID查询领取记录: {}", couponId); + List records = couponManagementService.getClaimRecordsByCouponId(couponId); + return ApiResponse.success(records); + } + + /** + * 根据状态查询领取记录 + */ + @GetMapping("/claim-records/status/{status}") + public ApiResponse> getClaimRecordsByStatus(@PathVariable CouponStatus status) { + log.info("根据状态查询领取记录: {}", status); + List records = couponManagementService.getClaimRecordsByStatus(status); + return ApiResponse.success(records); + } + + // ==================== 统计功能 ==================== + + /** + * 查询优惠券使用统计 + */ + @GetMapping("/stats/{couponId}") + public ApiResponse> getCouponUsageStats(@PathVariable Long couponId) { + log.info("查询优惠券使用统计: {}", couponId); + Map stats = couponManagementService.getCouponUsageStats(couponId); + return ApiResponse.success(stats); + } + + /** + * 查询优惠券详细统计 + */ + @GetMapping("/stats/{couponId}/detail") + public ApiResponse> getCouponDetailStats(@PathVariable Long couponId) { + log.info("查询优惠券详细统计: {}", couponId); + Map stats = couponManagementService.getCouponDetailStats(couponId); + return ApiResponse.success(stats); + } + + /** + * 查询时间范围内的统计数据 + */ + @GetMapping("/stats/period") + public ApiResponse> getPeriodStats( + @RequestParam String startDate, + @RequestParam String endDate) { + log.info("查询时间范围统计: startDate={}, endDate={}", startDate, endDate); + Map stats = couponManagementService.getPeriodStats(startDate, endDate); + return ApiResponse.success(stats); + } + + /** + * 查询所有优惠券的使用统计概览 + */ + @GetMapping("/stats/overview") + public ApiResponse>> getAllCouponUsageOverview() { + log.info("查询所有优惠券使用统计概览"); + List> overview = couponManagementService.getAllCouponUsageOverview(); + return ApiResponse.success(overview); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java index 74125f0..63002a1 100644 --- a/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/PricingConfigController.java @@ -4,9 +4,12 @@ import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.pricing.entity.PriceProductConfig; import com.ycwl.basic.pricing.entity.PriceTierConfig; import com.ycwl.basic.pricing.entity.PriceBundleConfig; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; import com.ycwl.basic.pricing.service.IProductConfigService; import com.ycwl.basic.pricing.service.IPriceBundleService; import com.ycwl.basic.pricing.service.IPricingManagementService; +import com.ycwl.basic.pricing.service.ICouponManagementService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -25,6 +28,7 @@ public class PricingConfigController { private final IProductConfigService productConfigService; private final IPriceBundleService bundleService; private final IPricingManagementService managementService; + private final ICouponManagementService couponManagementService; // ==================== 查询API ==================== @@ -273,4 +277,24 @@ public class PricingConfigController { List configs = bundleService.getAllBundlesForAdmin(); return ApiResponse.success(configs); } + + /** + * 管理端:获取所有优惠券配置(包含禁用的) + */ + @GetMapping("/admin/coupons") + public ApiResponse> getAllCouponConfigsForAdmin() { + log.info("管理端获取所有优惠券配置"); + List configs = couponManagementService.getAllCouponConfigs(); + return ApiResponse.success(configs); + } + + /** + * 管理端:获取所有优惠券领取记录 + */ + @GetMapping("/admin/coupon-records") + public ApiResponse> getAllCouponClaimRecordsForAdmin() { + log.info("管理端获取所有优惠券领取记录"); + List records = couponManagementService.getAllClaimRecords(); + return ApiResponse.success(records); + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/handler/BundleProductListTypeHandler.java b/src/main/java/com/ycwl/basic/pricing/handler/BundleProductListTypeHandler.java new file mode 100644 index 0000000..d6d9c8b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/handler/BundleProductListTypeHandler.java @@ -0,0 +1,72 @@ +package com.ycwl.basic.pricing.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.BundleProductItem; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * 一口价商品列表类型处理器 + */ +@Slf4j +public class BundleProductListTypeHandler extends BaseTypeHandler> { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final TypeReference> typeReference = new TypeReference>() {}; + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) throws SQLException { + try { + String json = objectMapper.writeValueAsString(parameter); + ps.setString(i, json); + log.debug("序列化商品列表: {}", json); + } catch (JsonProcessingException e) { + log.error("序列化商品列表失败", e); + throw new SQLException("序列化商品列表失败", e); + } + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + String json = rs.getString(columnName); + return parseJson(json, columnName); + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String json = rs.getString(columnIndex); + return parseJson(json, "columnIndex:" + columnIndex); + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String json = cs.getString(columnIndex); + return parseJson(json, "columnIndex:" + columnIndex); + } + + private List parseJson(String json, String source) { + if (json == null || json.trim().isEmpty()) { + log.debug("从{}获取的JSON为空,返回空列表", source); + return new ArrayList<>(); + } + + try { + List result = objectMapper.readValue(json, typeReference); + log.debug("从{}反序列化商品列表成功,数量: {}", source, result != null ? result.size() : 0); + return result != null ? result : new ArrayList<>(); + } catch (JsonProcessingException e) { + log.error("从{}反序列化商品列表失败,JSON: {}", source, json, e); + return new ArrayList<>(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java index 87ef06d..9c0dfd4 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java @@ -60,4 +60,116 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper selectAllForAdmin(); + + /** + * 管理端:根据条件查询优惠券领取记录(支持分页) + */ + @Select("") + List selectByConditionsForAdmin(@Param("userId") Long userId, + @Param("couponId") Long couponId, + @Param("status") CouponStatus status, + @Param("startTime") String startTime, + @Param("endTime") String endTime); + + /** + * 管理端:根据用户ID查询优惠券领取记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.user_id = #{userId} " + + "ORDER BY r.created_time DESC") + List selectByUserIdForAdmin(@Param("userId") Long userId); + + /** + * 管理端:根据优惠券ID查询领取记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.coupon_id = #{couponId} " + + "ORDER BY r.created_time DESC") + List selectByCouponIdForAdmin(@Param("couponId") Long couponId); + + /** + * 管理端:根据状态查询领取记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.status = #{status} " + + "ORDER BY r.created_time DESC") + List selectByStatusForAdmin(@Param("status") CouponStatus status); + + /** + * 管理端:统计优惠券使用情况 + */ + @Select("SELECT " + + "COUNT(*) as total_claimed, " + + "COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " + + "COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available " + + "FROM price_coupon_claim_record WHERE coupon_id = #{couponId}") + java.util.Map selectCouponUsageStats(@Param("couponId") Long couponId); + + /** + * 管理端:统计优惠券详细使用情况 + */ + @Select("SELECT " + + "COUNT(*) as total_claimed, " + + "COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " + + "COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " + + "CASE WHEN COUNT(*) > 0 THEN COUNT(CASE WHEN status = 'USED' THEN 1 END) / COUNT(*) ELSE 0 END as usage_rate, " + + "CASE WHEN COUNT(CASE WHEN status = 'USED' THEN 1 END) > 0 THEN " + + "AVG(CASE WHEN status = 'USED' AND use_time IS NOT NULL AND claim_time IS NOT NULL THEN " + + "DATEDIFF(use_time, claim_time) END) ELSE 0 END as avg_days_to_use " + + "FROM price_coupon_claim_record WHERE coupon_id = #{couponId}") + java.util.Map selectCouponDetailStats(@Param("couponId") Long couponId); + + /** + * 管理端:统计时间范围内的数据 + */ + @Select("SELECT " + + "COUNT(*) as total_claimed, " + + "COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " + + "COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " + + "COUNT(CASE WHEN status = 'EXPIRED' THEN 1 END) as total_expired, " + + "COUNT(DISTINCT coupon_id) as total_coupon_types, " + + "COUNT(DISTINCT user_id) as total_users " + + "FROM price_coupon_claim_record " + + "WHERE claim_time >= #{startDate} AND claim_time <= #{endDate}") + java.util.Map selectPeriodStats(@Param("startDate") String startDate, + @Param("endDate") String endDate); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java index 869a4b6..c44585d 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java @@ -59,4 +59,48 @@ public interface PriceCouponConfigMapper extends BaseMapper { "valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " + "updated_time = NOW() WHERE id = #{id}") int updateCoupon(PriceCouponConfig coupon); + + // ==================== 管理端接口 ==================== + + /** + * 管理端:查询所有优惠券配置(包含禁用的) + */ + @Select("SELECT * FROM price_coupon_config ORDER BY created_time DESC") + List selectAllForAdmin(); + + /** + * 管理端:根据条件查询优惠券配置(支持分页) + */ + @Select("") + List selectByConditionsForAdmin(@Param("isActive") Boolean isActive, + @Param("couponName") String couponName); + + /** + * 管理端:根据状态查询优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE is_active = #{isActive} ORDER BY created_time DESC") + List selectByStatusForAdmin(@Param("isActive") Boolean isActive); + + /** + * 管理端:更新优惠券状态 + */ + @Update("UPDATE price_coupon_config SET is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + int updateCouponStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); + + /** + * 管理端:删除优惠券配置 + */ + @Update("UPDATE price_coupon_config SET deleted = 1, updated_time = NOW() WHERE id = #{id}") + int deleteCoupon(Long id); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java b/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java new file mode 100644 index 0000000..61b6583 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java @@ -0,0 +1,107 @@ +package com.ycwl.basic.pricing.service; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; + +import java.util.List; +import java.util.Map; + +/** + * 优惠券管理服务接口(管理端) + */ +public interface ICouponManagementService { + + // ==================== 优惠券配置管理 ==================== + + /** + * 创建优惠券配置 + */ + Long createCouponConfig(PriceCouponConfig config); + + /** + * 更新优惠券配置 + */ + boolean updateCouponConfig(PriceCouponConfig config); + + /** + * 删除优惠券配置 + */ + boolean deleteCouponConfig(Long id); + + /** + * 启用/禁用优惠券配置 + */ + boolean updateCouponConfigStatus(Long id, Boolean isActive); + + /** + * 查询所有优惠券配置(包含禁用的) + */ + List getAllCouponConfigs(); + + /** + * 分页查询优惠券配置 + */ + PageInfo getCouponConfigsPage(Integer pageNum, Integer pageSize, + Boolean isActive, String couponName); + + /** + * 根据状态查询优惠券配置 + */ + List getCouponConfigsByStatus(Boolean isActive); + + /** + * 根据ID查询优惠券配置 + */ + PriceCouponConfig getCouponConfigById(Long id); + + // ==================== 优惠券领取记录查询 ==================== + + /** + * 查询所有优惠券领取记录 + */ + List getAllClaimRecords(); + + /** + * 分页查询优惠券领取记录 + */ + PageInfo getClaimRecordsPage(Integer pageNum, Integer pageSize, + Long userId, Long couponId, CouponStatus status, + String startTime, String endTime); + + /** + * 根据用户ID查询优惠券领取记录 + */ + List getClaimRecordsByUserId(Long userId); + + /** + * 根据优惠券ID查询领取记录 + */ + List getClaimRecordsByCouponId(Long couponId); + + /** + * 根据状态查询领取记录 + */ + List getClaimRecordsByStatus(CouponStatus status); + + /** + * 查询优惠券使用统计 + */ + Map getCouponUsageStats(Long couponId); + + /** + * 查询优惠券配置详细统计 + */ + Map getCouponDetailStats(Long couponId); + + /** + * 查询时间范围内的统计数据 + */ + Map getPeriodStats(String startDate, String endDate); + + /** + * 查询所有优惠券的使用统计概览 + */ + List> getAllCouponUsageOverview(); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java new file mode 100644 index 0000000..c7ccd42 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java @@ -0,0 +1,234 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import com.ycwl.basic.pricing.entity.PriceCouponConfig; +import com.ycwl.basic.pricing.enums.CouponStatus; +import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper; +import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper; +import com.ycwl.basic.pricing.service.ICouponManagementService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 优惠券管理服务实现(管理端) + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CouponManagementServiceImpl implements ICouponManagementService { + + private final PriceCouponConfigMapper couponConfigMapper; + private final PriceCouponClaimRecordMapper claimRecordMapper; + + // ==================== 优惠券配置管理 ==================== + + @Override + @Transactional + public Long createCouponConfig(PriceCouponConfig config) { + log.info("创建优惠券配置: {}", config.getCouponName()); + + // 设置默认值 + if (config.getUsedQuantity() == null) { + config.setUsedQuantity(0); + } + if (config.getIsActive() == null) { + config.setIsActive(true); + } + + int result = couponConfigMapper.insertCoupon(config); + if (result > 0) { + log.info("优惠券配置创建成功,ID: {}", config.getId()); + return config.getId(); + } else { + log.error("优惠券配置创建失败"); + return null; + } + } + + @Override + @Transactional + public boolean updateCouponConfig(PriceCouponConfig config) { + log.info("更新优惠券配置,ID: {}", config.getId()); + + PriceCouponConfig existing = couponConfigMapper.selectById(config.getId()); + if (existing == null) { + log.error("优惠券配置不存在,ID: {}", config.getId()); + return false; + } + + int result = couponConfigMapper.updateCoupon(config); + if (result > 0) { + log.info("优惠券配置更新成功,ID: {}", config.getId()); + return true; + } else { + log.error("优惠券配置更新失败,ID: {}", config.getId()); + return false; + } + } + + @Override + @Transactional + public boolean deleteCouponConfig(Long id) { + log.info("删除优惠券配置,ID: {}", id); + + PriceCouponConfig existing = couponConfigMapper.selectById(id); + if (existing == null) { + log.error("优惠券配置不存在,ID: {}", id); + return false; + } + + int result = couponConfigMapper.deleteCoupon(id); + if (result > 0) { + log.info("优惠券配置删除成功,ID: {}", id); + return true; + } else { + log.error("优惠券配置删除失败,ID: {}", id); + return false; + } + } + + @Override + @Transactional + public boolean updateCouponConfigStatus(Long id, Boolean isActive) { + log.info("更新优惠券配置状态,ID: {}, 状态: {}", id, isActive); + + PriceCouponConfig existing = couponConfigMapper.selectById(id); + if (existing == null) { + log.error("优惠券配置不存在,ID: {}", id); + return false; + } + + int result = couponConfigMapper.updateCouponStatus(id, isActive); + if (result > 0) { + log.info("优惠券配置状态更新成功,ID: {}", id); + return true; + } else { + log.error("优惠券配置状态更新失败,ID: {}", id); + return false; + } + } + + @Override + public List getAllCouponConfigs() { + log.info("查询所有优惠券配置"); + return couponConfigMapper.selectAllForAdmin(); + } + + @Override + public PageInfo getCouponConfigsPage(Integer pageNum, Integer pageSize, + Boolean isActive, String couponName) { + log.info("分页查询优惠券配置,页码: {}, 页大小: {}, 状态: {}, 名称: {}", + pageNum, pageSize, isActive, couponName); + + PageHelper.startPage(pageNum, pageSize); + List configs = couponConfigMapper.selectByConditionsForAdmin(isActive, couponName); + return new PageInfo<>(configs); + } + + @Override + public List getCouponConfigsByStatus(Boolean isActive) { + log.info("根据状态查询优惠券配置,状态: {}", isActive); + return couponConfigMapper.selectByStatusForAdmin(isActive); + } + + @Override + public PriceCouponConfig getCouponConfigById(Long id) { + log.info("根据ID查询优惠券配置,ID: {}", id); + return couponConfigMapper.selectById(id); + } + + // ==================== 优惠券领取记录查询 ==================== + + @Override + public List getAllClaimRecords() { + log.info("查询所有优惠券领取记录"); + return claimRecordMapper.selectAllForAdmin(); + } + + @Override + public PageInfo getClaimRecordsPage(Integer pageNum, Integer pageSize, + Long userId, Long couponId, CouponStatus status, + String startTime, String endTime) { + log.info("分页查询优惠券领取记录,页码: {}, 页大小: {}, 用户ID: {}, 优惠券ID: {}, 状态: {}, 开始时间: {}, 结束时间: {}", + pageNum, pageSize, userId, couponId, status, startTime, endTime); + + PageHelper.startPage(pageNum, pageSize); + List records = claimRecordMapper.selectByConditionsForAdmin(userId, couponId, status, startTime, endTime); + return new PageInfo<>(records); + } + + @Override + public List getClaimRecordsByUserId(Long userId) { + log.info("根据用户ID查询优惠券领取记录,用户ID: {}", userId); + return claimRecordMapper.selectByUserIdForAdmin(userId); + } + + @Override + public List getClaimRecordsByCouponId(Long couponId) { + log.info("根据优惠券ID查询领取记录,优惠券ID: {}", couponId); + return claimRecordMapper.selectByCouponIdForAdmin(couponId); + } + + @Override + public List getClaimRecordsByStatus(CouponStatus status) { + log.info("根据状态查询领取记录,状态: {}", status); + return claimRecordMapper.selectByStatusForAdmin(status); + } + + @Override + public Map getCouponUsageStats(Long couponId) { + log.info("查询优惠券使用统计,优惠券ID: {}", couponId); + return claimRecordMapper.selectCouponUsageStats(couponId); + } + + @Override + public Map getCouponDetailStats(Long couponId) { + log.info("查询优惠券详细统计,优惠券ID: {}", couponId); + return claimRecordMapper.selectCouponDetailStats(couponId); + } + + @Override + public Map getPeriodStats(String startDate, String endDate) { + log.info("查询时间范围统计,开始日期: {}, 结束日期: {}", startDate, endDate); + return claimRecordMapper.selectPeriodStats(startDate, endDate); + } + + @Override + public List> getAllCouponUsageOverview() { + log.info("查询所有优惠券使用统计概览"); + + List allCoupons = couponConfigMapper.selectAllForAdmin(); + List> overview = new ArrayList<>(); + + for (PriceCouponConfig coupon : allCoupons) { + Map stats = new HashMap<>(); + stats.put("couponId", coupon.getId()); + stats.put("couponName", coupon.getCouponName()); + stats.put("couponType", coupon.getCouponType()); + stats.put("totalQuantity", coupon.getTotalQuantity()); + stats.put("usedQuantity", coupon.getUsedQuantity()); + stats.put("remainingQuantity", coupon.getTotalQuantity() - coupon.getUsedQuantity()); + stats.put("isActive", coupon.getIsActive()); + stats.put("validFrom", coupon.getValidFrom()); + stats.put("validUntil", coupon.getValidUntil()); + + // 获取详细统计 + Map usageStats = claimRecordMapper.selectCouponUsageStats(coupon.getId()); + stats.putAll(usageStats); + + overview.add(stats); + } + + return overview; + } +} \ No newline at end of file From 9fef17bae5a3f66399766b414621d4096a19adf1 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 18 Aug 2025 04:34:13 +0800 Subject: [PATCH 06/18] =?UTF-8?q?docs(CLAUDE.md):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E6=9F=A5=E8=AF=A2=E7=B3=BB=E7=BB=9F=E6=8C=87?= =?UTF-8?q?=E5=AF=BC=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增价格查询系统的核心架构、关键组件、商品类型支持等内容 - 详细说明价格计算流程和优惠券系统 - 添加分页查询和统计功能的说明 - 新增开发模式下的操作指南,包括添加新商品类型和优惠券类型的步骤 - 介绍自定义TypeHandler的使用和测试策略 --- CLAUDE.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 63c9ce5..6c92571 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md -本文件为 Claude Code (claude.ai/code) 在此代码仓库中工作时提供指导。 +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 构建和开发命令 @@ -120,4 +120,71 @@ mvn test -DskipTests=false 1. 在 `com.ycwl.basic.task` 包中创建类 2. 添加 `@Component` 和 `@Profile("prod")` 注解 3. 使用 `@Scheduled` 进行基于 cron 的执行 -4. 遵循现有的错误处理和日志记录模式 \ No newline at end of file +4. 遵循现有的错误处理和日志记录模式 + +## 价格查询系统 (Pricing Module) + +### 核心架构 +价格查询系统是一个独立的业务模块,位于 `com.ycwl.basic.pricing` 包中,提供商品定价、优惠券管理和价格计算功能。 + +#### 关键组件 +- **PriceCalculationController** (`/api/pricing/calculate`):价格计算API +- **CouponManagementController** (`/api/pricing/admin/coupons/`):优惠券管理API +- **PricingConfigController** (`/api/pricing/config/`):价格配置管理API + +#### 商品类型支持 +```java +ProductType枚举定义了支持的商品类型: +- VLOG_VIDEO: Vlog视频 +- RECORDING_SET: 录像集 +- PHOTO_SET: 照相集 +- PHOTO_PRINT: 照片打印 +- MACHINE_PRINT: 一体机打印 +``` + +#### 价格计算流程 +1. 接收PriceCalculationRequest(包含商品列表和用户ID) +2. 查找商品基础配置和分层定价 +3. 处理套餐商品(BundleProductItem) +4. 自动应用最优优惠券 +5. 返回PriceCalculationResult(包含原价、最终价格、优惠详情) + +#### 优惠券系统 +- **CouponType**: PERCENTAGE(百分比)、FIXED_AMOUNT(固定金额) +- **CouponStatus**: CLAIMED(已领取)、USED(已使用)、EXPIRED(已过期) +- 支持商品类型限制 (`applicableProducts` JSON字段) +- 最小消费金额和最大折扣限制 +- 时间有效期控制 + +#### 分页查询功能 +所有管理接口都支持分页查询,使用PageHelper实现: +- 优惠券配置分页:支持按状态、名称筛选 +- 领取记录分页:支持按用户、优惠券、状态、时间范围筛选 + +#### 统计功能 +- 基础统计:领取数、使用数、可用数 +- 详细统计:使用率、平均使用天数 +- 时间范围统计:指定时间段的整体数据分析 + +### 开发模式 + +#### 添加新商品类型 +1. 在ProductType枚举中添加新类型 +2. 在PriceProductConfig表中配置default配置 +3. 根据需要添加分层定价(PriceTierConfig) +4. 更新前端产品类型映射 + +#### 添加新优惠券类型 +1. 在CouponType枚举中添加类型 +2. 在CouponServiceImpl中实现计算逻辑 +3. 更新applicableProducts验证规则 + +#### 自定义TypeHandler使用 +项目使用自定义TypeHandler处理复杂JSON字段: +- `BundleProductListTypeHandler`:处理套餐商品列表JSON序列化 + +### 测试策略 +- 单元测试:每个服务类都有对应测试类 +- 配置验证测试:DefaultConfigValidationTest验证default配置 +- JSON序列化测试:验证复杂对象的数据库存储 +- 分页功能测试:验证PageHelper集成 \ No newline at end of file From 9e0286e66ec134bcfa0351cc945c92f698b72f0f Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 18 Aug 2025 04:58:38 +0800 Subject: [PATCH 07/18] =?UTF-8?q?feat(pricing):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=99=AF=E5=8C=BA=E4=BC=98=E6=83=A0=E5=88=B8=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BC=98=E6=83=A0?= =?UTF-8?q?=E5=88=B8=E4=BD=BF=E7=94=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增景区优惠券统计接口和相关查询方法 - 为优惠券配置和使用记录添加景区ID字段 - 实现优惠券使用时的景区限制检查 - 优化优惠券适用性的判断逻辑,增加对景区和商品类型的检查 --- .../CouponManagementController.java | 35 +++++++++---- .../basic/pricing/dto/CouponUseRequest.java | 5 ++ .../ycwl/basic/pricing/dto/ProductItem.java | 5 ++ .../entity/PriceCouponClaimRecord.java | 5 ++ .../pricing/entity/PriceCouponConfig.java | 5 ++ .../mapper/PriceCouponClaimRecordMapper.java | 50 ++++++++++++++++--- .../mapper/PriceCouponConfigMapper.java | 38 ++++++++++++-- .../service/ICouponManagementService.java | 11 ++-- .../impl/CouponManagementServiceImpl.java | 41 +++++++++++---- .../service/impl/CouponServiceImpl.java | 28 ++++++++++- 10 files changed, 184 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java b/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java index 46d66ca..c061981 100644 --- a/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/CouponManagementController.java @@ -85,11 +85,12 @@ public class CouponManagementController { @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Boolean isActive, - @RequestParam(required = false) String couponName) { - log.info("分页查询优惠券配置: pageNum={}, pageSize={}, isActive={}, couponName={}", - pageNum, pageSize, isActive, couponName); + @RequestParam(required = false) String couponName, + @RequestParam(required = false) String scenicId) { + log.info("分页查询优惠券配置: pageNum={}, pageSize={}, isActive={}, couponName={}, scenicId={}", + pageNum, pageSize, isActive, couponName, scenicId); PageInfo pageInfo = couponManagementService.getCouponConfigsPage( - pageNum, pageSize, isActive, couponName); + pageNum, pageSize, isActive, couponName, scenicId); return ApiResponse.success(pageInfo); } @@ -136,11 +137,12 @@ public class CouponManagementController { @RequestParam(required = false) Long couponId, @RequestParam(required = false) CouponStatus status, @RequestParam(required = false) String startTime, - @RequestParam(required = false) String endTime) { - log.info("分页查询优惠券领取记录: pageNum={}, pageSize={}, userId={}, couponId={}, status={}, startTime={}, endTime={}", - pageNum, pageSize, userId, couponId, status, startTime, endTime); + @RequestParam(required = false) String endTime, + @RequestParam(required = false) String scenicId) { + log.info("分页查询优惠券领取记录: pageNum={}, pageSize={}, userId={}, couponId={}, status={}, startTime={}, endTime={}, scenicId={}", + pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId); PageInfo pageInfo = couponManagementService.getClaimRecordsPage( - pageNum, pageSize, userId, couponId, status, startTime, endTime); + pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId); return ApiResponse.success(pageInfo); } @@ -202,9 +204,20 @@ public class CouponManagementController { @GetMapping("/stats/period") public ApiResponse> getPeriodStats( @RequestParam String startDate, - @RequestParam String endDate) { - log.info("查询时间范围统计: startDate={}, endDate={}", startDate, endDate); - Map stats = couponManagementService.getPeriodStats(startDate, endDate); + @RequestParam String endDate, + @RequestParam(required = false) String scenicId) { + log.info("查询时间范围统计: startDate={}, endDate={}, scenicId={}", startDate, endDate, scenicId); + Map stats = couponManagementService.getPeriodStats(startDate, endDate, scenicId); + return ApiResponse.success(stats); + } + + /** + * 查询景区优惠券统计 + */ + @GetMapping("/stats/scenic/{scenicId}") + public ApiResponse> getScenicCouponStats(@PathVariable String scenicId) { + log.info("查询景区优惠券统计: scenicId={}", scenicId); + Map stats = couponManagementService.getScenicCouponStats(scenicId); return ApiResponse.success(stats); } diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java index f1f7253..a8231e6 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseRequest.java @@ -34,4 +34,9 @@ public class CouponUseRequest { * 优惠金额 */ private BigDecimal discountAmount; + + /** + * 景区ID + */ + private String scenicId; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java index 5bb0444..c5d2160 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java @@ -45,4 +45,9 @@ public class ProductItem { * 小计(计算后填入) */ private BigDecimal subtotal; + + /** + * 景区ID + */ + private String scenicId; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java index 352650f..22f1824 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java @@ -44,4 +44,9 @@ public class PriceCouponClaimRecord extends BaseEntity { * 状态 */ private CouponStatus status; + + /** + * 景区ID - 记录优惠券在哪个景区被领取/使用 + */ + private String scenicId; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java index f0e134e..386e6fe 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java @@ -70,4 +70,9 @@ public class PriceCouponConfig extends BaseEntity { * 是否启用 */ private Boolean isActive; + + /** + * 景区ID - 限制优惠券只能在该景区使用 + */ + private String scenicId; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java index 9c0dfd4..b95bf3d 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java @@ -40,18 +40,19 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper" + "AND r.claim_time <= #{endTime}" + "" + + "" + + "AND r.scenic_id = #{scenicId}" + + "" + "" + "ORDER BY r.created_time DESC" + "") @@ -102,7 +106,8 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper" + + "SELECT " + "COUNT(*) as total_claimed, " + "COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " + "COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " + @@ -169,7 +175,35 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper= #{startDate} AND claim_time <= #{endDate}") + "WHERE claim_time >= #{startDate} AND claim_time <= #{endDate} " + + "" + + "AND scenic_id = #{scenicId} " + + "" + + "") java.util.Map selectPeriodStats(@Param("startDate") String startDate, - @Param("endDate") String endDate); + @Param("endDate") String endDate, + @Param("scenicId") String scenicId); + + /** + * 根据景区ID查询优惠券领取记录 + */ + @Select("SELECT r.*, c.coupon_name, c.coupon_type, c.discount_value " + + "FROM price_coupon_claim_record r " + + "LEFT JOIN price_coupon_config c ON r.coupon_id = c.id " + + "WHERE r.scenic_id = #{scenicId} " + + "ORDER BY r.created_time DESC") + List selectByScenicIdForAdmin(@Param("scenicId") String scenicId); + + /** + * 统计景区优惠券使用情况 + */ + @Select("SELECT " + + "COUNT(*) as total_claimed, " + + "COUNT(CASE WHEN status = 'USED' THEN 1 END) as total_used, " + + "COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_available, " + + "COUNT(CASE WHEN status = 'EXPIRED' THEN 1 END) as total_expired, " + + "COUNT(DISTINCT coupon_id) as total_coupon_types, " + + "COUNT(DISTINCT user_id) as total_users " + + "FROM price_coupon_claim_record WHERE scenic_id = #{scenicId}") + java.util.Map selectScenicCouponUsageStats(@Param("scenicId") String scenicId); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java index c44585d..d1075a5 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java @@ -44,10 +44,10 @@ public interface PriceCouponConfigMapper extends BaseMapper { */ @Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " + "max_discount, applicable_products, total_quantity, used_quantity, valid_from, valid_until, " + - "is_active, created_time, updated_time) VALUES " + + "is_active, scenic_id, created_time, updated_time) VALUES " + "(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " + "#{applicableProducts}, #{totalQuantity}, #{usedQuantity}, #{validFrom}, #{validUntil}, " + - "#{isActive}, NOW(), NOW())") + "#{isActive}, #{scenicId}, NOW(), NOW())") int insertCoupon(PriceCouponConfig coupon); /** @@ -57,7 +57,7 @@ public interface PriceCouponConfigMapper extends BaseMapper { "discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " + "applicable_products = #{applicableProducts}, total_quantity = #{totalQuantity}, " + "valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " + - "updated_time = NOW() WHERE id = #{id}") + "scenic_id = #{scenicId}, updated_time = NOW() WHERE id = #{id}") int updateCoupon(PriceCouponConfig coupon); // ==================== 管理端接口 ==================== @@ -80,11 +80,15 @@ public interface PriceCouponConfigMapper extends BaseMapper { "" + "AND coupon_name LIKE CONCAT('%', #{couponName}, '%')" + "" + + "" + + "AND scenic_id = #{scenicId}" + + "" + "" + "ORDER BY created_time DESC" + "") List selectByConditionsForAdmin(@Param("isActive") Boolean isActive, - @Param("couponName") String couponName); + @Param("couponName") String couponName, + @Param("scenicId") String scenicId); /** * 管理端:根据状态查询优惠券配置 @@ -103,4 +107,30 @@ public interface PriceCouponConfigMapper extends BaseMapper { */ @Update("UPDATE price_coupon_config SET deleted = 1, updated_time = NOW() WHERE id = #{id}") int deleteCoupon(Long id); + + /** + * 查询指定景区的有效优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE is_active = 1 " + + "AND valid_from <= NOW() AND valid_until > NOW() " + + "AND used_quantity < total_quantity " + + "AND (scenic_id IS NULL OR scenic_id = #{scenicId})") + List selectValidCouponsByScenicId(@Param("scenicId") String scenicId); + + /** + * 管理端:根据景区ID查询优惠券配置 + */ + @Select("SELECT * FROM price_coupon_config WHERE scenic_id = #{scenicId} ORDER BY created_time DESC") + List selectByScenicIdForAdmin(@Param("scenicId") String scenicId); + + /** + * 统计景区优惠券配置数量 + */ + @Select("SELECT " + + "COUNT(*) as total_coupons, " + + "COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_coupons, " + + "SUM(total_quantity) as total_quantity, " + + "SUM(used_quantity) as used_quantity " + + "FROM price_coupon_config WHERE scenic_id = #{scenicId}") + java.util.Map selectScenicCouponConfigStats(@Param("scenicId") String scenicId); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java b/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java index 61b6583..71013cf 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/ICouponManagementService.java @@ -44,7 +44,7 @@ public interface ICouponManagementService { * 分页查询优惠券配置 */ PageInfo getCouponConfigsPage(Integer pageNum, Integer pageSize, - Boolean isActive, String couponName); + Boolean isActive, String couponName, String scenicId); /** * 根据状态查询优惠券配置 @@ -68,7 +68,7 @@ public interface ICouponManagementService { */ PageInfo getClaimRecordsPage(Integer pageNum, Integer pageSize, Long userId, Long couponId, CouponStatus status, - String startTime, String endTime); + String startTime, String endTime, String scenicId); /** * 根据用户ID查询优惠券领取记录 @@ -98,10 +98,15 @@ public interface ICouponManagementService { /** * 查询时间范围内的统计数据 */ - Map getPeriodStats(String startDate, String endDate); + Map getPeriodStats(String startDate, String endDate, String scenicId); /** * 查询所有优惠券的使用统计概览 */ List> getAllCouponUsageOverview(); + + /** + * 查询景区优惠券统计 + */ + Map getScenicCouponStats(String scenicId); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java index c7ccd42..4342fdf 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponManagementServiceImpl.java @@ -126,12 +126,12 @@ public class CouponManagementServiceImpl implements ICouponManagementService { @Override public PageInfo getCouponConfigsPage(Integer pageNum, Integer pageSize, - Boolean isActive, String couponName) { - log.info("分页查询优惠券配置,页码: {}, 页大小: {}, 状态: {}, 名称: {}", - pageNum, pageSize, isActive, couponName); + Boolean isActive, String couponName, String scenicId) { + log.info("分页查询优惠券配置,页码: {}, 页大小: {}, 状态: {}, 名称: {}, 景区ID: {}", + pageNum, pageSize, isActive, couponName, scenicId); PageHelper.startPage(pageNum, pageSize); - List configs = couponConfigMapper.selectByConditionsForAdmin(isActive, couponName); + List configs = couponConfigMapper.selectByConditionsForAdmin(isActive, couponName, scenicId); return new PageInfo<>(configs); } @@ -158,12 +158,12 @@ public class CouponManagementServiceImpl implements ICouponManagementService { @Override public PageInfo getClaimRecordsPage(Integer pageNum, Integer pageSize, Long userId, Long couponId, CouponStatus status, - String startTime, String endTime) { - log.info("分页查询优惠券领取记录,页码: {}, 页大小: {}, 用户ID: {}, 优惠券ID: {}, 状态: {}, 开始时间: {}, 结束时间: {}", - pageNum, pageSize, userId, couponId, status, startTime, endTime); + String startTime, String endTime, String scenicId) { + log.info("分页查询优惠券领取记录,页码: {}, 页大小: {}, 用户ID: {}, 优惠券ID: {}, 状态: {}, 开始时间: {}, 结束时间: {}, 景区ID: {}", + pageNum, pageSize, userId, couponId, status, startTime, endTime, scenicId); PageHelper.startPage(pageNum, pageSize); - List records = claimRecordMapper.selectByConditionsForAdmin(userId, couponId, status, startTime, endTime); + List records = claimRecordMapper.selectByConditionsForAdmin(userId, couponId, status, startTime, endTime, scenicId); return new PageInfo<>(records); } @@ -198,9 +198,9 @@ public class CouponManagementServiceImpl implements ICouponManagementService { } @Override - public Map getPeriodStats(String startDate, String endDate) { - log.info("查询时间范围统计,开始日期: {}, 结束日期: {}", startDate, endDate); - return claimRecordMapper.selectPeriodStats(startDate, endDate); + public Map getPeriodStats(String startDate, String endDate, String scenicId) { + log.info("查询时间范围统计,开始日期: {}, 结束日期: {}, 景区ID: {}", startDate, endDate, scenicId); + return claimRecordMapper.selectPeriodStats(startDate, endDate, scenicId); } @Override @@ -231,4 +231,23 @@ public class CouponManagementServiceImpl implements ICouponManagementService { return overview; } + + @Override + public Map getScenicCouponStats(String scenicId) { + log.info("查询景区优惠券统计,景区ID: {}", scenicId); + + // 获取景区优惠券配置统计 + Map configStats = couponConfigMapper.selectScenicCouponConfigStats(scenicId); + + // 获取景区优惠券使用统计 + Map usageStats = claimRecordMapper.selectScenicCouponUsageStats(scenicId); + + // 合并统计结果 + Map result = new HashMap<>(); + result.put("scenic_id", scenicId); + result.putAll(configStats); + result.putAll(usageStats); + + return result; + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java index 4394dd5..a908f27 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java @@ -82,10 +82,29 @@ public class CouponServiceImpl implements ICouponService { @Override public boolean isCouponApplicable(PriceCouponConfig coupon, List products, BigDecimal totalAmount) { + // 1. 检查最小使用金额 if (totalAmount.compareTo(coupon.getMinAmount()) < 0) { return false; } + // 2. 检查景区限制 + if (coupon.getScenicId() != null && !coupon.getScenicId().isEmpty()) { + boolean hasMatchingScenicProduct = false; + for (ProductItem product : products) { + if (coupon.getScenicId().equals(product.getScenicId())) { + hasMatchingScenicProduct = true; + break; + } + } + if (!hasMatchingScenicProduct) { + log.debug("优惠券景区限制不匹配: 优惠券景区={}, 商品景区={}", + coupon.getScenicId(), + products.stream().map(ProductItem::getScenicId).distinct().toList()); + return false; + } + } + + // 3. 检查商品类型限制 if (coupon.getApplicableProducts() == null || coupon.getApplicableProducts().isEmpty()) { return true; } @@ -127,14 +146,19 @@ public class CouponServiceImpl implements ICouponService { LocalDateTime useTime = LocalDateTime.now(); - // 设置使用时间和订单信息 + // 设置使用时间、订单信息和景区信息 record.setStatus(CouponStatus.USED); record.setUseTime(useTime); record.setOrderId(request.getOrderId()); record.setUpdatedTime(LocalDateTime.now()); + // 如果请求中包含景区ID,记录到使用记录中 + if (request.getScenicId() != null && !request.getScenicId().isEmpty()) { + record.setScenicId(request.getScenicId()); + } + couponClaimRecordMapper.updateCouponStatus( - record.getId(), CouponStatus.USED, useTime, request.getOrderId()); + record.getId(), CouponStatus.USED, useTime, request.getOrderId(), request.getScenicId()); CouponUseResult result = new CouponUseResult(); result.setCouponId(request.getCouponId()); From 85a179c5b4bf976141c23d2c10556c57bc98113f Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 21 Aug 2025 01:13:32 +0800 Subject: [PATCH 08/18] =?UTF-8?q?feat(voucher):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=88=B8=E7=A0=81=E6=A0=B8=E9=94=80=E5=8A=9F=E8=83=BD=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加券码批次管理和券码管理相关接口和实现 - 新增券码生成、领取、使用等核心业务逻辑 - 实现了全场免费、商品降价、商品打折三种优惠模式 - 添加了券码状态管理和统计功能 - 优化了数据库表结构和索引 - 编写了详细的开发文档和使用示例 --- .../java/com/ycwl/basic/voucher/CLAUDE.md | 179 ++++++++++++++++ .../voucher/controller/VoucherController.java | 92 +++++++++ .../dto/req/VoucherBatchCreateReq.java | 15 ++ .../voucher/dto/req/VoucherBatchQueryReq.java | 14 ++ .../voucher/dto/req/VoucherClaimReq.java | 10 + .../voucher/dto/req/VoucherCodeQueryReq.java | 15 ++ .../voucher/dto/resp/VoucherBatchResp.java | 25 +++ .../dto/resp/VoucherBatchStatsResp.java | 15 ++ .../voucher/dto/resp/VoucherCodeResp.java | 27 +++ .../voucher/entity/VoucherBatchEntity.java | 30 +++ .../voucher/entity/VoucherCodeEntity.java | 27 +++ .../voucher/enums/VoucherCodeStatus.java | 27 +++ .../voucher/enums/VoucherDiscountType.java | 28 +++ .../voucher/mapper/VoucherBatchMapper.java | 9 + .../voucher/mapper/VoucherCodeMapper.java | 17 ++ .../voucher/service/VoucherBatchService.java | 27 +++ .../voucher/service/VoucherCodeService.java | 23 +++ .../service/impl/VoucherBatchServiceImpl.java | 190 +++++++++++++++++ .../service/impl/VoucherCodeServiceImpl.java | 194 ++++++++++++++++++ 19 files changed, 964 insertions(+) create mode 100644 src/main/java/com/ycwl/basic/voucher/CLAUDE.md create mode 100644 src/main/java/com/ycwl/basic/voucher/controller/VoucherController.java create mode 100644 src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchCreateReq.java create mode 100644 src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchQueryReq.java create mode 100644 src/main/java/com/ycwl/basic/voucher/dto/req/VoucherClaimReq.java create mode 100644 src/main/java/com/ycwl/basic/voucher/dto/req/VoucherCodeQueryReq.java create mode 100644 src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchResp.java create mode 100644 src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchStatsResp.java create mode 100644 src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherCodeResp.java create mode 100644 src/main/java/com/ycwl/basic/voucher/entity/VoucherBatchEntity.java create mode 100644 src/main/java/com/ycwl/basic/voucher/entity/VoucherCodeEntity.java create mode 100644 src/main/java/com/ycwl/basic/voucher/enums/VoucherCodeStatus.java create mode 100644 src/main/java/com/ycwl/basic/voucher/enums/VoucherDiscountType.java create mode 100644 src/main/java/com/ycwl/basic/voucher/mapper/VoucherBatchMapper.java create mode 100644 src/main/java/com/ycwl/basic/voucher/mapper/VoucherCodeMapper.java create mode 100644 src/main/java/com/ycwl/basic/voucher/service/VoucherBatchService.java create mode 100644 src/main/java/com/ycwl/basic/voucher/service/VoucherCodeService.java create mode 100644 src/main/java/com/ycwl/basic/voucher/service/impl/VoucherBatchServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/voucher/service/impl/VoucherCodeServiceImpl.java diff --git a/src/main/java/com/ycwl/basic/voucher/CLAUDE.md b/src/main/java/com/ycwl/basic/voucher/CLAUDE.md new file mode 100644 index 0000000..cadf343 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/CLAUDE.md @@ -0,0 +1,179 @@ +# 券码核销功能模块 + +本模块实现景区券码的批量创建、分发和核销管理功能。支持全场免费、商品降价、商品打折三种优惠模式,确保每个用户在每个景区只能领取一次券码。 + +## 功能概述 + +- **批量创建券码**:管理员可创建券码批次,自动生成指定数量的唯一券码 +- **精准分发控制**:通过景区ID、推客ID、用户faceId进行精准投放 +- **三种优惠模式**:全场免费、商品降价、商品打折 +- **唯一性保证**:同一用户在同一景区只能领取一次券码 +- **完整管理功能**:批次管理、券码查询、使用统计、手动核销 + +## 数据库表结构 + +### 券码批次表 (voucher_batch) +```sql +CREATE TABLE voucher_batch ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + batch_name VARCHAR(100) NOT NULL COMMENT '券码批次名称', + scenic_id BIGINT NOT NULL COMMENT '景区ID', + broker_id BIGINT NOT NULL COMMENT '推客ID', + discount_type TINYINT NOT NULL COMMENT '优惠类型:0=全场免费,1=商品降价,2=商品打折', + discount_value DECIMAL(10,2) COMMENT '优惠值(降价金额或折扣百分比)', + total_count INT NOT NULL COMMENT '总券码数量', + used_count INT DEFAULT 0 COMMENT '已使用数量', + claimed_count INT DEFAULT 0 COMMENT '已领取数量', + status TINYINT DEFAULT 1 COMMENT '状态:0=禁用,1=启用', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + create_by BIGINT COMMENT '创建人ID', + deleted TINYINT DEFAULT 0, + deleted_at DATETIME +); +``` + +### 券码表 (voucher_code) +```sql +CREATE TABLE voucher_code ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + batch_id BIGINT NOT NULL COMMENT '批次ID', + scenic_id BIGINT NOT NULL COMMENT '景区ID', + code VARCHAR(32) NOT NULL UNIQUE COMMENT '券码', + status TINYINT DEFAULT 0 COMMENT '状态:0=未领取,1=已领取未使用,2=已使用', + face_id BIGINT COMMENT '领取人faceId', + claimed_time DATETIME COMMENT '领取时间', + used_time DATETIME COMMENT '使用时间', + remark VARCHAR(500) COMMENT '使用备注', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + deleted TINYINT DEFAULT 0, + deleted_at DATETIME +); +``` + +## 包结构说明 + +``` +com.ycwl.basic.voucher/ +├── controller/ +│ └── VoucherController.java # 控制器:PC端管理和移动端用户接口 +├── service/ +│ ├── VoucherBatchService.java # 券码批次服务接口 +│ ├── VoucherCodeService.java # 券码服务接口 +│ └── impl/ +│ ├── VoucherBatchServiceImpl.java # 券码批次服务实现 +│ └── VoucherCodeServiceImpl.java # 券码服务实现 +├── mapper/ +│ ├── VoucherBatchMapper.java # 券码批次数据访问 +│ └── VoucherCodeMapper.java # 券码数据访问 +├── entity/ +│ ├── VoucherBatchEntity.java # 券码批次实体 +│ └── VoucherCodeEntity.java # 券码实体 +├── dto/ +│ ├── req/ # 请求DTO +│ │ ├── VoucherBatchCreateReq.java # 创建批次请求 +│ │ ├── VoucherBatchQueryReq.java # 批次查询请求 +│ │ ├── VoucherCodeQueryReq.java # 券码查询请求 +│ │ └── VoucherClaimReq.java # 券码领取请求 +│ └── resp/ # 响应DTO +│ ├── VoucherBatchResp.java # 批次响应 +│ ├── VoucherCodeResp.java # 券码响应 +│ └── VoucherBatchStatsResp.java # 批次统计响应 +└── enums/ + ├── VoucherDiscountType.java # 优惠类型枚举 + └── VoucherCodeStatus.java # 券码状态枚举 +``` + +## 核心业务逻辑 + +### 1. 券码生成规则 +- 使用UUID生成8位大写字母数字组合 +- 确保每个券码在系统中唯一 +- 券码初始状态为"未领取" + +### 2. 领取验证逻辑 +```java +// 核心验证:同一faceId在同一scenicId中只能领取一次 +public boolean canClaimVoucher(Long faceId, Long scenicId) { + Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId); + return count == 0; +} +``` + +### 3. 优惠类型说明 +- **全场免费(0)**:所有商品免费,discountValue可为空 +- **商品降价(1)**:每个商品减免固定金额,discountValue为减免金额 +- **商品打折(2)**:每个商品按百分比打折,discountValue为折扣百分比 + +### 4. 状态流转 +券码状态:未领取(0) → 已领取未使用(1) → 已使用(2) + +## 主要接口说明 + +### PC端管理接口 +- `POST /api/voucher/batch/create` - 创建券码批次 +- `POST /api/voucher/batch/list` - 批次列表查询 +- `GET /api/voucher/batch/{id}` - 批次详情 +- `GET /api/voucher/batch/{id}/stats` - 批次统计 +- `PUT /api/voucher/batch/{id}/status` - 启用/禁用批次 +- `POST /api/voucher/codes` - 券码列表查询 +- `PUT /api/voucher/code/{id}/use` - 手动标记券码已使用 +- `GET /api/voucher/scenic/{scenicId}/users` - 查看景区下用户领取情况 + +### 移动端用户接口 +- `POST /api/voucher/mobile/claim` - 领取券码(一步到位) +- `GET /api/voucher/mobile/my-codes` - 我的券码列表 + +## 关键技术特点 + +### 1. 事务管理 +- 使用`@Transactional`确保券码领取操作的原子性 +- 批次创建和券码生成在同一事务中完成 + +### 2. 并发控制 +- 通过应用层验证避免数据库唯一约束冲突 +- 使用数据库索引优化查询性能 + +### 3. 参数验证 +- 在Service层进行手动参数校验 +- 使用BizException统一错误处理 + +### 4. 数据统计 +- 实时更新批次的已领取数量和已使用数量 +- 提供详细的使用统计和分析数据 + +## 使用示例 + +### 创建券码批次 +```java +VoucherBatchCreateReq req = new VoucherBatchCreateReq(); +req.setBatchName("春节特惠券"); +req.setScenicId(1001L); +req.setBrokerId(2001L); +req.setDiscountType(1); // 商品降价 +req.setDiscountValue(new BigDecimal("10.00")); +req.setTotalCount(1000); + +Long batchId = voucherBatchService.createBatch(req); +``` + +### 用户领取券码 +```java +VoucherClaimReq req = new VoucherClaimReq(); +req.setScenicId(1001L); +req.setBrokerId(2001L); +req.setFaceId(3001L); + +VoucherCodeResp result = voucherCodeService.claimVoucher(req); +``` + +## 注意事项 + +1. **唯一性限制**:同一个faceId在同一个scenicId中只能领取一次券码 +2. **批次状态**:只有启用状态的批次才能领取券码 +3. **券码数量**:确保批次有可用券码才能成功领取 +4. **优惠值验证**:除全场免费外,其他优惠类型必须设置优惠值 +5. **删除机制**:使用逻辑删除,deleted字段标记删除状态 + +## 扩展说明 + +本模块设计为独立功能模块,不依赖支付系统,为后续接入其他优惠策略预留了扩展空间。所有接口都提供了完整的错误处理和参数验证,确保系统的稳定性和可维护性。 \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/controller/VoucherController.java b/src/main/java/com/ycwl/basic/voucher/controller/VoucherController.java new file mode 100644 index 0000000..fea2ab1 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/controller/VoucherController.java @@ -0,0 +1,92 @@ +package com.ycwl.basic.voucher.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.voucher.dto.req.VoucherClaimReq; +import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp; +import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp; +import com.ycwl.basic.voucher.service.VoucherBatchService; +import com.ycwl.basic.voucher.service.VoucherCodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/voucher") +@RequiredArgsConstructor +public class VoucherController { + + private final VoucherBatchService voucherBatchService; + private final VoucherCodeService voucherCodeService; + + @PostMapping("/batch/create") + public ApiResponse createBatch(@RequestBody VoucherBatchCreateReq req) { + Long batchId = voucherBatchService.createBatch(req); + return ApiResponse.success(batchId); + } + + @PostMapping("/batch/list") + public ApiResponse> getBatchList(@RequestBody VoucherBatchQueryReq req) { + Page page = voucherBatchService.queryBatchList(req); + return ApiResponse.success(page); + } + + @GetMapping("/batch/{id}") + public ApiResponse getBatchDetail(@PathVariable Long id) { + VoucherBatchResp batch = voucherBatchService.getBatchDetail(id); + return ApiResponse.success(batch); + } + + @GetMapping("/batch/{id}/stats") + public ApiResponse getBatchStats(@PathVariable Long id) { + VoucherBatchStatsResp stats = voucherBatchService.getBatchStats(id); + return ApiResponse.success(stats); + } + + @PutMapping("/batch/{id}/status") + public ApiResponse updateBatchStatus(@PathVariable Long id, @RequestParam Integer status) { + voucherBatchService.updateBatchStatus(id, status); + return ApiResponse.success(null); + } + + @PostMapping("/codes") + public ApiResponse> getCodeList(@RequestBody VoucherCodeQueryReq req) { + Page page = voucherCodeService.queryCodeList(req); + return ApiResponse.success(page); + } + + @PutMapping("/code/{id}/use") + public ApiResponse markCodeAsUsed(@PathVariable Long id, @RequestParam(required = false) String remark) { + voucherCodeService.markCodeAsUsed(id, remark); + return ApiResponse.success(null); + } + + @GetMapping("/scenic/{scenicId}/users") + public ApiResponse> getUsersInScenic(@PathVariable Long scenicId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + VoucherCodeQueryReq req = new VoucherCodeQueryReq(); + req.setScenicId(scenicId); + req.setPageNum(pageNum); + req.setPageSize(pageSize); + Page page = voucherCodeService.queryCodeList(req); + return ApiResponse.success(page); + } + + @PostMapping("/mobile/claim") + public ApiResponse claimVoucher(@RequestBody VoucherClaimReq req) { + VoucherCodeResp result = voucherCodeService.claimVoucher(req); + return ApiResponse.success(result); + } + + @GetMapping("/mobile/my-codes") + public ApiResponse> getMyVoucherCodes(@RequestParam Long faceId) { + List codes = voucherCodeService.getMyVoucherCodes(faceId); + return ApiResponse.success(codes); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchCreateReq.java b/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchCreateReq.java new file mode 100644 index 0000000..47c1653 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchCreateReq.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.voucher.dto.req; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class VoucherBatchCreateReq { + private String batchName; + private Long scenicId; + private Long brokerId; + private Integer discountType; + private BigDecimal discountValue; + private Integer totalCount; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchQueryReq.java b/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchQueryReq.java new file mode 100644 index 0000000..ce82a73 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchQueryReq.java @@ -0,0 +1,14 @@ +package com.ycwl.basic.voucher.dto.req; + +import com.ycwl.basic.model.common.BaseQueryParameterReq; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class VoucherBatchQueryReq extends BaseQueryParameterReq { + private Long scenicId; + private Long brokerId; + private Integer status; + private String batchName; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherClaimReq.java b/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherClaimReq.java new file mode 100644 index 0000000..e6d318d --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherClaimReq.java @@ -0,0 +1,10 @@ +package com.ycwl.basic.voucher.dto.req; + +import lombok.Data; + +@Data +public class VoucherClaimReq { + private Long scenicId; + private Long brokerId; + private Long faceId; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherCodeQueryReq.java b/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherCodeQueryReq.java new file mode 100644 index 0000000..ea853bd --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherCodeQueryReq.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.voucher.dto.req; + +import com.ycwl.basic.model.common.BaseQueryParameterReq; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class VoucherCodeQueryReq extends BaseQueryParameterReq { + private Long batchId; + private Long scenicId; + private Long faceId; + private Integer status; + private String code; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchResp.java b/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchResp.java new file mode 100644 index 0000000..cbee09d --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchResp.java @@ -0,0 +1,25 @@ +package com.ycwl.basic.voucher.dto.resp; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class VoucherBatchResp { + private Long id; + private String batchName; + private Long scenicId; + private Long brokerId; + private Integer discountType; + private String discountTypeName; + private BigDecimal discountValue; + private Integer totalCount; + private Integer usedCount; + private Integer claimedCount; + private Integer availableCount; + private Integer status; + private String statusName; + private Date createTime; + private Long createBy; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchStatsResp.java b/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchStatsResp.java new file mode 100644 index 0000000..285aee1 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchStatsResp.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.voucher.dto.resp; + +import lombok.Data; + +@Data +public class VoucherBatchStatsResp { + private Long batchId; + private String batchName; + private Integer totalCount; + private Integer claimedCount; + private Integer usedCount; + private Integer availableCount; + private Double claimedRate; + private Double usedRate; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherCodeResp.java b/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherCodeResp.java new file mode 100644 index 0000000..6b9a498 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherCodeResp.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.voucher.dto.resp; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class VoucherCodeResp { + private Long id; + private Long batchId; + private String batchName; + private Long scenicId; + private String code; + private Integer status; + private String statusName; + private Long faceId; + private Date claimedTime; + private Date usedTime; + private String remark; + private Date createTime; + + private Integer discountType; + private String discountTypeName; + private String discountDescription; + private BigDecimal discountValue; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/entity/VoucherBatchEntity.java b/src/main/java/com/ycwl/basic/voucher/entity/VoucherBatchEntity.java new file mode 100644 index 0000000..9d2dfef --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/entity/VoucherBatchEntity.java @@ -0,0 +1,30 @@ +package com.ycwl.basic.voucher.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +@TableName("voucher_batch") +public class VoucherBatchEntity { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String batchName; + private Long scenicId; + private Long brokerId; + private Integer discountType; + private BigDecimal discountValue; + private Integer totalCount; + private Integer usedCount; + private Integer claimedCount; + private Integer status; + private Date createTime; + private Long createBy; + private Integer deleted; + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/entity/VoucherCodeEntity.java b/src/main/java/com/ycwl/basic/voucher/entity/VoucherCodeEntity.java new file mode 100644 index 0000000..63a5dab --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/entity/VoucherCodeEntity.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.voucher.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +@Data +@TableName("voucher_code") +public class VoucherCodeEntity { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long batchId; + private Long scenicId; + private String code; + private Integer status; + private Long faceId; + private Date claimedTime; + private Date usedTime; + private String remark; + private Date createTime; + private Integer deleted; + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/enums/VoucherCodeStatus.java b/src/main/java/com/ycwl/basic/voucher/enums/VoucherCodeStatus.java new file mode 100644 index 0000000..a55bbca --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/enums/VoucherCodeStatus.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.voucher.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum VoucherCodeStatus { + UNCLAIMED(0, "未领取"), + CLAIMED_UNUSED(1, "已领取未使用"), + USED(2, "已使用"); + + private final Integer code; + private final String name; + + public static VoucherCodeStatus getByCode(Integer code) { + if (code == null) { + return null; + } + for (VoucherCodeStatus status : values()) { + if (status.getCode().equals(code)) { + return status; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/enums/VoucherDiscountType.java b/src/main/java/com/ycwl/basic/voucher/enums/VoucherDiscountType.java new file mode 100644 index 0000000..5e53d8d --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/enums/VoucherDiscountType.java @@ -0,0 +1,28 @@ +package com.ycwl.basic.voucher.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum VoucherDiscountType { + FREE_ALL(0, "全场免费", "所有商品免费"), + REDUCE_PRICE(1, "商品降价", "每个商品减免指定金额"), + DISCOUNT(2, "商品打折", "每个商品按百分比打折"); + + private final Integer code; + private final String name; + private final String description; + + public static VoucherDiscountType getByCode(Integer code) { + if (code == null) { + return null; + } + for (VoucherDiscountType type : values()) { + if (type.getCode().equals(code)) { + return type; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/mapper/VoucherBatchMapper.java b/src/main/java/com/ycwl/basic/voucher/mapper/VoucherBatchMapper.java new file mode 100644 index 0000000..e3f561a --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/mapper/VoucherBatchMapper.java @@ -0,0 +1,9 @@ +package com.ycwl.basic.voucher.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.voucher.entity.VoucherBatchEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface VoucherBatchMapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/mapper/VoucherCodeMapper.java b/src/main/java/com/ycwl/basic/voucher/mapper/VoucherCodeMapper.java new file mode 100644 index 0000000..f7c6011 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/mapper/VoucherCodeMapper.java @@ -0,0 +1,17 @@ +package com.ycwl.basic.voucher.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.voucher.entity.VoucherCodeEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface VoucherCodeMapper extends BaseMapper { + + @Select("SELECT COUNT(*) FROM voucher_code WHERE scenic_id = #{scenicId} AND face_id = #{faceId} AND status != 0 AND deleted = 0") + Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); + + @Select("SELECT * FROM voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1") + VoucherCodeEntity findFirstAvailableByBatchId(@Param("batchId") Long batchId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/service/VoucherBatchService.java b/src/main/java/com/ycwl/basic/voucher/service/VoucherBatchService.java new file mode 100644 index 0000000..063e0b8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/service/VoucherBatchService.java @@ -0,0 +1,27 @@ +package com.ycwl.basic.voucher.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp; +import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.voucher.entity.VoucherBatchEntity; + +public interface VoucherBatchService { + + Long createBatch(VoucherBatchCreateReq req); + + Page queryBatchList(VoucherBatchQueryReq req); + + VoucherBatchResp getBatchDetail(Long id); + + VoucherBatchStatsResp getBatchStats(Long id); + + void updateBatchStatus(Long id, Integer status); + + void updateBatchClaimedCount(Long batchId); + + void updateBatchUsedCount(Long batchId); + + VoucherBatchEntity getAvailableBatch(Long scenicId, Long brokerId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/service/VoucherCodeService.java b/src/main/java/com/ycwl/basic/voucher/service/VoucherCodeService.java new file mode 100644 index 0000000..118acf1 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/service/VoucherCodeService.java @@ -0,0 +1,23 @@ +package com.ycwl.basic.voucher.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.voucher.dto.req.VoucherClaimReq; +import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp; + +import java.util.List; + +public interface VoucherCodeService { + + void generateVoucherCodes(Long batchId, Long scenicId, Integer count); + + VoucherCodeResp claimVoucher(VoucherClaimReq req); + + Page queryCodeList(VoucherCodeQueryReq req); + + List getMyVoucherCodes(Long faceId); + + void markCodeAsUsed(Long codeId, String remark); + + boolean canClaimVoucher(Long faceId, Long scenicId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherBatchServiceImpl.java b/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherBatchServiceImpl.java new file mode 100644 index 0000000..0a4bf9d --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherBatchServiceImpl.java @@ -0,0 +1,190 @@ +package com.ycwl.basic.voucher.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.exception.BizException; +import com.ycwl.basic.interceptor.BaseContextHandler; +import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp; +import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.voucher.entity.VoucherBatchEntity; +import com.ycwl.basic.voucher.enums.VoucherDiscountType; +import com.ycwl.basic.voucher.mapper.VoucherBatchMapper; +import com.ycwl.basic.voucher.service.VoucherBatchService; +import com.ycwl.basic.voucher.service.VoucherCodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.Date; + +@Service +@RequiredArgsConstructor +public class VoucherBatchServiceImpl implements VoucherBatchService { + + private final VoucherBatchMapper voucherBatchMapper; + private final VoucherCodeService voucherCodeService; + + @Override + @Transactional + public Long createBatch(VoucherBatchCreateReq req) { + if (req.getBatchName() == null || req.getBatchName().trim().isEmpty()) { + throw new BizException(400, "券码批次名称不能为空"); + } + if (req.getScenicId() == null) { + throw new BizException(400, "景区ID不能为空"); + } + if (req.getBrokerId() == null) { + throw new BizException(400, "推客ID不能为空"); + } + if (req.getDiscountType() == null) { + throw new BizException(400, "优惠类型不能为空"); + } + if (req.getTotalCount() == null || req.getTotalCount() < 1) { + throw new BizException(400, "券码数量必须大于0"); + } + + VoucherDiscountType discountType = VoucherDiscountType.getByCode(req.getDiscountType()); + if (discountType == null) { + throw new BizException(400, "无效的优惠类型"); + } + + if (discountType != VoucherDiscountType.FREE_ALL && req.getDiscountValue() == null) { + throw new BizException(400, "优惠金额不能为空"); + } + + VoucherBatchEntity batch = new VoucherBatchEntity(); + BeanUtils.copyProperties(req, batch); + batch.setUsedCount(0); + batch.setClaimedCount(0); + batch.setStatus(1); + batch.setCreateTime(new Date()); + batch.setCreateBy(BaseContextHandler.getUserId()); + batch.setDeleted(0); + + voucherBatchMapper.insert(batch); + + voucherCodeService.generateVoucherCodes(batch.getId(), req.getScenicId(), req.getTotalCount()); + + return batch.getId(); + } + + @Override + public Page queryBatchList(VoucherBatchQueryReq req) { + Page page = new Page<>(req.getPageNum(), req.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(VoucherBatchEntity::getDeleted, 0) + .eq(req.getScenicId() != null, VoucherBatchEntity::getScenicId, req.getScenicId()) + .eq(req.getBrokerId() != null, VoucherBatchEntity::getBrokerId, req.getBrokerId()) + .eq(req.getStatus() != null, VoucherBatchEntity::getStatus, req.getStatus()) + .like(StringUtils.hasText(req.getBatchName()), VoucherBatchEntity::getBatchName, req.getBatchName()) + .orderByDesc(VoucherBatchEntity::getCreateTime); + + Page entityPage = voucherBatchMapper.selectPage(page, wrapper); + + Page respPage = new Page<>(); + BeanUtils.copyProperties(entityPage, respPage); + + respPage.setRecords(entityPage.getRecords().stream().map(this::convertToResp).toList()); + + return respPage; + } + + @Override + public VoucherBatchResp getBatchDetail(Long id) { + VoucherBatchEntity batch = voucherBatchMapper.selectById(id); + if (batch == null || batch.getDeleted() == 1) { + throw new BizException(404, "券码批次不存在"); + } + + return convertToResp(batch); + } + + @Override + public VoucherBatchStatsResp getBatchStats(Long id) { + VoucherBatchEntity batch = voucherBatchMapper.selectById(id); + if (batch == null || batch.getDeleted() == 1) { + throw new BizException(404, "券码批次不存在"); + } + + VoucherBatchStatsResp stats = new VoucherBatchStatsResp(); + stats.setBatchId(batch.getId()); + stats.setBatchName(batch.getBatchName()); + stats.setTotalCount(batch.getTotalCount()); + stats.setClaimedCount(batch.getClaimedCount()); + stats.setUsedCount(batch.getUsedCount()); + stats.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount()); + + if (batch.getTotalCount() > 0) { + stats.setClaimedRate((double) batch.getClaimedCount() / batch.getTotalCount() * 100); + stats.setUsedRate((double) batch.getUsedCount() / batch.getTotalCount() * 100); + } else { + stats.setClaimedRate(0.0); + stats.setUsedRate(0.0); + } + + return stats; + } + + @Override + public void updateBatchStatus(Long id, Integer status) { + VoucherBatchEntity batch = new VoucherBatchEntity(); + batch.setId(id); + batch.setStatus(status); + + int updated = voucherBatchMapper.updateById(batch); + if (updated == 0) { + throw new BizException(404, "券码批次不存在"); + } + } + + @Override + public void updateBatchClaimedCount(Long batchId) { + VoucherBatchEntity batch = voucherBatchMapper.selectById(batchId); + if (batch != null) { + batch.setClaimedCount(batch.getClaimedCount() + 1); + voucherBatchMapper.updateById(batch); + } + } + + @Override + public void updateBatchUsedCount(Long batchId) { + VoucherBatchEntity batch = voucherBatchMapper.selectById(batchId); + if (batch != null) { + batch.setUsedCount(batch.getUsedCount() + 1); + voucherBatchMapper.updateById(batch); + } + } + + @Override + public VoucherBatchEntity getAvailableBatch(Long scenicId, Long brokerId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(VoucherBatchEntity::getScenicId, scenicId) + .eq(VoucherBatchEntity::getBrokerId, brokerId) + .eq(VoucherBatchEntity::getStatus, 1) + .eq(VoucherBatchEntity::getDeleted, 0) + .lt(VoucherBatchEntity::getClaimedCount, VoucherBatchEntity::getTotalCount) + .orderByDesc(VoucherBatchEntity::getCreateTime); + + return voucherBatchMapper.selectOne(wrapper); + } + + private VoucherBatchResp convertToResp(VoucherBatchEntity batch) { + VoucherBatchResp resp = new VoucherBatchResp(); + BeanUtils.copyProperties(batch, resp); + + VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType()); + if (discountType != null) { + resp.setDiscountTypeName(discountType.getName()); + } + + resp.setStatusName(batch.getStatus() == 1 ? "启用" : "禁用"); + resp.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount()); + + return resp; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherCodeServiceImpl.java new file mode 100644 index 0000000..c30ed73 --- /dev/null +++ b/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherCodeServiceImpl.java @@ -0,0 +1,194 @@ +package com.ycwl.basic.voucher.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.exception.BizException; +import com.ycwl.basic.voucher.dto.req.VoucherClaimReq; +import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp; +import com.ycwl.basic.voucher.entity.VoucherBatchEntity; +import com.ycwl.basic.voucher.entity.VoucherCodeEntity; +import com.ycwl.basic.voucher.enums.VoucherCodeStatus; +import com.ycwl.basic.voucher.enums.VoucherDiscountType; +import com.ycwl.basic.voucher.mapper.VoucherBatchMapper; +import com.ycwl.basic.voucher.mapper.VoucherCodeMapper; +import com.ycwl.basic.voucher.service.VoucherBatchService; +import com.ycwl.basic.voucher.service.VoucherCodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class VoucherCodeServiceImpl implements VoucherCodeService { + + private final VoucherCodeMapper voucherCodeMapper; + private final VoucherBatchMapper voucherBatchMapper; + private final VoucherBatchService voucherBatchService; + + @Override + public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) { + List codes = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + VoucherCodeEntity code = new VoucherCodeEntity(); + code.setBatchId(batchId); + code.setScenicId(scenicId); + code.setCode(generateVoucherCode()); + code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode()); + code.setCreateTime(new Date()); + code.setDeleted(0); + codes.add(code); + } + + for (VoucherCodeEntity code : codes) { + voucherCodeMapper.insert(code); + } + } + + @Override + @Transactional + public VoucherCodeResp claimVoucher(VoucherClaimReq req) { + if (req.getScenicId() == null) { + throw new BizException(400, "景区ID不能为空"); + } + if (req.getBrokerId() == null) { + throw new BizException(400, "推客ID不能为空"); + } + if (req.getFaceId() == null) { + throw new BizException(400, "用户faceId不能为空"); + } + + if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) { + throw new BizException(400, "该用户在此景区已领取过券码"); + } + + VoucherBatchEntity batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId()); + if (batch == null) { + throw new BizException(400, "暂无可用券码批次"); + } + + VoucherCodeEntity availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId()); + if (availableCode == null) { + throw new BizException(400, "券码已领完"); + } + + availableCode.setFaceId(req.getFaceId()); + availableCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode()); + availableCode.setClaimedTime(new Date()); + + voucherCodeMapper.updateById(availableCode); + + voucherBatchService.updateBatchClaimedCount(batch.getId()); + + return convertToResp(availableCode, batch); + } + + @Override + public Page queryCodeList(VoucherCodeQueryReq req) { + Page page = new Page<>(req.getPageNum(), req.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(VoucherCodeEntity::getDeleted, 0) + .eq(req.getBatchId() != null, VoucherCodeEntity::getBatchId, req.getBatchId()) + .eq(req.getScenicId() != null, VoucherCodeEntity::getScenicId, req.getScenicId()) + .eq(req.getFaceId() != null, VoucherCodeEntity::getFaceId, req.getFaceId()) + .eq(req.getStatus() != null, VoucherCodeEntity::getStatus, req.getStatus()) + .like(StringUtils.hasText(req.getCode()), VoucherCodeEntity::getCode, req.getCode()) + .orderByDesc(VoucherCodeEntity::getCreateTime); + + Page entityPage = voucherCodeMapper.selectPage(page, wrapper); + + Page respPage = new Page<>(); + BeanUtils.copyProperties(entityPage, respPage); + + List respList = new ArrayList<>(); + for (VoucherCodeEntity code : entityPage.getRecords()) { + VoucherBatchEntity batch = voucherBatchMapper.selectById(code.getBatchId()); + respList.add(convertToResp(code, batch)); + } + respPage.setRecords(respList); + + return respPage; + } + + @Override + public List getMyVoucherCodes(Long faceId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(VoucherCodeEntity::getFaceId, faceId) + .eq(VoucherCodeEntity::getDeleted, 0) + .orderByDesc(VoucherCodeEntity::getClaimedTime); + + List codes = voucherCodeMapper.selectList(wrapper); + + List respList = new ArrayList<>(); + for (VoucherCodeEntity code : codes) { + VoucherBatchEntity batch = voucherBatchMapper.selectById(code.getBatchId()); + respList.add(convertToResp(code, batch)); + } + + return respList; + } + + @Override + @Transactional + public void markCodeAsUsed(Long codeId, String remark) { + VoucherCodeEntity code = voucherCodeMapper.selectById(codeId); + if (code == null || code.getDeleted() == 1) { + throw new BizException(404, "券码不存在"); + } + + if (code.getStatus() != VoucherCodeStatus.CLAIMED_UNUSED.getCode()) { + throw new BizException(400, "券码状态异常,无法使用"); + } + + code.setStatus(VoucherCodeStatus.USED.getCode()); + code.setUsedTime(new Date()); + code.setRemark(remark); + + voucherCodeMapper.updateById(code); + + voucherBatchService.updateBatchUsedCount(code.getBatchId()); + } + + @Override + public boolean canClaimVoucher(Long faceId, Long scenicId) { + Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId); + return count == 0; + } + + private String generateVoucherCode() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(); + } + + private VoucherCodeResp convertToResp(VoucherCodeEntity code, VoucherBatchEntity batch) { + VoucherCodeResp resp = new VoucherCodeResp(); + BeanUtils.copyProperties(code, resp); + + if (batch != null) { + resp.setBatchName(batch.getBatchName()); + resp.setDiscountType(batch.getDiscountType()); + resp.setDiscountValue(batch.getDiscountValue()); + + VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType()); + if (discountType != null) { + resp.setDiscountTypeName(discountType.getName()); + resp.setDiscountDescription(discountType.getDescription()); + } + } + + VoucherCodeStatus status = VoucherCodeStatus.getByCode(code.getStatus()); + if (status != null) { + resp.setStatusName(status.getName()); + } + + return resp; + } +} \ No newline at end of file From eb327723cd37458ac481d1d2d52319f021c32212 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 21 Aug 2025 09:35:08 +0800 Subject: [PATCH 09/18] =?UTF-8?q?feat(pricing):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=88=B8=E7=A0=81=E7=AE=A1=E7=90=86=E5=92=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增券码批次配置和券码实体 - 实现券码创建、领取、使用等接口 - 添加券码状态和优惠类型枚举 - 优化价格计算逻辑,支持券码优惠 - 新增优惠检测和应用相关功能 --- .../java/com/ycwl/basic/pricing/CLAUDE.md | 557 ++++++++++++++++++ .../VoucherManagementController.java} | 24 +- .../basic/pricing/dto/CouponUseResult.java | 4 +- .../dto/DiscountCombinationResult.java | 53 ++ .../basic/pricing/dto/DiscountDetail.java | 19 +- .../pricing/dto/DiscountDetectionContext.java | 53 ++ .../ycwl/basic/pricing/dto/DiscountInfo.java | 82 +++ .../basic/pricing/dto/DiscountResult.java | 43 ++ .../pricing/dto/PriceCalculationRequest.java | 25 + .../pricing/dto/PriceCalculationResult.java | 12 +- .../ycwl/basic/pricing/dto/VoucherInfo.java | 84 +++ .../dto/req/VoucherBatchCreateReq.java | 2 +- .../dto/req/VoucherBatchQueryReq.java | 2 +- .../dto/req/VoucherClaimReq.java | 2 +- .../dto/req/VoucherCodeQueryReq.java | 2 +- .../dto/resp/VoucherBatchResp.java | 2 +- .../dto/resp/VoucherBatchStatsResp.java | 2 +- .../dto/resp/VoucherCodeResp.java | 2 +- .../ycwl/basic/pricing/entity/BaseEntity.java | 14 +- .../entity/PriceCouponClaimRecord.java | 6 +- .../entity/PriceVoucherBatchConfig.java | 72 +++ .../pricing/entity/PriceVoucherCode.java | 61 ++ .../pricing/enums/VoucherCodeStatus.java | 74 +++ .../enums/VoucherDiscountType.java | 31 +- .../exception/DiscountDetectionException.java | 15 + .../exception/PricingExceptionHandler.java | 40 ++ .../VoucherAlreadyUsedException.java | 15 + .../exception/VoucherInvalidException.java | 15 + .../VoucherNotClaimableException.java | 15 + .../mapper/PriceCouponClaimRecordMapper.java | 2 +- .../mapper/PriceVoucherBatchConfigMapper.java | 47 ++ .../mapper/PriceVoucherCodeMapper.java | 94 +++ .../service/IDiscountDetectionService.java | 47 ++ .../pricing/service/IDiscountProvider.java | 61 ++ .../pricing/service/IVoucherService.java | 62 ++ .../service/VoucherBatchService.java | 14 +- .../service/VoucherCodeService.java | 8 +- .../service/impl/CouponDiscountProvider.java | 106 ++++ .../service/impl/CouponServiceImpl.java | 6 +- .../impl/DiscountDetectionServiceImpl.java | 206 +++++++ .../impl/PriceCalculationServiceImpl.java | 164 +++++- .../impl/PricingManagementServiceImpl.java | 34 +- .../service/impl/VoucherBatchServiceImpl.java | 79 +-- .../service/impl/VoucherCodeServiceImpl.java | 82 +-- .../service/impl/VoucherDiscountProvider.java | 175 ++++++ .../service/impl/VoucherServiceImpl.java | 223 +++++++ .../java/com/ycwl/basic/voucher/CLAUDE.md | 179 ------ .../voucher/entity/VoucherBatchEntity.java | 30 - .../voucher/entity/VoucherCodeEntity.java | 27 - .../voucher/enums/VoucherCodeStatus.java | 27 - .../voucher/mapper/VoucherBatchMapper.java | 9 - .../voucher/mapper/VoucherCodeMapper.java | 17 - 52 files changed, 2572 insertions(+), 455 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/pricing/CLAUDE.md rename src/main/java/com/ycwl/basic/{voucher/controller/VoucherController.java => pricing/controller/VoucherManagementController.java} (83%) create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/DiscountCombinationResult.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/DiscountDetectionContext.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java rename src/main/java/com/ycwl/basic/{voucher => pricing}/dto/req/VoucherBatchCreateReq.java (87%) rename src/main/java/com/ycwl/basic/{voucher => pricing}/dto/req/VoucherBatchQueryReq.java (89%) rename src/main/java/com/ycwl/basic/{voucher => pricing}/dto/req/VoucherClaimReq.java (77%) rename src/main/java/com/ycwl/basic/{voucher => pricing}/dto/req/VoucherCodeQueryReq.java (89%) rename src/main/java/com/ycwl/basic/{voucher => pricing}/dto/resp/VoucherBatchResp.java (93%) rename src/main/java/com/ycwl/basic/{voucher => pricing}/dto/resp/VoucherBatchStatsResp.java (88%) rename src/main/java/com/ycwl/basic/{voucher => pricing}/dto/resp/VoucherCodeResp.java (93%) create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java create mode 100644 src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java rename src/main/java/com/ycwl/basic/{voucher => pricing}/enums/VoucherDiscountType.java (54%) create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/DiscountDetectionException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/VoucherAlreadyUsedException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/VoucherInvalidException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/exception/VoucherNotClaimableException.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherBatchConfigMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IDiscountDetectionService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java rename src/main/java/com/ycwl/basic/{voucher => pricing}/service/VoucherBatchService.java (54%) rename src/main/java/com/ycwl/basic/{voucher => pricing}/service/VoucherCodeService.java (71%) create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/DiscountDetectionServiceImpl.java rename src/main/java/com/ycwl/basic/{voucher => pricing}/service/impl/VoucherBatchServiceImpl.java (64%) rename src/main/java/com/ycwl/basic/{voucher => pricing}/service/impl/VoucherCodeServiceImpl.java (63%) create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java delete mode 100644 src/main/java/com/ycwl/basic/voucher/CLAUDE.md delete mode 100644 src/main/java/com/ycwl/basic/voucher/entity/VoucherBatchEntity.java delete mode 100644 src/main/java/com/ycwl/basic/voucher/entity/VoucherCodeEntity.java delete mode 100644 src/main/java/com/ycwl/basic/voucher/enums/VoucherCodeStatus.java delete mode 100644 src/main/java/com/ycwl/basic/voucher/mapper/VoucherBatchMapper.java delete mode 100644 src/main/java/com/ycwl/basic/voucher/mapper/VoucherCodeMapper.java diff --git a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md new file mode 100644 index 0000000..c1c0ebf --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md @@ -0,0 +1,557 @@ +# 价格查询系统 (Pricing Module) 开发指南 + +此文档为pricing包的专用开发指南,提供该模块的详细架构说明和开发最佳实践。 + +## 模块概览 + +价格查询系统 (`com.ycwl.basic.pricing`) 是一个独立的业务模块,提供商品定价、优惠券管理、券码管理和价格计算功能。采用分层架构设计,具备完整的CRUD操作、异常处理和数据统计功能。支持优惠券和券码的同时使用,券码优先级更高。 + +## 目录结构 + +``` +com.ycwl.basic.pricing/ +├── controller/ # REST API控制器层 +│ ├── CouponManagementController.java # 优惠券管理API +│ ├── PriceCalculationController.java # 价格计算API +│ └── PricingConfigController.java # 价格配置管理API +├── dto/ # 数据传输对象 +│ ├── BundleProductItem.java # 套餐商品项 +│ ├── CouponInfo.java # 优惠券信息 +│ ├── CouponUseRequest.java # 优惠券使用请求 +│ ├── CouponUseResult.java # 优惠券使用结果 +│ ├── DiscountDetail.java # 折扣详情 +│ ├── PriceCalculationRequest.java # 价格计算请求 +│ ├── PriceCalculationResult.java # 价格计算结果 +│ ├── PriceDetails.java # 价格详情 +│ ├── ProductItem.java # 商品项 +│ ├── ProductPriceInfo.java # 商品价格信息 +│ ├── VoucherInfo.java # 券码信息 +│ ├── DiscountDetectionContext.java # 优惠检测上下文 +│ ├── DiscountInfo.java # 优惠信息 +│ ├── DiscountResult.java # 优惠结果 +│ └── DiscountCombinationResult.java # 优惠组合结果 +├── entity/ # 数据库实体类 +│ ├── BaseEntity.java # 基础实体类 +│ ├── PriceBundleConfig.java # 套餐配置 +│ ├── PriceCouponClaimRecord.java # 优惠券领取记录 +│ ├── PriceCouponConfig.java # 优惠券配置 +│ ├── PriceProductConfig.java # 商品价格配置 +│ ├── PriceTierConfig.java # 分层价格配置 +│ ├── PriceVoucherBatchConfig.java # 券码批次配置 +│ └── PriceVoucherCode.java # 券码实体 +├── enums/ # 枚举类 +│ ├── CouponStatus.java # 优惠券状态 +│ ├── CouponType.java # 优惠券类型 +│ ├── ProductType.java # 商品类型 +│ ├── VoucherDiscountType.java # 券码优惠类型 +│ └── VoucherCodeStatus.java # 券码状态 +├── exception/ # 异常处理 +│ ├── CouponInvalidException.java # 优惠券无效异常 +│ ├── PriceCalculationException.java # 价格计算异常 +│ ├── PricingExceptionHandler.java # 定价异常处理器 +│ ├── ProductConfigNotFoundException.java # 商品配置未找到异常 +│ ├── VoucherInvalidException.java # 券码无效异常 +│ ├── VoucherAlreadyUsedException.java # 券码已使用异常 +│ ├── VoucherNotClaimableException.java # 券码不可领取异常 +│ └── DiscountDetectionException.java # 优惠检测异常 +├── handler/ # 自定义处理器 +│ └── BundleProductListTypeHandler.java # 套餐商品列表类型处理器 +├── mapper/ # MyBatis数据访问层 +│ ├── PriceBundleConfigMapper.java +│ ├── PriceCouponClaimRecordMapper.java +│ ├── PriceCouponConfigMapper.java +│ ├── PriceProductConfigMapper.java +│ ├── PriceTierConfigMapper.java +│ ├── PriceVoucherBatchConfigMapper.java +│ └── PriceVoucherCodeMapper.java +└── service/ # 业务逻辑层 + ├── ICouponManagementService.java # 优惠券管理服务接口 + ├── ICouponService.java # 优惠券服务接口 + ├── IPriceBundleService.java # 套餐服务接口 + ├── IPriceCalculationService.java # 价格计算服务接口 + ├── IPricingManagementService.java # 定价管理服务接口 + ├── IProductConfigService.java # 商品配置服务接口 + ├── IVoucherService.java # 券码服务接口 + ├── IDiscountProvider.java # 优惠提供者接口 + ├── IDiscountDetectionService.java # 优惠检测服务接口 + └── impl/ # 服务实现类 + ├── CouponManagementServiceImpl.java + ├── CouponServiceImpl.java + ├── PriceBundleServiceImpl.java + ├── PriceCalculationServiceImpl.java + ├── PricingManagementServiceImpl.java + ├── ProductConfigServiceImpl.java + ├── VoucherServiceImpl.java + ├── CouponDiscountProvider.java + ├── VoucherDiscountProvider.java + └── DiscountDetectionServiceImpl.java +``` + +## 核心功能 + +### 1. 价格计算引擎 + +#### API端点 +- `POST /api/pricing/calculate` - 执行价格计算 + +#### 计算流程 +```java +// 价格计算核心流程 +1. 验证PriceCalculationRequest请求参数 +2. 加载商品基础配置 (PriceProductConfig) +3. 应用分层定价规则 (PriceTierConfig) +4. 处理套餐商品逻辑 (BundleProductItem) +5. 使用统一优惠检测系统处理券码和优惠券 +6. 按优先级应用优惠:券码 > 优惠券 +7. 计算最终价格并返回详细结果 +``` + +#### 关键类 +- `PriceCalculationService`: 价格计算核心逻辑 +- `PriceCalculationRequest`: 计算请求DTO +- `PriceCalculationResult`: 计算结果DTO + +### 2. 优惠券管理系统 + +#### API端点 +- `GET /api/pricing/admin/coupons/` - 分页查询优惠券配置 +- `POST /api/pricing/admin/coupons/` - 创建优惠券配置 +- `PUT /api/pricing/admin/coupons/{id}` - 更新优惠券配置 +- `DELETE /api/pricing/admin/coupons/{id}` - 删除优惠券配置 +- `GET /api/pricing/admin/coupons/{id}/claims` - 查询优惠券领取记录 +- `GET /api/pricing/admin/coupons/{id}/stats` - 获取优惠券统计信息 + +#### 优惠券类型 +```java +public enum CouponType { + PERCENTAGE, // 百分比折扣 + FIXED_AMOUNT // 固定金额减免 +} + +public enum CouponStatus { + CLAIMED, // 已领取 + USED, // 已使用 + EXPIRED // 已过期 +} +``` + +#### 关键特性 +- **商品类型限制**: 通过`applicableProducts` JSON字段控制适用商品 +- **消费限制**: 支持最小消费金额和最大折扣限制 +- **时效性**: 基于时间的有效期控制 +- **统计分析**: 完整的使用统计和分析功能 + +### 3. 商品配置管理 + +#### API端点 +- `GET /api/pricing/config/products` - 查询商品配置 +- `POST /api/pricing/config/products` - 创建商品配置 +- `PUT /api/pricing/config/products/{id}` - 更新商品配置 + +#### 商品类型定义 +```java +public enum ProductType { + VLOG_VIDEO("vlog_video", "Vlog视频"), + RECORDING_SET("recording_set", "录像集"), + PHOTO_SET("photo_set", "照相集"), + PHOTO_PRINT("photo_print", "照片打印"), + MACHINE_PRINT("machine_print", "一体机打印"); +} +``` + +#### 分层定价 +支持基于数量的分层定价策略,通过`PriceTierConfig`实体配置不同数量区间的单价。 + +## 开发最佳实践 + +### 1. 添加新商品类型 + +```java +// 步骤1: 在ProductType枚举中添加新类型 +public enum ProductType { + // 现有类型... + NEW_PRODUCT("new_product", "新商品类型"); +} + +// 步骤2: 在数据库中添加default配置 +INSERT INTO price_product_config (product_type, base_price, ...) +VALUES ('new_product', 100.00, ...); + +// 步骤3: 添加分层定价配置(可选) +INSERT INTO price_tier_config (product_type, min_quantity, max_quantity, unit_price, ...) +VALUES ('new_product', 1, 10, 95.00, ...); + +// 步骤4: 更新前端产品类型映射 +``` + +### 2. 扩展优惠券类型 + +```java +// 步骤1: 在CouponType枚举中添加新类型 +public enum CouponType { + PERCENTAGE, + FIXED_AMOUNT, + NEW_COUPON_TYPE // 新增类型 +} + +// 步骤2: 在CouponServiceImpl中实现计算逻辑 +@Override +public BigDecimal calculateDiscount(CouponConfig coupon, BigDecimal originalPrice) { + return switch (coupon.getCouponType()) { + case PERCENTAGE -> calculatePercentageDiscount(coupon, originalPrice); + case FIXED_AMOUNT -> calculateFixedAmountDiscount(coupon, originalPrice); + case NEW_COUPON_TYPE -> calculateNewTypeDiscount(coupon, originalPrice); + }; +} + +// 步骤3: 更新applicableProducts验证规则 +``` + +### 3. 自定义TypeHandler使用 + +项目使用MyBatis自定义TypeHandler处理复杂JSON字段: + +```java +// BundleProductListTypeHandler处理套餐商品列表 +@MappedTypes(List.class) +@MappedJdbcTypes(JdbcType.VARCHAR) +public class BundleProductListTypeHandler extends BaseTypeHandler> { + // JSON序列化/反序列化逻辑 +} + +// 在实体类中使用 +public class SomeEntity { + @TableField(typeHandler = BundleProductListTypeHandler.class) + private List bundleProducts; +} +``` + +### 4. 异常处理模式 + +```java +// 自定义异常类 +public class PriceCalculationException extends RuntimeException { + public PriceCalculationException(String message) { + super(message); + } +} + +// 在PricingExceptionHandler中统一处理 +@ExceptionHandler(PriceCalculationException.class) +public ApiResponse handlePriceCalculationException(PriceCalculationException e) { + return ApiResponse.error(ErrorCode.PRICE_CALCULATION_ERROR, e.getMessage()); +} +``` + +### 5. 分页查询实现 + +```java +// 使用PageHelper实现分页 +@Override +public PageInfo getCouponsByPage(int pageNum, int pageSize, String status, String name) { + PageHelper.startPage(pageNum, pageSize); + + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (StringUtils.hasText(status)) { + queryWrapper.eq("status", status); + } + if (StringUtils.hasText(name)) { + queryWrapper.like("name", name); + } + + List list = couponConfigMapper.selectList(queryWrapper); + return new PageInfo<>(list); +} +``` + +## 测试策略 + +### 1. 单元测试 +每个服务类都应有对应的测试类,重点测试: +- 价格计算逻辑的准确性 +- 优惠券适用性验证 +- 边界条件处理 +- 异常场景覆盖 + +### 2. 集成测试 +- 数据库操作测试 +- JSON序列化测试 +- 分页功能测试 +- API端点测试 + +### 3. 配置验证测试 +- `DefaultConfigValidationTest`: 验证default配置的完整性和正确性 +- 确保所有ProductType都有对应的基础配置 + +## 数据库设计 + +### 核心表结构 +- `price_product_config`: 商品价格基础配置 +- `price_tier_config`: 分层定价配置 +- `price_bundle_config`: 套餐配置 +- `price_coupon_config`: 优惠券配置 +- `price_coupon_claim_record`: 优惠券领取记录 + +### 关键字段设计 +- JSON字段处理: `applicable_products`使用JSON存储适用商品类型列表 +- 时间字段: 统一使用`LocalDateTime`类型 +- 价格字段: 使用`BigDecimal`确保精度 +- 状态字段: 使用枚举类型确保数据一致性 + +## 性能优化建议 + +1. **数据库查询优化** + - 为常用查询字段添加索引 + - 使用分页查询避免大量数据加载 + - 优化复杂的JOIN查询 + +2. **缓存策略** + - 对商品配置进行缓存,减少数据库访问 + - 使用Redis缓存热点优惠券信息 + +3. **计算性能** + - 价格计算使用BigDecimal确保精度 + - 批量计算时考虑并行处理 + +## 安全考虑 + +1. **输入验证** + - 严格验证所有输入参数 + - 防止SQL注入和XSS攻击 + +2. **权限控制** + - 管理接口需要适当的权限验证 + - 用户只能访问自己的优惠券记录 + +3. **数据完整性** + - 使用事务确保数据一致性 + - 关键操作添加审计日志 + +## 券码管理系统 (Voucher System) + +### 1. 核心特性 + +券码系统是从原`voucher`包迁移而来,现已完全集成到pricing包中,与优惠券系统并行工作。 + +#### 券码优惠类型 +```java +public enum VoucherDiscountType { + FREE_ALL(0, "全场免费"), // 所有商品免费,优先级最高且不可叠加 + REDUCE_PRICE(1, "商品降价"), // 每个商品减免固定金额 + DISCOUNT(2, "商品打折") // 每个商品按百分比打折 +} +``` + +#### 券码状态流转 +``` +UNCLAIMED(0) → CLAIMED_UNUSED(1) → USED(2) + 未领取 → 已领取未使用 → 已使用 +``` + +### 2. 数据库表结构 + +#### 券码批次配置表 (price_voucher_batch_config) +- 批次管理:支持按景区、推客创建券码批次 +- 统计功能:实时统计已领取数、已使用数 +- 状态控制:支持启用/禁用批次 + +#### 券码表 (price_voucher_code) +- 唯一性约束:每个券码全局唯一 +- 用户限制:同一用户在同一景区只能领取一次券码 +- 时间追踪:记录领取时间、使用时间 + +### 3. 关键业务规则 + +#### 领取限制 +- 同一`faceId`在同一`scenicId`中只能领取一次券码 +- 只有启用状态的批次才能领取券码 +- 批次必须有可用券码才能成功领取 + +#### 使用验证 +- 券码必须是`CLAIMED_UNUSED`状态才能使用 +- 必须验证券码与景区的匹配关系 +- 使用后自动更新批次统计数据 + +## 统一优惠检测系统 (Unified Discount Detection) + +### 1. 架构设计 + +采用策略模式设计的可扩展优惠检测系统,支持多种优惠类型的统一管理和自动优化组合。 + +#### 核心接口 +```java +// 优惠提供者接口 +public interface IDiscountProvider { + String getProviderType(); // 提供者类型 + int getPriority(); // 优先级(数字越大越高) + List detectAvailableDiscounts(); // 检测可用优惠 + DiscountResult applyDiscount(); // 应用优惠 +} + +// 优惠检测服务接口 +public interface IDiscountDetectionService { + DiscountCombinationResult calculateOptimalCombination(); // 计算最优组合 + DiscountCombinationResult previewOptimalCombination(); // 预览优惠组合 +} +``` + +### 2. 优惠提供者实现 + +#### VoucherDiscountProvider (优先级: 100) +- 处理券码优惠逻辑 +- 支持用户主动输入券码或自动选择最优券码 +- 全场免费券码不可与其他优惠叠加 + +#### CouponDiscountProvider (优先级: 80) +- 处理优惠券优惠逻辑 +- 自动选择最优优惠券 +- 可与券码叠加使用(除全场免费券码外) + +### 3. 优惠应用策略 + +#### 优先级规则 +``` +券码优惠 (Priority: 100) → 优惠券优惠 (Priority: 80) +``` + +#### 叠加逻辑 +```java +原价 → 应用券码优惠 → 应用优惠券优惠 → 最终价格 + +特殊情况: +- 全场免费券码:最终价格直接为0,不再应用其他优惠 +- 其他券码类型:可与优惠券叠加使用 +``` + +#### 显示顺序 +``` +1. 券码优惠 (sortOrder: 1) +2. 限时立减 (sortOrder: 2) +3. 优惠券优惠 (sortOrder: 3) +4. 一口价优惠 (sortOrder: 4) +``` + +### 4. 扩展支持 + +#### 添加新优惠类型 +```java +@Component +public class FlashSaleDiscountProvider implements IDiscountProvider { + @Override + public String getProviderType() { return "FLASH_SALE"; } + + @Override + public int getPriority() { return 90; } // 介于券码和优惠券之间 + + // 实现其他方法... +} +``` + +#### 动态注册 +```java +// 系统启动时自动扫描所有IDiscountProvider实现类 +// 按优先级排序并注册到DiscountDetectionService中 +``` + +## API接口扩展 + +### 1. 价格计算接口扩展 + +#### 新增请求参数 +```java +public class PriceCalculationRequest { + // 原有字段... + private String voucherCode; // 用户输入的券码 + private Long scenicId; // 景区ID + private Long faceId; // 用户faceId + private Boolean autoUseVoucher; // 是否自动使用券码 + private Boolean previewOnly; // 是否仅预览优惠 +} +``` + +#### 新增响应字段 +```java +public class PriceCalculationResult { + // 原有字段... + private VoucherInfo usedVoucher; // 使用的券码信息 + private List availableDiscounts; // 可用优惠列表(预览模式) +} +``` + +### 2. 券码管理接口 + +#### 移动端接口 +- `POST /api/pricing/mobile/voucher/claim` - 领取券码 +- `GET /api/pricing/mobile/voucher/my-codes` - 我的券码列表 + +#### 管理端接口 +- `POST /api/pricing/admin/voucher/batch/create` - 创建券码批次 +- `GET /api/pricing/admin/voucher/batch/list` - 批次列表查询 +- `GET /api/pricing/admin/voucher/codes` - 券码列表查询 + +## 开发最佳实践更新 + +### 1. 优惠检测开发 +```java +// 检测上下文构建 +DiscountDetectionContext context = new DiscountDetectionContext(); +context.setUserId(userId); +context.setFaceId(faceId); +context.setScenicId(scenicId); +context.setProducts(products); +context.setCurrentAmount(amount); +context.setVoucherCode(voucherCode); + +// 使用统一服务检测优惠 +DiscountCombinationResult result = discountDetectionService + .calculateOptimalCombination(context); +``` + +### 2. 券码服务使用 +```java +// 验证券码 +VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo( + voucherCode, faceId, scenicId); + +// 计算券码优惠 +BigDecimal discount = voucherService.calculateVoucherDiscount( + voucherInfo, context); + +// 标记券码已使用 +voucherService.markVoucherAsUsed(voucherCode, "订单使用"); +``` + +### 3. 异常处理扩展 +```java +// 券码相关异常 +try { + // 券码操作 +} catch (VoucherInvalidException e) { + // 券码无效 +} catch (VoucherAlreadyUsedException e) { + // 券码已使用 +} catch (VoucherNotClaimableException e) { + // 券码不可领取 +} +``` + +## 数据库扩展 + +### 新增表结构 +- `price_voucher_batch_config`: 券码批次配置表 +- `price_voucher_code`: 券码表 + +### 索引优化 +```sql +-- 券码查询优化 +CREATE INDEX idx_voucher_code ON price_voucher_code(code); +CREATE INDEX idx_face_scenic ON price_voucher_code(face_id, scenic_id); + +-- 批次查询优化 +CREATE INDEX idx_scenic_broker ON price_voucher_batch_config(scenic_id, broker_id); +``` + +### 性能考虑 +- 券码表可能数据量较大,考虑按景区分表 +- 定期清理已删除的过期数据 +- 使用数据完整性检查SQL验证统计数据准确性 \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/controller/VoucherController.java b/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java similarity index 83% rename from src/main/java/com/ycwl/basic/voucher/controller/VoucherController.java rename to src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java index fea2ab1..dda20ab 100644 --- a/src/main/java/com/ycwl/basic/voucher/controller/VoucherController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java @@ -1,25 +1,25 @@ -package com.ycwl.basic.voucher.controller; +package com.ycwl.basic.pricing.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; +import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; +import com.ycwl.basic.pricing.service.VoucherBatchService; +import com.ycwl.basic.pricing.service.VoucherCodeService; import com.ycwl.basic.utils.ApiResponse; -import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq; -import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq; -import com.ycwl.basic.voucher.dto.req.VoucherClaimReq; -import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq; -import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp; -import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp; -import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp; -import com.ycwl.basic.voucher.service.VoucherBatchService; -import com.ycwl.basic.voucher.service.VoucherCodeService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController -@RequestMapping("/api/voucher") +@RequestMapping("/api/pricing/voucher") @RequiredArgsConstructor -public class VoucherController { +public class VoucherManagementController { private final VoucherBatchService voucherBatchService; private final VoucherCodeService voucherCodeService; diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java index b5df283..a82aa00 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponUseResult.java @@ -3,7 +3,7 @@ package com.ycwl.basic.pricing.dto; import lombok.Data; import java.math.BigDecimal; -import java.time.LocalDateTime; +import java.util.Date; /** * 优惠券使用结果DTO @@ -29,7 +29,7 @@ public class CouponUseResult { /** * 使用时间 */ - private LocalDateTime useTime; + private Date useTime; /** * 优惠金额 diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountCombinationResult.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountCombinationResult.java new file mode 100644 index 0000000..7478b80 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountCombinationResult.java @@ -0,0 +1,53 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠组合结果 + */ +@Data +public class DiscountCombinationResult { + + /** + * 原始金额 + */ + private BigDecimal originalAmount; + + /** + * 最终金额 + */ + private BigDecimal finalAmount; + + /** + * 总优惠金额 + */ + private BigDecimal totalDiscountAmount; + + /** + * 应用的优惠列表(按优先级排序) + */ + private List appliedDiscounts; + + /** + * 可用但未应用的优惠列表 + */ + private List availableDiscounts; + + /** + * 优惠详情列表(用于展示) + */ + private List discountDetails; + + /** + * 计算是否成功 + */ + private Boolean success; + + /** + * 错误信息(如果success为false) + */ + private String errorMessage; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java index 30954e7..e7bacc8 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java @@ -44,7 +44,7 @@ public class DiscountDetail { detail.setDiscountName("限时立减"); detail.setDiscountAmount(discountAmount); detail.setDescription("限时优惠,立即享受"); - detail.setSortOrder(1); // 限时立减排在最前面 + detail.setSortOrder(2); // 限时立减排在券码后面 return detail; } @@ -57,7 +57,20 @@ public class DiscountDetail { detail.setDiscountName(couponName); detail.setDiscountAmount(discountAmount); detail.setDescription("优惠券减免"); - detail.setSortOrder(2); // 优惠券排在限时立减后面 + detail.setSortOrder(3); // 优惠券排在限时立减后面 + return detail; + } + + /** + * 创建券码折扣明细 + */ + public static DiscountDetail createVoucherDiscount(String voucherCode, String discountTypeName, BigDecimal discountAmount) { + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType("VOUCHER"); + detail.setDiscountName("券码优惠"); + detail.setDiscountAmount(discountAmount); + detail.setDescription(String.format("券码 %s - %s", voucherCode, discountTypeName)); + detail.setSortOrder(1); // 券码优先级最高,排在最前面 return detail; } @@ -70,7 +83,7 @@ public class DiscountDetail { detail.setDiscountName("一口价优惠"); detail.setDiscountAmount(discountAmount); detail.setDescription("一口价购买更优惠"); - detail.setSortOrder(3); // 一口价排在最后 + detail.setSortOrder(4); // 一口价排在最后 return detail; } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetectionContext.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetectionContext.java new file mode 100644 index 0000000..a0a08ea --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetectionContext.java @@ -0,0 +1,53 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠检测上下文 + */ +@Data +public class DiscountDetectionContext { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户faceId + */ + private Long faceId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 商品列表 + */ + private List products; + + /** + * 当前金额 + */ + private BigDecimal currentAmount; + + /** + * 用户主动输入的券码 + */ + private String voucherCode; + + /** + * 是否自动使用优惠券 + */ + private Boolean autoUseCoupon; + + /** + * 是否自动使用券码 + */ + private Boolean autoUseVoucher; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java new file mode 100644 index 0000000..f5d59c3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java @@ -0,0 +1,82 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 优惠信息DTO + */ +@Data +public class DiscountInfo { + + /** + * 优惠ID + */ + private Long discountId; + + /** + * 优惠类型(COUPON, VOUCHER等) + */ + private String discountType; + + /** + * 优惠名称 + */ + private String discountName; + + /** + * 优惠描述 + */ + private String discountDescription; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 原始优惠值(用于百分比折扣等) + */ + private BigDecimal originalValue; + + /** + * 优惠值类型(PERCENTAGE, FIXED_AMOUNT等) + */ + private String valueType; + + /** + * 最小消费金额限制 + */ + private BigDecimal minAmount; + + /** + * 最大优惠金额限制 + */ + private BigDecimal maxDiscount; + + /** + * 优惠提供者类型 + */ + private String providerType; + + /** + * 优惠优先级 + */ + private Integer priority; + + /** + * 是否可与其他优惠叠加 + */ + private Boolean stackable; + + /** + * 券码(如果是voucher类型) + */ + private String voucherCode; + + /** + * 优惠券ID(如果是coupon类型) + */ + private Long couponId; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java new file mode 100644 index 0000000..e59c835 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountResult.java @@ -0,0 +1,43 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 优惠应用结果 + */ +@Data +public class DiscountResult { + + /** + * 应用的优惠信息 + */ + private DiscountInfo discountInfo; + + /** + * 实际优惠金额 + */ + private BigDecimal actualDiscountAmount; + + /** + * 应用后的金额 + */ + private BigDecimal finalAmount; + + /** + * 是否成功应用 + */ + private Boolean success; + + /** + * 失败原因(如果success为false) + */ + private String failureReason; + + /** + * 影响的商品项(用于商品级别的优惠) + */ + private List affectedProducts; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java index 72b2048..9563334 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationRequest.java @@ -24,4 +24,29 @@ public class PriceCalculationRequest { * 是否自动使用优惠券 */ private Boolean autoUseCoupon = true; + + /** + * 用户输入的券码 + */ + private String voucherCode; + + /** + * 景区ID(用于券码验证) + */ + private Long scenicId; + + /** + * 用户faceId(用于券码领取资格验证) + */ + private Long faceId; + + /** + * 是否自动使用券码优惠 + */ + private Boolean autoUseVoucher = true; + + /** + * 是否仅预览优惠(不实际使用) + */ + private Boolean previewOnly = false; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java index bd00cdb..c23e8bb 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java @@ -37,10 +37,20 @@ public class PriceCalculationResult { private CouponInfo usedCoupon; /** - * 折扣明细列表(包含限时立减、优惠券、一口价等) + * 使用的券码信息 + */ + private VoucherInfo usedVoucher; + + /** + * 折扣明细列表(包含限时立减、优惠券、券码、一口价等) */ private List discountDetails; + /** + * 可用但未使用的优惠列表(预览时使用) + */ + private List availableDiscounts; + /** * 商品明细列表 */ diff --git a/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java new file mode 100644 index 0000000..b4e80b8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java @@ -0,0 +1,84 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 券码信息DTO + */ +@Data +public class VoucherInfo { + + /** + * 券码ID + */ + private Long voucherId; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 批次ID + */ + private Long batchId; + + /** + * 批次名称 + */ + private String batchName; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 推客ID + */ + private Long brokerId; + + /** + * 优惠类型 + */ + private VoucherDiscountType discountType; + + /** + * 优惠值 + */ + private BigDecimal discountValue; + + /** + * 实际优惠金额 + */ + private BigDecimal actualDiscountAmount; + + /** + * 状态 + */ + private Integer status; + + /** + * 领取时间 + */ + private Date claimedTime; + + /** + * 使用时间 + */ + private Date usedTime; + + /** + * 是否可用 + */ + private Boolean available; + + /** + * 不可用原因 + */ + private String unavailableReason; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchCreateReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReq.java similarity index 87% rename from src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchCreateReq.java rename to src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReq.java index 47c1653..8582fea 100644 --- a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchCreateReq.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReq.java @@ -1,4 +1,4 @@ -package com.ycwl.basic.voucher.dto.req; +package com.ycwl.basic.pricing.dto.req; import lombok.Data; diff --git a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchQueryReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchQueryReq.java similarity index 89% rename from src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchQueryReq.java rename to src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchQueryReq.java index ce82a73..426c947 100644 --- a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherBatchQueryReq.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchQueryReq.java @@ -1,4 +1,4 @@ -package com.ycwl.basic.voucher.dto.req; +package com.ycwl.basic.pricing.dto.req; import com.ycwl.basic.model.common.BaseQueryParameterReq; import lombok.Data; diff --git a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherClaimReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java similarity index 77% rename from src/main/java/com/ycwl/basic/voucher/dto/req/VoucherClaimReq.java rename to src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java index e6d318d..385b1ac 100644 --- a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherClaimReq.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java @@ -1,4 +1,4 @@ -package com.ycwl.basic.voucher.dto.req; +package com.ycwl.basic.pricing.dto.req; import lombok.Data; diff --git a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherCodeQueryReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherCodeQueryReq.java similarity index 89% rename from src/main/java/com/ycwl/basic/voucher/dto/req/VoucherCodeQueryReq.java rename to src/main/java/com/ycwl/basic/pricing/dto/req/VoucherCodeQueryReq.java index ea853bd..a0065c5 100644 --- a/src/main/java/com/ycwl/basic/voucher/dto/req/VoucherCodeQueryReq.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherCodeQueryReq.java @@ -1,4 +1,4 @@ -package com.ycwl.basic.voucher.dto.req; +package com.ycwl.basic.pricing.dto.req; import com.ycwl.basic.model.common.BaseQueryParameterReq; import lombok.Data; diff --git a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java similarity index 93% rename from src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchResp.java rename to src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java index cbee09d..7c75781 100644 --- a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchResp.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java @@ -1,4 +1,4 @@ -package com.ycwl.basic.voucher.dto.resp; +package com.ycwl.basic.pricing.dto.resp; import lombok.Data; diff --git a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchStatsResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchStatsResp.java similarity index 88% rename from src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchStatsResp.java rename to src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchStatsResp.java index 285aee1..423036c 100644 --- a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherBatchStatsResp.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchStatsResp.java @@ -1,4 +1,4 @@ -package com.ycwl.basic.voucher.dto.resp; +package com.ycwl.basic.pricing.dto.resp; import lombok.Data; diff --git a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherCodeResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java similarity index 93% rename from src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherCodeResp.java rename to src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java index 6b9a498..b7f0f8a 100644 --- a/src/main/java/com/ycwl/basic/voucher/dto/resp/VoucherCodeResp.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java @@ -1,4 +1,4 @@ -package com.ycwl.basic.voucher.dto.resp; +package com.ycwl.basic.pricing.dto.resp; import lombok.Data; diff --git a/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java b/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java index b66c468..849b036 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java @@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; -import java.time.LocalDateTime; +import java.util.Date; /** * 基础实体类 @@ -15,7 +15,15 @@ public class BaseEntity { @TableId(type = IdType.AUTO) private Long id; - private LocalDateTime createdTime; + private Date createdTime; - private LocalDateTime updatedTime; + private Date updatedTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java index 22f1824..a48511d 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java @@ -5,7 +5,7 @@ import com.ycwl.basic.pricing.enums.CouponStatus; import lombok.Data; import lombok.EqualsAndHashCode; -import java.time.LocalDateTime; +import java.util.Date; /** * 优惠券领用记录实体 @@ -28,12 +28,12 @@ public class PriceCouponClaimRecord extends BaseEntity { /** * 领取时间 */ - private LocalDateTime claimTime; + private Date claimTime; /** * 使用时间 */ - private LocalDateTime useTime; + private Date useTime; /** * 订单ID diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java new file mode 100644 index 0000000..a68510c --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java @@ -0,0 +1,72 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 券码批次配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_voucher_batch_config") +public class PriceVoucherBatchConfig extends BaseEntity { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 券码批次名称 + */ + private String batchName; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 推客ID + */ + private Long brokerId; + + /** + * 优惠类型:0=全场免费,1=商品降价,2=商品打折 + */ + private Integer discountType; + + /** + * 优惠值(降价金额或折扣百分比) + */ + private BigDecimal discountValue; + + /** + * 总券码数量 + */ + private Integer totalCount; + + /** + * 已使用数量 + */ + private Integer usedCount; + + /** + * 已领取数量 + */ + private Integer claimedCount; + + /** + * 状态:0=禁用,1=启用 + */ + private Integer status; + + /** + * 创建人ID + */ + private Long createBy; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java new file mode 100644 index 0000000..6efcbdc --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java @@ -0,0 +1,61 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 券码实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("price_voucher_code") +public class PriceVoucherCode extends BaseEntity { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 批次ID + */ + private Long batchId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 券码 + */ + private String code; + + /** + * 状态:0=未领取,1=已领取未使用,2=已使用 + */ + private Integer status; + + /** + * 领取人faceId + */ + private Long faceId; + + /** + * 领取时间 + */ + private Date claimedTime; + + /** + * 使用时间 + */ + private Date usedTime; + + /** + * 使用备注 + */ + private String remark; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java b/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java new file mode 100644 index 0000000..b4651bb --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java @@ -0,0 +1,74 @@ +package com.ycwl.basic.pricing.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 券码状态枚举 + */ +@Getter +@AllArgsConstructor +public enum VoucherCodeStatus { + + /** + * 未领取 + */ + UNCLAIMED(0, "未领取"), + + /** + * 已领取未使用 + */ + CLAIMED_UNUSED(1, "已领取未使用"), + + /** + * 已使用 + */ + USED(2, "已使用"); + + private final Integer code; + private final String name; + + /** + * 根据代码获取枚举值 + * @param code 代码 + * @return 枚举值 + */ + public static VoucherCodeStatus getByCode(Integer code) { + if (code == null) { + return null; + } + for (VoucherCodeStatus status : values()) { + if (status.getCode().equals(code)) { + return status; + } + } + return null; + } + + /** + * 检查是否为有效的状态代码 + * @param code 代码 + * @return 是否有效 + */ + public static boolean isValidCode(Integer code) { + return getByCode(code) != null; + } + + /** + * 检查是否可以使用(已领取未使用状态) + * @param code 状态代码 + * @return 是否可以使用 + */ + public static boolean canUse(Integer code) { + return CLAIMED_UNUSED.getCode().equals(code); + } + + /** + * 检查是否已使用 + * @param code 状态代码 + * @return 是否已使用 + */ + public static boolean isUsed(Integer code) { + return USED.getCode().equals(code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/enums/VoucherDiscountType.java b/src/main/java/com/ycwl/basic/pricing/enums/VoucherDiscountType.java similarity index 54% rename from src/main/java/com/ycwl/basic/voucher/enums/VoucherDiscountType.java rename to src/main/java/com/ycwl/basic/pricing/enums/VoucherDiscountType.java index 5e53d8d..af9fdfc 100644 --- a/src/main/java/com/ycwl/basic/voucher/enums/VoucherDiscountType.java +++ b/src/main/java/com/ycwl/basic/pricing/enums/VoucherDiscountType.java @@ -1,19 +1,39 @@ -package com.ycwl.basic.voucher.enums; +package com.ycwl.basic.pricing.enums; import lombok.AllArgsConstructor; import lombok.Getter; +/** + * 券码优惠类型枚举 + */ @Getter @AllArgsConstructor public enum VoucherDiscountType { + + /** + * 全场免费 - 所有商品免费 + */ FREE_ALL(0, "全场免费", "所有商品免费"), + + /** + * 商品降价 - 每个商品减免指定金额 + */ REDUCE_PRICE(1, "商品降价", "每个商品减免指定金额"), + + /** + * 商品打折 - 每个商品按百分比打折 + */ DISCOUNT(2, "商品打折", "每个商品按百分比打折"); private final Integer code; private final String name; private final String description; + /** + * 根据代码获取枚举值 + * @param code 代码 + * @return 枚举值 + */ public static VoucherDiscountType getByCode(Integer code) { if (code == null) { return null; @@ -25,4 +45,13 @@ public enum VoucherDiscountType { } return null; } + + /** + * 检查是否为有效的优惠类型代码 + * @param code 代码 + * @return 是否有效 + */ + public static boolean isValidCode(Integer code) { + return getByCode(code) != null; + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/DiscountDetectionException.java b/src/main/java/com/ycwl/basic/pricing/exception/DiscountDetectionException.java new file mode 100644 index 0000000..b1b2852 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/DiscountDetectionException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 优惠检测异常 + */ +public class DiscountDetectionException extends RuntimeException { + + public DiscountDetectionException(String message) { + super(message); + } + + public DiscountDetectionException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java b/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java index d4acdd0..9b84eb9 100644 --- a/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java +++ b/src/main/java/com/ycwl/basic/pricing/exception/PricingExceptionHandler.java @@ -54,6 +54,46 @@ public class PricingExceptionHandler { return ApiResponse.buildResponse(400, "参数错误: " + e.getMessage()); } + /** + * 处理券码无效异常 + */ + @ExceptionHandler(VoucherInvalidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleVoucherInvalidException(VoucherInvalidException e) { + log.error("券码无效异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理券码已使用异常 + */ + @ExceptionHandler(VoucherAlreadyUsedException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleVoucherAlreadyUsedException(VoucherAlreadyUsedException e) { + log.error("券码已使用异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理券码不可领取异常 + */ + @ExceptionHandler(VoucherNotClaimableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleVoucherNotClaimableException(VoucherNotClaimableException e) { + log.error("券码不可领取异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + + /** + * 处理优惠检测异常 + */ + @ExceptionHandler(DiscountDetectionException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleDiscountDetectionException(DiscountDetectionException e) { + log.error("优惠检测异常", e); + return ApiResponse.buildResponse(400, e.getMessage()); + } + /** * 处理通用异常 */ diff --git a/src/main/java/com/ycwl/basic/pricing/exception/VoucherAlreadyUsedException.java b/src/main/java/com/ycwl/basic/pricing/exception/VoucherAlreadyUsedException.java new file mode 100644 index 0000000..2067de0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/VoucherAlreadyUsedException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 券码已使用异常 + */ +public class VoucherAlreadyUsedException extends RuntimeException { + + public VoucherAlreadyUsedException(String message) { + super(message); + } + + public VoucherAlreadyUsedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/VoucherInvalidException.java b/src/main/java/com/ycwl/basic/pricing/exception/VoucherInvalidException.java new file mode 100644 index 0000000..6c62288 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/VoucherInvalidException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 券码无效异常 + */ +public class VoucherInvalidException extends RuntimeException { + + public VoucherInvalidException(String message) { + super(message); + } + + public VoucherInvalidException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/exception/VoucherNotClaimableException.java b/src/main/java/com/ycwl/basic/pricing/exception/VoucherNotClaimableException.java new file mode 100644 index 0000000..2ea2e2d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/exception/VoucherNotClaimableException.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.pricing.exception; + +/** + * 券码不可领取异常 + */ +public class VoucherNotClaimableException extends RuntimeException { + + public VoucherNotClaimableException(String message) { + super(message); + } + + public VoucherNotClaimableException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java index b95bf3d..725c9f7 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java @@ -44,7 +44,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper { + + /** + * 根据景区ID和推客ID查询有效的批次列表 + * @param scenicId 景区ID + * @param brokerId 推客ID + * @return 批次列表 + */ + List selectActiveBatchesByScenicAndBroker(@Param("scenicId") Long scenicId, + @Param("brokerId") Long brokerId); + + /** + * 更新批次的已领取数量 + * @param batchId 批次ID + * @param increment 增量(可为负数) + * @return 影响行数 + */ + int updateClaimedCount(@Param("batchId") Long batchId, @Param("increment") Integer increment); + + /** + * 更新批次的已使用数量 + * @param batchId 批次ID + * @param increment 增量(可为负数) + * @return 影响行数 + */ + int updateUsedCount(@Param("batchId") Long batchId, @Param("increment") Integer increment); + + /** + * 获取批次统计信息 + * @param batchId 批次ID + * @return 统计信息 + */ + PriceVoucherBatchConfig selectBatchStats(@Param("batchId") Long batchId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java new file mode 100644 index 0000000..732d414 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java @@ -0,0 +1,94 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 券码Mapper + */ +@Mapper +public interface PriceVoucherCodeMapper extends BaseMapper { + + /** + * 根据券码查询券码信息 + * @param code 券码 + * @return 券码信息 + */ + PriceVoucherCode selectByCode(@Param("code") String code); + + /** + * 根据faceId和scenicId统计已领取的券码数量 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 数量 + */ + Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); + + /** + * 查询用户在指定景区的可用券码列表 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 券码列表 + */ + List selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId, + @Param("scenicId") Long scenicId); + + /** + * 根据批次ID获取可领取的券码(未领取状态) + * @param batchId 批次ID + * @param limit 限制数量 + * @return 券码列表 + */ + List selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId, + @Param("limit") Integer limit); + + /** + * 领取券码(更新状态为已领取) + * @param id 券码ID + * @param faceId 用户faceId + * @param claimedTime 领取时间 + * @return 影响行数 + */ + int claimVoucher(@Param("id") Long id, + @Param("faceId") Long faceId, + @Param("claimedTime") LocalDateTime claimedTime); + + /** + * 使用券码(更新状态为已使用) + * @param code 券码 + * @param usedTime 使用时间 + * @param remark 使用备注 + * @return 影响行数 + */ + int useVoucher(@Param("code") String code, + @Param("usedTime") LocalDateTime usedTime, + @Param("remark") String remark); + + /** + * 根据批次ID查询券码列表(支持分页) + * @param batchId 批次ID + * @return 券码列表 + */ + List selectByBatchId(@Param("batchId") Long batchId); + + /** + * 查询用户的券码列表 + * @param faceId 用户faceId + * @param scenicId 景区ID(可选) + * @return 券码列表 + */ + List selectUserVouchers(@Param("faceId") Long faceId, + @Param("scenicId") Long scenicId); + + /** + * 根据批次ID查询第一个可用的券码 + * @param batchId 批次ID + * @return 可用券码 + */ + PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IDiscountDetectionService.java b/src/main/java/com/ycwl/basic/pricing/service/IDiscountDetectionService.java new file mode 100644 index 0000000..549d21b --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IDiscountDetectionService.java @@ -0,0 +1,47 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.DiscountCombinationResult; +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.DiscountInfo; + +import java.util.List; + +/** + * 优惠检测服务接口 + * 负责协调所有优惠提供者,计算最优优惠组合 + */ +public interface IDiscountDetectionService { + + /** + * 检测所有可用的优惠 + * @param context 检测上下文 + * @return 所有可用的优惠列表 + */ + List detectAllAvailableDiscounts(DiscountDetectionContext context); + + /** + * 计算最优优惠组合 + * @param context 检测上下文 + * @return 最优优惠组合结果 + */ + DiscountCombinationResult calculateOptimalCombination(DiscountDetectionContext context); + + /** + * 预览优惠组合(不实际应用) + * @param context 检测上下文 + * @return 预览结果 + */ + DiscountCombinationResult previewOptimalCombination(DiscountDetectionContext context); + + /** + * 注册优惠提供者 + * @param provider 优惠提供者 + */ + void registerProvider(IDiscountProvider provider); + + /** + * 获取所有已注册的优惠提供者 + * @return 优惠提供者列表 + */ + List getAllProviders(); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java new file mode 100644 index 0000000..4d2a562 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java @@ -0,0 +1,61 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.DiscountInfo; +import com.ycwl.basic.pricing.dto.DiscountResult; + +import java.util.List; + +/** + * 优惠提供者接口 + * 所有优惠类型(coupon、voucher、促销活动等)都需要实现此接口 + */ +public interface IDiscountProvider { + + /** + * 获取提供者类型 + * @return 提供者类型标识,如 "COUPON", "VOUCHER", "FLASH_SALE" 等 + */ + String getProviderType(); + + /** + * 获取优先级 + * @return 优先级,数字越大优先级越高 + */ + int getPriority(); + + /** + * 检测可用的优惠 + * @param context 优惠检测上下文 + * @return 可用的优惠列表 + */ + List detectAvailableDiscounts(DiscountDetectionContext context); + + /** + * 应用优惠 + * @param discountInfo 要应用的优惠信息 + * @param context 优惠检测上下文 + * @return 优惠应用结果 + */ + DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context); + + /** + * 验证优惠是否可以应用 + * @param discountInfo 优惠信息 + * @param context 优惠检测上下文 + * @return 是否可以应用 + */ + default boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) { + return true; + } + + /** + * 获取优惠的最大可能折扣金额(用于排序) + * @param discountInfo 优惠信息 + * @param context 优惠检测上下文 + * @return 最大可能折扣金额 + */ + default java.math.BigDecimal getMaxPossibleDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) { + return discountInfo.getDiscountAmount() != null ? discountInfo.getDiscountAmount() : java.math.BigDecimal.ZERO; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java new file mode 100644 index 0000000..51be47e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java @@ -0,0 +1,62 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.VoucherInfo; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 券码服务接口 + */ +public interface IVoucherService { + + /** + * 验证并获取券码信息 + * @param voucherCode 券码 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 券码信息(如果有效) + */ + VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId); + + /** + * 获取用户在指定景区的可用券码列表 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 可用券码列表 + */ + List getAvailableVouchers(Long faceId, Long scenicId); + + /** + * 标记券码为已使用 + * @param voucherCode 券码 + * @param remark 使用备注 + */ + void markVoucherAsUsed(String voucherCode, String remark); + + /** + * 检查用户是否可以在指定景区领取券码 + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 是否可以领取 + */ + boolean canClaimVoucher(Long faceId, Long scenicId); + + /** + * 计算券码优惠金额 + * @param voucherInfo 券码信息 + * @param context 检测上下文 + * @return 优惠金额 + */ + BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context); + + /** + * 获取最优的券码(如果用户有多个可用券码) + * @param faceId 用户faceId + * @param scenicId 景区ID + * @param context 检测上下文 + * @return 最优券码信息 + */ + VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/service/VoucherBatchService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java similarity index 54% rename from src/main/java/com/ycwl/basic/voucher/service/VoucherBatchService.java rename to src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java index 063e0b8..f54d9be 100644 --- a/src/main/java/com/ycwl/basic/voucher/service/VoucherBatchService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java @@ -1,11 +1,11 @@ -package com.ycwl.basic.voucher.service; +package com.ycwl.basic.pricing.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq; -import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq; -import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp; -import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp; -import com.ycwl.basic.voucher.entity.VoucherBatchEntity; +import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; public interface VoucherBatchService { @@ -23,5 +23,5 @@ public interface VoucherBatchService { void updateBatchUsedCount(Long batchId); - VoucherBatchEntity getAvailableBatch(Long scenicId, Long brokerId); + PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/service/VoucherCodeService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherCodeService.java similarity index 71% rename from src/main/java/com/ycwl/basic/voucher/service/VoucherCodeService.java rename to src/main/java/com/ycwl/basic/pricing/service/VoucherCodeService.java index 118acf1..27854f5 100644 --- a/src/main/java/com/ycwl/basic/voucher/service/VoucherCodeService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherCodeService.java @@ -1,9 +1,9 @@ -package com.ycwl.basic.voucher.service; +package com.ycwl.basic.pricing.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.ycwl.basic.voucher.dto.req.VoucherClaimReq; -import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq; -import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp; +import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; +import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; import java.util.List; 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 new file mode 100644 index 0000000..1e88cf8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponDiscountProvider.java @@ -0,0 +1,106 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.service.ICouponService; +import com.ycwl.basic.pricing.service.IDiscountProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 优惠券折扣提供者 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CouponDiscountProvider implements IDiscountProvider { + + private final ICouponService couponService; + + @Override + public String getProviderType() { + return "COUPON"; + } + + @Override + public int getPriority() { + return 80; // 优惠券优先级为80,低于券码的100 + } + + @Override + public List detectAvailableDiscounts(DiscountDetectionContext context) { + List discounts = new ArrayList<>(); + + if (!Boolean.TRUE.equals(context.getAutoUseCoupon()) || context.getUserId() == null) { + return discounts; + } + + try { + CouponInfo bestCoupon = couponService.selectBestCoupon( + context.getUserId(), + context.getProducts(), + context.getCurrentAmount() + ); + + if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + DiscountInfo discountInfo = new DiscountInfo(); + discountInfo.setDiscountId(bestCoupon.getCouponId()); + discountInfo.setDiscountType("COUPON"); + discountInfo.setDiscountName(bestCoupon.getCouponName()); + discountInfo.setDiscountDescription("优惠券减免"); + discountInfo.setDiscountAmount(bestCoupon.getActualDiscountAmount()); + discountInfo.setProviderType(getProviderType()); + discountInfo.setPriority(getPriority()); + discountInfo.setStackable(true); // 优惠券可与券码叠加 + discountInfo.setCouponId(bestCoupon.getCouponId()); + + discounts.add(discountInfo); + } + } catch (Exception e) { + log.warn("检测优惠券时发生异常", e); + } + + return discounts; + } + + @Override + public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) { + DiscountResult result = new DiscountResult(); + result.setDiscountInfo(discountInfo); + + try { + // 应用优惠券逻辑 + BigDecimal actualDiscount = discountInfo.getDiscountAmount(); + BigDecimal finalAmount = context.getCurrentAmount().subtract(actualDiscount); + + if (finalAmount.compareTo(BigDecimal.ZERO) < 0) { + finalAmount = BigDecimal.ZERO; + actualDiscount = context.getCurrentAmount(); + } + + result.setActualDiscountAmount(actualDiscount); + result.setFinalAmount(finalAmount); + result.setSuccess(true); + + log.info("成功应用优惠券: {}, 优惠金额: {}", discountInfo.getDiscountName(), actualDiscount); + + } catch (Exception e) { + log.error("应用优惠券失败: " + discountInfo.getDiscountName(), e); + result.setSuccess(false); + result.setFailureReason("优惠券应用失败: " + e.getMessage()); + } + + return result; + } + + @Override + public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) { + return "COUPON".equals(discountInfo.getDiscountType()) && + Boolean.TRUE.equals(context.getAutoUseCoupon()) && + context.getUserId() != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java index a908f27..c28335c 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java @@ -13,6 +13,8 @@ import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper; import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper; import com.ycwl.basic.pricing.service.ICouponService; import lombok.RequiredArgsConstructor; + +import java.util.Date; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -144,13 +146,13 @@ public class CouponServiceImpl implements ICouponService { throw new CouponInvalidException("优惠券使用失败,可能已达到使用上限"); } - LocalDateTime useTime = LocalDateTime.now(); + Date useTime = new Date(); // 设置使用时间、订单信息和景区信息 record.setStatus(CouponStatus.USED); record.setUseTime(useTime); record.setOrderId(request.getOrderId()); - record.setUpdatedTime(LocalDateTime.now()); + record.setUpdatedTime(new Date()); // 如果请求中包含景区ID,记录到使用记录中 if (request.getScenicId() != null && !request.getScenicId().isEmpty()) { diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/DiscountDetectionServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/DiscountDetectionServiceImpl.java new file mode 100644 index 0000000..111c7ef --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/DiscountDetectionServiceImpl.java @@ -0,0 +1,206 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.service.IDiscountDetectionService; +import com.ycwl.basic.pricing.service.IDiscountProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 优惠检测服务实现 + */ +@Slf4j +@Service +public class DiscountDetectionServiceImpl implements IDiscountDetectionService { + + private final List discountProviders = new ArrayList<>(); + + @Autowired + public DiscountDetectionServiceImpl(List providers) { + this.discountProviders.addAll(providers); + // 按优先级排序(优先级高的在前) + this.discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed()); + + log.info("注册了 {} 个优惠提供者: {}", + providers.size(), + providers.stream().map(IDiscountProvider::getProviderType).collect(Collectors.toList())); + } + + @Override + public List detectAllAvailableDiscounts(DiscountDetectionContext context) { + List allDiscounts = new ArrayList<>(); + + for (IDiscountProvider provider : discountProviders) { + try { + List providerDiscounts = provider.detectAvailableDiscounts(context); + if (providerDiscounts != null && !providerDiscounts.isEmpty()) { + allDiscounts.addAll(providerDiscounts); + log.debug("提供者 {} 检测到 {} 个优惠", provider.getProviderType(), providerDiscounts.size()); + } + } catch (Exception e) { + log.error("优惠提供者 {} 检测失败", provider.getProviderType(), e); + } + } + + // 按优先级排序 + allDiscounts.sort(Comparator.comparing(DiscountInfo::getPriority).reversed()); + + return allDiscounts; + } + + @Override + public DiscountCombinationResult calculateOptimalCombination(DiscountDetectionContext context) { + DiscountCombinationResult result = new DiscountCombinationResult(); + result.setOriginalAmount(context.getCurrentAmount()); + + try { + List availableDiscounts = detectAllAvailableDiscounts(context); + result.setAvailableDiscounts(availableDiscounts); + + if (availableDiscounts.isEmpty()) { + result.setFinalAmount(context.getCurrentAmount()); + result.setTotalDiscountAmount(BigDecimal.ZERO); + result.setAppliedDiscounts(new ArrayList<>()); + result.setDiscountDetails(new ArrayList<>()); + result.setSuccess(true); + return result; + } + + List appliedDiscounts = new ArrayList<>(); + List discountDetails = new ArrayList<>(); + BigDecimal currentAmount = context.getCurrentAmount(); + + // 按优先级应用优惠 + for (DiscountInfo discountInfo : availableDiscounts) { + IDiscountProvider provider = findProvider(discountInfo.getProviderType()); + if (provider == null || !provider.canApply(discountInfo, context)) { + continue; + } + + // 更新上下文中的当前金额 + context.setCurrentAmount(currentAmount); + + DiscountResult discountResult = provider.applyDiscount(discountInfo, context); + if (Boolean.TRUE.equals(discountResult.getSuccess())) { + appliedDiscounts.add(discountResult); + + // 创建显示用的优惠详情 + DiscountDetail detail = createDiscountDetail(discountResult); + if (detail != null) { + discountDetails.add(detail); + } + + // 更新当前金额 + currentAmount = discountResult.getFinalAmount(); + + log.info("成功应用优惠: {} - {}, 优惠金额: {}", + discountInfo.getProviderType(), + discountInfo.getDiscountName(), + discountResult.getActualDiscountAmount()); + + // 如果是不可叠加的优惠(如全场免费),则停止应用其他优惠 + if (!Boolean.TRUE.equals(discountInfo.getStackable())) { + log.info("遇到不可叠加优惠,停止应用其他优惠: {}", discountInfo.getDiscountName()); + break; + } + } else { + log.warn("优惠应用失败: {} - {}, 原因: {}", + discountInfo.getProviderType(), + discountInfo.getDiscountName(), + discountResult.getFailureReason()); + } + } + + // 计算总优惠金额 + BigDecimal totalDiscountAmount = appliedDiscounts.stream() + .map(DiscountResult::getActualDiscountAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 按显示顺序排序折扣详情 + discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder)); + + result.setFinalAmount(currentAmount); + result.setTotalDiscountAmount(totalDiscountAmount); + result.setAppliedDiscounts(appliedDiscounts); + result.setDiscountDetails(discountDetails); + result.setSuccess(true); + + } catch (Exception e) { + log.error("计算最优优惠组合失败", e); + result.setSuccess(false); + result.setErrorMessage("优惠计算失败: " + e.getMessage()); + result.setFinalAmount(context.getCurrentAmount()); + result.setTotalDiscountAmount(BigDecimal.ZERO); + } + + return result; + } + + @Override + public DiscountCombinationResult previewOptimalCombination(DiscountDetectionContext context) { + // 预览模式与正常计算相同,但不会实际标记优惠为已使用 + return calculateOptimalCombination(context); + } + + @Override + public void registerProvider(IDiscountProvider provider) { + if (provider != null && !discountProviders.contains(provider)) { + discountProviders.add(provider); + // 重新排序 + discountProviders.sort(Comparator.comparing(IDiscountProvider::getPriority).reversed()); + log.info("注册新的优惠提供者: {}", provider.getProviderType()); + } + } + + @Override + public List getAllProviders() { + return new ArrayList<>(discountProviders); + } + + /** + * 查找指定类型的优惠提供者 + */ + private IDiscountProvider findProvider(String providerType) { + return discountProviders.stream() + .filter(provider -> providerType.equals(provider.getProviderType())) + .findFirst() + .orElse(null); + } + + /** + * 创建显示用的优惠详情 + */ + private DiscountDetail createDiscountDetail(DiscountResult discountResult) { + DiscountInfo discountInfo = discountResult.getDiscountInfo(); + String providerType = discountInfo.getProviderType(); + + return switch (providerType) { + case "VOUCHER" -> DiscountDetail.createVoucherDiscount( + discountInfo.getVoucherCode(), + discountInfo.getDiscountDescription(), + discountResult.getActualDiscountAmount() + ); + case "COUPON" -> DiscountDetail.createCouponDiscount( + discountInfo.getDiscountName(), + discountResult.getActualDiscountAmount() + ); + default -> { + // 其他类型的优惠,创建通用的折扣详情 + DiscountDetail detail = new DiscountDetail(); + detail.setDiscountType(providerType); + detail.setDiscountName(discountInfo.getDiscountName()); + detail.setDiscountAmount(discountResult.getActualDiscountAmount()); + detail.setDescription(discountInfo.getDiscountDescription()); + detail.setSortOrder(10); // 默认排序 + yield detail; + } + }; + } +} \ No newline at end of file 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 59795ee..4eaac53 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 @@ -5,10 +5,7 @@ import com.ycwl.basic.pricing.entity.PriceProductConfig; import com.ycwl.basic.pricing.entity.PriceTierConfig; import com.ycwl.basic.pricing.enums.ProductType; import com.ycwl.basic.pricing.exception.PriceCalculationException; -import com.ycwl.basic.pricing.service.ICouponService; -import com.ycwl.basic.pricing.service.IPriceBundleService; -import com.ycwl.basic.pricing.service.IPriceCalculationService; -import com.ycwl.basic.pricing.service.IProductConfigService; +import com.ycwl.basic.pricing.service.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -30,6 +27,8 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { private final IProductConfigService productConfigService; private final ICouponService couponService; private final IPriceBundleService bundleService; + private final IDiscountDetectionService discountDetectionService; + private final IVoucherService voucherService; @Override public PriceCalculationResult calculatePrice(PriceCalculationRequest request) { @@ -59,35 +58,60 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount); } + // 构建价格计算结果 PriceCalculationResult result = new PriceCalculationResult(); result.setOriginalAmount(originalTotalAmount); // 原总价 result.setSubtotalAmount(priceDetails.getTotalAmount()); // 商品小计 result.setProductDetails(request.getProducts()); - // 处理优惠券 - BigDecimal couponDiscountAmount = BigDecimal.ZERO; - if (Boolean.TRUE.equals(request.getAutoUseCoupon()) && request.getUserId() != null) { - CouponInfo bestCoupon = couponService.selectBestCoupon( - request.getUserId(), request.getProducts(), totalAmount); - - if (bestCoupon != null && bestCoupon.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { - result.setUsedCoupon(bestCoupon); - couponDiscountAmount = bestCoupon.getActualDiscountAmount(); - discountDetails.add(DiscountDetail.createCouponDiscount(bestCoupon.getCouponName(), couponDiscountAmount)); - } - } + // 使用新的优惠检测系统处理所有优惠(券码 + 优惠券) + DiscountCombinationResult discountResult = calculateDiscounts(request, totalAmount); - // 计算总优惠金额 - BigDecimal totalDiscountAmount = discountDetails.stream() + if (Boolean.TRUE.equals(discountResult.getSuccess())) { + // 合并所有优惠详情 + List allDiscountDetails = new ArrayList<>(discountDetails); + if (discountResult.getDiscountDetails() != null) { + allDiscountDetails.addAll(discountResult.getDiscountDetails()); + } + + // 重新排序 + allDiscountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder)); + + // 计算总优惠金额(包括限时立减、一口价和其他优惠) + BigDecimal totalDiscountAmount = allDiscountDetails.stream() .map(DiscountDetail::getDiscountAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 设置结果 + result.setDiscountAmount(totalDiscountAmount); + result.setDiscountDetails(allDiscountDetails); + result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount)); + + // 设置使用的券码和优惠券信息 + setUsedDiscountInfo(result, discountResult, request); + + // 如果是预览模式,设置可用优惠列表 + if (Boolean.TRUE.equals(request.getPreviewOnly())) { + result.setAvailableDiscounts(discountResult.getAvailableDiscounts()); + } + + } else { + log.warn("优惠计算失败: {}", discountResult.getErrorMessage()); + + // 降级处理:仅使用基础优惠(限时立减、一口价) + BigDecimal totalDiscountAmount = discountDetails.stream() + .map(DiscountDetail::getDiscountAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + result.setDiscountAmount(totalDiscountAmount); + result.setDiscountDetails(discountDetails); + result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount)); + } - // 按排序排列折扣明细 - discountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder)); - - result.setDiscountAmount(totalDiscountAmount); - result.setDiscountDetails(discountDetails); - result.setFinalAmount(originalTotalAmount.subtract(totalDiscountAmount)); + // 标记使用的优惠(仅在非预览模式下) + if (!Boolean.TRUE.equals(request.getPreviewOnly())) { + markDiscountsAsUsed(result, request); + } return result; } @@ -269,4 +293,96 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { return new ProductPriceInfo(actualPrice, originalPrice); } + + /** + * 计算优惠(券码 + 优惠券) + */ + private DiscountCombinationResult calculateDiscounts(PriceCalculationRequest request, BigDecimal currentAmount) { + try { + // 构建优惠检测上下文 + DiscountDetectionContext context = new DiscountDetectionContext(); + context.setUserId(request.getUserId()); + context.setFaceId(request.getFaceId()); + context.setScenicId(request.getScenicId()); + context.setProducts(request.getProducts()); + context.setCurrentAmount(currentAmount); + context.setVoucherCode(request.getVoucherCode()); + context.setAutoUseCoupon(request.getAutoUseCoupon()); + context.setAutoUseVoucher(request.getAutoUseVoucher()); + + // 使用优惠检测服务计算最优组合 + if (Boolean.TRUE.equals(request.getPreviewOnly())) { + return discountDetectionService.previewOptimalCombination(context); + } else { + return discountDetectionService.calculateOptimalCombination(context); + } + + } catch (Exception e) { + log.error("计算优惠时发生异常", e); + + // 返回失败结果 + DiscountCombinationResult failureResult = new DiscountCombinationResult(); + failureResult.setOriginalAmount(currentAmount); + failureResult.setFinalAmount(currentAmount); + failureResult.setTotalDiscountAmount(BigDecimal.ZERO); + failureResult.setSuccess(false); + failureResult.setErrorMessage("优惠计算失败: " + e.getMessage()); + return failureResult; + } + } + + /** + * 设置使用的优惠信息到结果中 + */ + private void setUsedDiscountInfo(PriceCalculationResult result, DiscountCombinationResult discountResult, PriceCalculationRequest request) { + if (discountResult.getAppliedDiscounts() == null) { + return; + } + + for (DiscountResult discountApplied : discountResult.getAppliedDiscounts()) { + DiscountInfo discountInfo = discountApplied.getDiscountInfo(); + + if ("COUPON".equals(discountInfo.getProviderType()) && discountInfo.getCouponId() != null) { + // 构建优惠券信息(这里可能需要重新查询完整信息) + CouponInfo couponInfo = new CouponInfo(); + couponInfo.setCouponId(discountInfo.getCouponId()); + couponInfo.setCouponName(discountInfo.getDiscountName()); + couponInfo.setActualDiscountAmount(discountApplied.getActualDiscountAmount()); + result.setUsedCoupon(couponInfo); + + } else if ("VOUCHER".equals(discountInfo.getProviderType()) && discountInfo.getVoucherCode() != null) { + // 获取券码信息 + VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo( + discountInfo.getVoucherCode(), + request.getFaceId(), + request.getScenicId() + ); + if (voucherInfo != null) { + voucherInfo.setActualDiscountAmount(discountApplied.getActualDiscountAmount()); + result.setUsedVoucher(voucherInfo); + } + } + } + } + + /** + * 标记优惠为已使用(仅在非预览模式下调用) + */ + private void markDiscountsAsUsed(PriceCalculationResult result, PriceCalculationRequest request) { + try { + // 标记券码为已使用 + if (result.getUsedVoucher() != null && result.getUsedVoucher().getVoucherCode() != null) { + String remark = String.format("价格计算使用 - 订单金额: %s", result.getFinalAmount()); + voucherService.markVoucherAsUsed(result.getUsedVoucher().getVoucherCode(), remark); + log.info("已标记券码为使用: {}", result.getUsedVoucher().getVoucherCode()); + } + + // 优惠券的使用标记由原有的CouponService处理 + // 这里不需要额外处理 + + } catch (Exception e) { + log.error("标记优惠使用状态时发生异常", e); + // 不抛出异常,避免影响主流程 + } + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java index 12bfec9..20f0fcc 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java @@ -8,7 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; +import java.util.Date; /** * 价格管理服务实现(用于配置管理,手动处理时间字段) @@ -35,8 +35,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService { } } - config.setCreatedTime(LocalDateTime.now()); - config.setUpdatedTime(LocalDateTime.now()); + config.setCreatedTime(new Date()); + config.setUpdatedTime(new Date()); productConfigMapper.insertProductConfig(config); return config.getId(); } @@ -44,7 +44,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateProductConfig(PriceProductConfig config) { - config.setUpdatedTime(LocalDateTime.now()); + config.setUpdatedTime(new Date()); return productConfigMapper.updateProductConfig(config) > 0; } @@ -58,8 +58,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService { config.getProductType(), config.getMinQuantity(), config.getMaxQuantity()); } - config.setCreatedTime(LocalDateTime.now()); - config.setUpdatedTime(LocalDateTime.now()); + config.setCreatedTime(new Date()); + config.setUpdatedTime(new Date()); tierConfigMapper.insertTierConfig(config); return config.getId(); } @@ -67,15 +67,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateTierConfig(PriceTierConfig config) { - config.setUpdatedTime(LocalDateTime.now()); + config.setUpdatedTime(new Date()); return tierConfigMapper.updateTierConfig(config) > 0; } @Override @Transactional public Long createCouponConfig(PriceCouponConfig config) { - config.setCreatedTime(LocalDateTime.now()); - config.setUpdatedTime(LocalDateTime.now()); + config.setCreatedTime(new Date()); + config.setUpdatedTime(new Date()); couponConfigMapper.insertCoupon(config); return config.getId(); } @@ -83,16 +83,16 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateCouponConfig(PriceCouponConfig config) { - config.setUpdatedTime(LocalDateTime.now()); + config.setUpdatedTime(new Date()); return couponConfigMapper.updateCoupon(config) > 0; } @Override @Transactional public Long createCouponClaimRecord(PriceCouponClaimRecord record) { - record.setClaimTime(LocalDateTime.now()); - record.setCreatedTime(LocalDateTime.now()); - record.setUpdatedTime(LocalDateTime.now()); + record.setClaimTime(new Date()); + record.setCreatedTime(new Date()); + record.setUpdatedTime(new Date()); couponClaimRecordMapper.insertClaimRecord(record); return record.getId(); } @@ -100,15 +100,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) { - record.setUpdatedTime(LocalDateTime.now()); + record.setUpdatedTime(new Date()); return couponClaimRecordMapper.updateClaimRecord(record) > 0; } @Override @Transactional public Long createBundleConfig(PriceBundleConfig config) { - config.setCreatedTime(LocalDateTime.now()); - config.setUpdatedTime(LocalDateTime.now()); + config.setCreatedTime(new Date()); + config.setUpdatedTime(new Date()); bundleConfigMapper.insertBundleConfig(config); return config.getId(); } @@ -116,7 +116,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateBundleConfig(PriceBundleConfig config) { - config.setUpdatedTime(LocalDateTime.now()); + config.setUpdatedTime(new Date()); return bundleConfigMapper.updateBundleConfig(config) > 0; } diff --git a/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherBatchServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java similarity index 64% rename from src/main/java/com/ycwl/basic/voucher/service/impl/VoucherBatchServiceImpl.java rename to src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java index 0a4bf9d..8f1d2de 100644 --- a/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherBatchServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java @@ -1,18 +1,18 @@ -package com.ycwl.basic.voucher.service.impl; +package com.ycwl.basic.pricing.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.exception.BizException; -import com.ycwl.basic.interceptor.BaseContextHandler; -import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq; -import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq; -import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp; -import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp; -import com.ycwl.basic.voucher.entity.VoucherBatchEntity; -import com.ycwl.basic.voucher.enums.VoucherDiscountType; -import com.ycwl.basic.voucher.mapper.VoucherBatchMapper; -import com.ycwl.basic.voucher.service.VoucherBatchService; -import com.ycwl.basic.voucher.service.VoucherCodeService; +import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp; +import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper; +import com.ycwl.basic.pricing.service.VoucherBatchService; +import com.ycwl.basic.pricing.service.VoucherCodeService; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; @@ -25,7 +25,7 @@ import java.util.Date; @RequiredArgsConstructor public class VoucherBatchServiceImpl implements VoucherBatchService { - private final VoucherBatchMapper voucherBatchMapper; + private final PriceVoucherBatchConfigMapper voucherBatchMapper; private final VoucherCodeService voucherCodeService; @Override @@ -56,13 +56,16 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { throw new BizException(400, "优惠金额不能为空"); } - VoucherBatchEntity batch = new VoucherBatchEntity(); + PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig(); BeanUtils.copyProperties(req, batch); batch.setUsedCount(0); batch.setClaimedCount(0); batch.setStatus(1); - batch.setCreateTime(new Date()); - batch.setCreateBy(BaseContextHandler.getUserId()); + batch.setCreatedTime(new Date()); + String userIdStr = BaseContextHandler.getUserId(); + if (userIdStr != null) { + batch.setCreateBy(Long.valueOf(userIdStr)); + } batch.setDeleted(0); voucherBatchMapper.insert(batch); @@ -74,17 +77,17 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { @Override public Page queryBatchList(VoucherBatchQueryReq req) { - Page page = new Page<>(req.getPageNum(), req.getPageSize()); + Page page = new Page<>(req.getPageNum(), req.getPageSize()); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(VoucherBatchEntity::getDeleted, 0) - .eq(req.getScenicId() != null, VoucherBatchEntity::getScenicId, req.getScenicId()) - .eq(req.getBrokerId() != null, VoucherBatchEntity::getBrokerId, req.getBrokerId()) - .eq(req.getStatus() != null, VoucherBatchEntity::getStatus, req.getStatus()) - .like(StringUtils.hasText(req.getBatchName()), VoucherBatchEntity::getBatchName, req.getBatchName()) - .orderByDesc(VoucherBatchEntity::getCreateTime); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherBatchConfig::getDeleted, 0) + .eq(req.getScenicId() != null, PriceVoucherBatchConfig::getScenicId, req.getScenicId()) + .eq(req.getBrokerId() != null, PriceVoucherBatchConfig::getBrokerId, req.getBrokerId()) + .eq(req.getStatus() != null, PriceVoucherBatchConfig::getStatus, req.getStatus()) + .like(StringUtils.hasText(req.getBatchName()), PriceVoucherBatchConfig::getBatchName, req.getBatchName()) + .orderByDesc(PriceVoucherBatchConfig::getCreatedTime); - Page entityPage = voucherBatchMapper.selectPage(page, wrapper); + Page entityPage = voucherBatchMapper.selectPage(page, wrapper); Page respPage = new Page<>(); BeanUtils.copyProperties(entityPage, respPage); @@ -96,7 +99,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { @Override public VoucherBatchResp getBatchDetail(Long id) { - VoucherBatchEntity batch = voucherBatchMapper.selectById(id); + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id); if (batch == null || batch.getDeleted() == 1) { throw new BizException(404, "券码批次不存在"); } @@ -106,7 +109,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { @Override public VoucherBatchStatsResp getBatchStats(Long id) { - VoucherBatchEntity batch = voucherBatchMapper.selectById(id); + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(id); if (batch == null || batch.getDeleted() == 1) { throw new BizException(404, "券码批次不存在"); } @@ -132,7 +135,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { @Override public void updateBatchStatus(Long id, Integer status) { - VoucherBatchEntity batch = new VoucherBatchEntity(); + PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig(); batch.setId(id); batch.setStatus(status); @@ -144,7 +147,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { @Override public void updateBatchClaimedCount(Long batchId) { - VoucherBatchEntity batch = voucherBatchMapper.selectById(batchId); + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId); if (batch != null) { batch.setClaimedCount(batch.getClaimedCount() + 1); voucherBatchMapper.updateById(batch); @@ -153,7 +156,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { @Override public void updateBatchUsedCount(Long batchId) { - VoucherBatchEntity batch = voucherBatchMapper.selectById(batchId); + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(batchId); if (batch != null) { batch.setUsedCount(batch.getUsedCount() + 1); voucherBatchMapper.updateById(batch); @@ -161,19 +164,19 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { } @Override - public VoucherBatchEntity getAvailableBatch(Long scenicId, Long brokerId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(VoucherBatchEntity::getScenicId, scenicId) - .eq(VoucherBatchEntity::getBrokerId, brokerId) - .eq(VoucherBatchEntity::getStatus, 1) - .eq(VoucherBatchEntity::getDeleted, 0) - .lt(VoucherBatchEntity::getClaimedCount, VoucherBatchEntity::getTotalCount) - .orderByDesc(VoucherBatchEntity::getCreateTime); + public PriceVoucherBatchConfig getAvailableBatch(Long scenicId, Long brokerId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherBatchConfig::getScenicId, scenicId) + .eq(PriceVoucherBatchConfig::getBrokerId, brokerId) + .eq(PriceVoucherBatchConfig::getStatus, 1) + .eq(PriceVoucherBatchConfig::getDeleted, 0) + .apply("claimed_count < total_count") + .orderByDesc(PriceVoucherBatchConfig::getCreatedTime); return voucherBatchMapper.selectOne(wrapper); } - private VoucherBatchResp convertToResp(VoucherBatchEntity batch) { + private VoucherBatchResp convertToResp(PriceVoucherBatchConfig batch) { VoucherBatchResp resp = new VoucherBatchResp(); BeanUtils.copyProperties(batch, resp); diff --git a/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java similarity index 63% rename from src/main/java/com/ycwl/basic/voucher/service/impl/VoucherCodeServiceImpl.java rename to src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java index c30ed73..cdb2c54 100644 --- a/src/main/java/com/ycwl/basic/voucher/service/impl/VoucherCodeServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java @@ -1,19 +1,19 @@ -package com.ycwl.basic.voucher.service.impl; +package com.ycwl.basic.pricing.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ycwl.basic.exception.BizException; -import com.ycwl.basic.voucher.dto.req.VoucherClaimReq; -import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq; -import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp; -import com.ycwl.basic.voucher.entity.VoucherBatchEntity; -import com.ycwl.basic.voucher.entity.VoucherCodeEntity; -import com.ycwl.basic.voucher.enums.VoucherCodeStatus; -import com.ycwl.basic.voucher.enums.VoucherDiscountType; -import com.ycwl.basic.voucher.mapper.VoucherBatchMapper; -import com.ycwl.basic.voucher.mapper.VoucherCodeMapper; -import com.ycwl.basic.voucher.service.VoucherBatchService; -import com.ycwl.basic.voucher.service.VoucherCodeService; +import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; +import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import com.ycwl.basic.pricing.enums.VoucherCodeStatus; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; +import com.ycwl.basic.pricing.service.VoucherBatchService; +import com.ycwl.basic.pricing.service.VoucherCodeService; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; @@ -29,26 +29,26 @@ import java.util.UUID; @RequiredArgsConstructor public class VoucherCodeServiceImpl implements VoucherCodeService { - private final VoucherCodeMapper voucherCodeMapper; - private final VoucherBatchMapper voucherBatchMapper; + private final PriceVoucherCodeMapper voucherCodeMapper; + private final PriceVoucherBatchConfigMapper voucherBatchMapper; private final VoucherBatchService voucherBatchService; @Override public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) { - List codes = new ArrayList<>(); + List codes = new ArrayList<>(); for (int i = 0; i < count; i++) { - VoucherCodeEntity code = new VoucherCodeEntity(); + PriceVoucherCode code = new PriceVoucherCode(); code.setBatchId(batchId); code.setScenicId(scenicId); code.setCode(generateVoucherCode()); code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode()); - code.setCreateTime(new Date()); + code.setCreatedTime(new Date()); code.setDeleted(0); codes.add(code); } - for (VoucherCodeEntity code : codes) { + for (PriceVoucherCode code : codes) { voucherCodeMapper.insert(code); } } @@ -70,12 +70,12 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { throw new BizException(400, "该用户在此景区已领取过券码"); } - VoucherBatchEntity batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId()); + PriceVoucherBatchConfig batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId()); if (batch == null) { throw new BizException(400, "暂无可用券码批次"); } - VoucherCodeEntity availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId()); + PriceVoucherCode availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId()); if (availableCode == null) { throw new BizException(400, "券码已领完"); } @@ -93,25 +93,25 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { @Override public Page queryCodeList(VoucherCodeQueryReq req) { - Page page = new Page<>(req.getPageNum(), req.getPageSize()); + Page page = new Page<>(req.getPageNum(), req.getPageSize()); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(VoucherCodeEntity::getDeleted, 0) - .eq(req.getBatchId() != null, VoucherCodeEntity::getBatchId, req.getBatchId()) - .eq(req.getScenicId() != null, VoucherCodeEntity::getScenicId, req.getScenicId()) - .eq(req.getFaceId() != null, VoucherCodeEntity::getFaceId, req.getFaceId()) - .eq(req.getStatus() != null, VoucherCodeEntity::getStatus, req.getStatus()) - .like(StringUtils.hasText(req.getCode()), VoucherCodeEntity::getCode, req.getCode()) - .orderByDesc(VoucherCodeEntity::getCreateTime); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherCode::getDeleted, 0) + .eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId()) + .eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId()) + .eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId()) + .eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus()) + .like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode()) + .orderByDesc(PriceVoucherCode::getCreatedTime); - Page entityPage = voucherCodeMapper.selectPage(page, wrapper); + Page entityPage = voucherCodeMapper.selectPage(page, wrapper); Page respPage = new Page<>(); BeanUtils.copyProperties(entityPage, respPage); List respList = new ArrayList<>(); - for (VoucherCodeEntity code : entityPage.getRecords()) { - VoucherBatchEntity batch = voucherBatchMapper.selectById(code.getBatchId()); + for (PriceVoucherCode code : entityPage.getRecords()) { + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId()); respList.add(convertToResp(code, batch)); } respPage.setRecords(respList); @@ -121,16 +121,16 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { @Override public List getMyVoucherCodes(Long faceId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(VoucherCodeEntity::getFaceId, faceId) - .eq(VoucherCodeEntity::getDeleted, 0) - .orderByDesc(VoucherCodeEntity::getClaimedTime); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherCode::getFaceId, faceId) + .eq(PriceVoucherCode::getDeleted, 0) + .orderByDesc(PriceVoucherCode::getClaimedTime); - List codes = voucherCodeMapper.selectList(wrapper); + List codes = voucherCodeMapper.selectList(wrapper); List respList = new ArrayList<>(); - for (VoucherCodeEntity code : codes) { - VoucherBatchEntity batch = voucherBatchMapper.selectById(code.getBatchId()); + for (PriceVoucherCode code : codes) { + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(code.getBatchId()); respList.add(convertToResp(code, batch)); } @@ -140,7 +140,7 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { @Override @Transactional public void markCodeAsUsed(Long codeId, String remark) { - VoucherCodeEntity code = voucherCodeMapper.selectById(codeId); + PriceVoucherCode code = voucherCodeMapper.selectById(codeId); if (code == null || code.getDeleted() == 1) { throw new BizException(404, "券码不存在"); } @@ -168,7 +168,7 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(); } - private VoucherCodeResp convertToResp(VoucherCodeEntity code, VoucherBatchEntity batch) { + private VoucherCodeResp convertToResp(PriceVoucherCode code, PriceVoucherBatchConfig batch) { VoucherCodeResp resp = new VoucherCodeResp(); BeanUtils.copyProperties(code, resp); 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 new file mode 100644 index 0000000..32e5643 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java @@ -0,0 +1,175 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.service.IDiscountProvider; +import com.ycwl.basic.pricing.service.IVoucherService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 券码折扣提供者 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class VoucherDiscountProvider implements IDiscountProvider { + + private final IVoucherService voucherService; + + @Override + public String getProviderType() { + return "VOUCHER"; + } + + @Override + public int getPriority() { + return 100; // 券码优先级最高 + } + + @Override + public List detectAvailableDiscounts(DiscountDetectionContext context) { + List discounts = new ArrayList<>(); + + if (context.getFaceId() == null || context.getScenicId() == null) { + return discounts; + } + + try { + VoucherInfo voucherInfo = null; + + // 优先检查用户主动输入的券码 + if (StringUtils.hasText(context.getVoucherCode())) { + voucherInfo = voucherService.validateAndGetVoucherInfo( + context.getVoucherCode(), + context.getFaceId(), + context.getScenicId() + ); + } + // 如果没有输入券码且允许自动使用,则查找最优券码 + else if (Boolean.TRUE.equals(context.getAutoUseVoucher())) { + voucherInfo = voucherService.getBestVoucher( + context.getFaceId(), + context.getScenicId(), + context + ); + } + + if (voucherInfo != null && Boolean.TRUE.equals(voucherInfo.getAvailable())) { + // 计算券码优惠金额 + BigDecimal discountAmount = voucherService.calculateVoucherDiscount(voucherInfo, context); + + if (discountAmount.compareTo(BigDecimal.ZERO) > 0) { + DiscountInfo discountInfo = new DiscountInfo(); + discountInfo.setDiscountId(voucherInfo.getVoucherId()); + discountInfo.setDiscountType("VOUCHER"); + discountInfo.setDiscountName("券码优惠"); + discountInfo.setDiscountDescription(buildDiscountDescription(voucherInfo)); + discountInfo.setDiscountAmount(discountAmount); + discountInfo.setProviderType(getProviderType()); + discountInfo.setPriority(getPriority()); + discountInfo.setStackable(isStackable(voucherInfo)); // 只有全场免费不可叠加 + discountInfo.setVoucherCode(voucherInfo.getVoucherCode()); + + discounts.add(discountInfo); + } + } + } catch (Exception e) { + log.warn("检测券码优惠时发生异常", e); + } + + return discounts; + } + + @Override + public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) { + DiscountResult result = new DiscountResult(); + result.setDiscountInfo(discountInfo); + + try { + String voucherCode = discountInfo.getVoucherCode(); + if (!StringUtils.hasText(voucherCode)) { + result.setSuccess(false); + result.setFailureReason("券码信息丢失"); + return result; + } + + // 重新验证券码 + VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo( + voucherCode, + context.getFaceId(), + context.getScenicId() + ); + + if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable())) { + result.setSuccess(false); + result.setFailureReason("券码无效或不可用"); + return result; + } + + // 计算实际优惠金额 + BigDecimal actualDiscount = voucherService.calculateVoucherDiscount(voucherInfo, context); + BigDecimal finalAmount; + + // 对于全场免费券码,最终金额为0 + if (voucherInfo.getDiscountType() == VoucherDiscountType.FREE_ALL) { + finalAmount = BigDecimal.ZERO; + actualDiscount = context.getCurrentAmount(); + } else { + finalAmount = context.getCurrentAmount().subtract(actualDiscount); + if (finalAmount.compareTo(BigDecimal.ZERO) < 0) { + finalAmount = BigDecimal.ZERO; + actualDiscount = context.getCurrentAmount(); + } + } + + result.setActualDiscountAmount(actualDiscount); + result.setFinalAmount(finalAmount); + result.setSuccess(true); + + log.info("成功应用券码: {}, 优惠金额: {}", voucherCode, actualDiscount); + + } catch (Exception e) { + log.error("应用券码失败: " + discountInfo.getVoucherCode(), e); + result.setSuccess(false); + result.setFailureReason("券码应用失败: " + e.getMessage()); + } + + return result; + } + + @Override + public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) { + return "VOUCHER".equals(discountInfo.getDiscountType()) && + context.getFaceId() != null && + context.getScenicId() != null && + StringUtils.hasText(discountInfo.getVoucherCode()); + } + + /** + * 构建优惠描述 + */ + private String buildDiscountDescription(VoucherInfo voucherInfo) { + if (voucherInfo.getDiscountType() == null) { + return "券码优惠"; + } + + return String.format("券码 %s - %s", + voucherInfo.getVoucherCode(), + voucherInfo.getDiscountType().getName()); + } + + /** + * 判断是否可以与其他优惠叠加 + */ + private boolean isStackable(VoucherInfo voucherInfo) { + // 全场免费券码不可与其他优惠叠加 + return voucherInfo.getDiscountType() != VoucherDiscountType.FREE_ALL; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java new file mode 100644 index 0000000..943267d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java @@ -0,0 +1,223 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import com.ycwl.basic.pricing.enums.VoucherCodeStatus; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; +import com.ycwl.basic.pricing.service.IVoucherService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 券码服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherServiceImpl implements IVoucherService { + + private final PriceVoucherCodeMapper voucherCodeMapper; + private final PriceVoucherBatchConfigMapper voucherBatchConfigMapper; + + @Override + public VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId) { + if (!StringUtils.hasText(voucherCode)) { + return null; + } + + // 查询券码信息 + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) { + return null; + } + + // 查询批次信息 + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId()); + if (batchConfig == null || batchConfig.getDeleted() == 1 || batchConfig.getStatus() == 0) { + return null; + } + + // 验证景区匹配 + if (scenicId != null && !scenicId.equals(voucherCodeEntity.getScenicId())) { + return null; + } + + VoucherInfo voucherInfo = buildVoucherInfo(voucherCodeEntity, batchConfig); + + // 检查券码状态和可用性 + if (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) { + // 未领取状态,检查是否可以领取 + if (faceId != null && canClaimVoucher(faceId, voucherCodeEntity.getScenicId())) { + voucherInfo.setAvailable(true); + } else { + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("您已在该景区领取过券码"); + } + } else if (VoucherCodeStatus.CLAIMED_UNUSED.getCode().equals(voucherCodeEntity.getStatus())) { + // 已领取未使用,检查是否为当前用户 + if (faceId != null && faceId.equals(voucherCodeEntity.getFaceId())) { + voucherInfo.setAvailable(true); + } else { + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码已被其他用户领取"); + } + } else { + // 已使用 + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码已使用"); + } + + return voucherInfo; + } + + @Override + public List getAvailableVouchers(Long faceId, Long scenicId) { + if (faceId == null || scenicId == null) { + return new ArrayList<>(); + } + + List voucherCodes = voucherCodeMapper.selectAvailableVouchersByFaceIdAndScenicId(faceId, scenicId); + List voucherInfos = new ArrayList<>(); + + for (PriceVoucherCode voucherCode : voucherCodes) { + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCode.getBatchId()); + if (batchConfig != null && batchConfig.getDeleted() == 0 && batchConfig.getStatus() == 1) { + VoucherInfo voucherInfo = buildVoucherInfo(voucherCode, batchConfig); + voucherInfo.setAvailable(true); + voucherInfos.add(voucherInfo); + } + } + + return voucherInfos; + } + + @Override + public void markVoucherAsUsed(String voucherCode, String remark) { + if (!StringUtils.hasText(voucherCode)) { + return; + } + + int result = voucherCodeMapper.useVoucher(voucherCode, LocalDateTime.now(), remark); + if (result > 0) { + // 更新批次统计 + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity != null) { + voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1); + } + log.info("券码已标记为使用: {}", voucherCode); + } + } + + @Override + public boolean canClaimVoucher(Long faceId, Long scenicId) { + if (faceId == null || scenicId == null) { + return false; + } + + Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId); + return count == 0; + } + + @Override + public BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context) { + if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable()) || + context.getProducts() == null || context.getProducts().isEmpty()) { + return BigDecimal.ZERO; + } + + VoucherDiscountType discountType = voucherInfo.getDiscountType(); + BigDecimal discountValue = voucherInfo.getDiscountValue(); + + if (discountType == null) { + return BigDecimal.ZERO; + } + + return switch (discountType) { + case FREE_ALL -> { + // 全场免费,返回当前总金额 + yield context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO; + } + case REDUCE_PRICE -> { + // 商品降价,每个商品减免固定金额 + if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0) { + yield BigDecimal.ZERO; + } + BigDecimal totalDiscount = BigDecimal.ZERO; + for (ProductItem product : context.getProducts()) { + BigDecimal productDiscount = discountValue.multiply(BigDecimal.valueOf(product.getQuantity())); + totalDiscount = totalDiscount.add(productDiscount); + } + yield totalDiscount; + } + case DISCOUNT -> { + // 商品打折,按百分比计算 + if (discountValue == null || discountValue.compareTo(BigDecimal.ZERO) <= 0 || + discountValue.compareTo(BigDecimal.valueOf(100)) >= 0) { + yield BigDecimal.ZERO; + } + BigDecimal currentAmount = context.getCurrentAmount() != null ? context.getCurrentAmount() : BigDecimal.ZERO; + BigDecimal discountRate = discountValue.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP); + yield currentAmount.multiply(discountRate).setScale(2, RoundingMode.HALF_UP); + } + }; + } + + @Override + public VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context) { + List availableVouchers = getAvailableVouchers(faceId, scenicId); + if (availableVouchers.isEmpty()) { + return null; + } + + // 计算每个券码的优惠金额,选择最优的 + VoucherInfo bestVoucher = null; + BigDecimal maxDiscount = BigDecimal.ZERO; + + for (VoucherInfo voucher : availableVouchers) { + BigDecimal discount = calculateVoucherDiscount(voucher, context); + voucher.setActualDiscountAmount(discount); + + if (discount.compareTo(maxDiscount) > 0) { + maxDiscount = discount; + bestVoucher = voucher; + } + } + + return bestVoucher; + } + + /** + * 构建券码信息DTO + */ + private VoucherInfo buildVoucherInfo(PriceVoucherCode voucherCode, PriceVoucherBatchConfig batchConfig) { + VoucherInfo voucherInfo = new VoucherInfo(); + voucherInfo.setVoucherId(voucherCode.getId()); + voucherInfo.setVoucherCode(voucherCode.getCode()); + voucherInfo.setBatchId(batchConfig.getId()); + voucherInfo.setBatchName(batchConfig.getBatchName()); + voucherInfo.setScenicId(voucherCode.getScenicId()); + voucherInfo.setBrokerId(batchConfig.getBrokerId()); + voucherInfo.setDiscountType(VoucherDiscountType.getByCode(batchConfig.getDiscountType())); + voucherInfo.setDiscountValue(batchConfig.getDiscountValue()); + voucherInfo.setStatus(voucherCode.getStatus()); + voucherInfo.setClaimedTime(voucherCode.getClaimedTime()); + voucherInfo.setUsedTime(voucherCode.getUsedTime()); + + return voucherInfo; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/CLAUDE.md b/src/main/java/com/ycwl/basic/voucher/CLAUDE.md deleted file mode 100644 index cadf343..0000000 --- a/src/main/java/com/ycwl/basic/voucher/CLAUDE.md +++ /dev/null @@ -1,179 +0,0 @@ -# 券码核销功能模块 - -本模块实现景区券码的批量创建、分发和核销管理功能。支持全场免费、商品降价、商品打折三种优惠模式,确保每个用户在每个景区只能领取一次券码。 - -## 功能概述 - -- **批量创建券码**:管理员可创建券码批次,自动生成指定数量的唯一券码 -- **精准分发控制**:通过景区ID、推客ID、用户faceId进行精准投放 -- **三种优惠模式**:全场免费、商品降价、商品打折 -- **唯一性保证**:同一用户在同一景区只能领取一次券码 -- **完整管理功能**:批次管理、券码查询、使用统计、手动核销 - -## 数据库表结构 - -### 券码批次表 (voucher_batch) -```sql -CREATE TABLE voucher_batch ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - batch_name VARCHAR(100) NOT NULL COMMENT '券码批次名称', - scenic_id BIGINT NOT NULL COMMENT '景区ID', - broker_id BIGINT NOT NULL COMMENT '推客ID', - discount_type TINYINT NOT NULL COMMENT '优惠类型:0=全场免费,1=商品降价,2=商品打折', - discount_value DECIMAL(10,2) COMMENT '优惠值(降价金额或折扣百分比)', - total_count INT NOT NULL COMMENT '总券码数量', - used_count INT DEFAULT 0 COMMENT '已使用数量', - claimed_count INT DEFAULT 0 COMMENT '已领取数量', - status TINYINT DEFAULT 1 COMMENT '状态:0=禁用,1=启用', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP, - create_by BIGINT COMMENT '创建人ID', - deleted TINYINT DEFAULT 0, - deleted_at DATETIME -); -``` - -### 券码表 (voucher_code) -```sql -CREATE TABLE voucher_code ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - batch_id BIGINT NOT NULL COMMENT '批次ID', - scenic_id BIGINT NOT NULL COMMENT '景区ID', - code VARCHAR(32) NOT NULL UNIQUE COMMENT '券码', - status TINYINT DEFAULT 0 COMMENT '状态:0=未领取,1=已领取未使用,2=已使用', - face_id BIGINT COMMENT '领取人faceId', - claimed_time DATETIME COMMENT '领取时间', - used_time DATETIME COMMENT '使用时间', - remark VARCHAR(500) COMMENT '使用备注', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP, - deleted TINYINT DEFAULT 0, - deleted_at DATETIME -); -``` - -## 包结构说明 - -``` -com.ycwl.basic.voucher/ -├── controller/ -│ └── VoucherController.java # 控制器:PC端管理和移动端用户接口 -├── service/ -│ ├── VoucherBatchService.java # 券码批次服务接口 -│ ├── VoucherCodeService.java # 券码服务接口 -│ └── impl/ -│ ├── VoucherBatchServiceImpl.java # 券码批次服务实现 -│ └── VoucherCodeServiceImpl.java # 券码服务实现 -├── mapper/ -│ ├── VoucherBatchMapper.java # 券码批次数据访问 -│ └── VoucherCodeMapper.java # 券码数据访问 -├── entity/ -│ ├── VoucherBatchEntity.java # 券码批次实体 -│ └── VoucherCodeEntity.java # 券码实体 -├── dto/ -│ ├── req/ # 请求DTO -│ │ ├── VoucherBatchCreateReq.java # 创建批次请求 -│ │ ├── VoucherBatchQueryReq.java # 批次查询请求 -│ │ ├── VoucherCodeQueryReq.java # 券码查询请求 -│ │ └── VoucherClaimReq.java # 券码领取请求 -│ └── resp/ # 响应DTO -│ ├── VoucherBatchResp.java # 批次响应 -│ ├── VoucherCodeResp.java # 券码响应 -│ └── VoucherBatchStatsResp.java # 批次统计响应 -└── enums/ - ├── VoucherDiscountType.java # 优惠类型枚举 - └── VoucherCodeStatus.java # 券码状态枚举 -``` - -## 核心业务逻辑 - -### 1. 券码生成规则 -- 使用UUID生成8位大写字母数字组合 -- 确保每个券码在系统中唯一 -- 券码初始状态为"未领取" - -### 2. 领取验证逻辑 -```java -// 核心验证:同一faceId在同一scenicId中只能领取一次 -public boolean canClaimVoucher(Long faceId, Long scenicId) { - Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId); - return count == 0; -} -``` - -### 3. 优惠类型说明 -- **全场免费(0)**:所有商品免费,discountValue可为空 -- **商品降价(1)**:每个商品减免固定金额,discountValue为减免金额 -- **商品打折(2)**:每个商品按百分比打折,discountValue为折扣百分比 - -### 4. 状态流转 -券码状态:未领取(0) → 已领取未使用(1) → 已使用(2) - -## 主要接口说明 - -### PC端管理接口 -- `POST /api/voucher/batch/create` - 创建券码批次 -- `POST /api/voucher/batch/list` - 批次列表查询 -- `GET /api/voucher/batch/{id}` - 批次详情 -- `GET /api/voucher/batch/{id}/stats` - 批次统计 -- `PUT /api/voucher/batch/{id}/status` - 启用/禁用批次 -- `POST /api/voucher/codes` - 券码列表查询 -- `PUT /api/voucher/code/{id}/use` - 手动标记券码已使用 -- `GET /api/voucher/scenic/{scenicId}/users` - 查看景区下用户领取情况 - -### 移动端用户接口 -- `POST /api/voucher/mobile/claim` - 领取券码(一步到位) -- `GET /api/voucher/mobile/my-codes` - 我的券码列表 - -## 关键技术特点 - -### 1. 事务管理 -- 使用`@Transactional`确保券码领取操作的原子性 -- 批次创建和券码生成在同一事务中完成 - -### 2. 并发控制 -- 通过应用层验证避免数据库唯一约束冲突 -- 使用数据库索引优化查询性能 - -### 3. 参数验证 -- 在Service层进行手动参数校验 -- 使用BizException统一错误处理 - -### 4. 数据统计 -- 实时更新批次的已领取数量和已使用数量 -- 提供详细的使用统计和分析数据 - -## 使用示例 - -### 创建券码批次 -```java -VoucherBatchCreateReq req = new VoucherBatchCreateReq(); -req.setBatchName("春节特惠券"); -req.setScenicId(1001L); -req.setBrokerId(2001L); -req.setDiscountType(1); // 商品降价 -req.setDiscountValue(new BigDecimal("10.00")); -req.setTotalCount(1000); - -Long batchId = voucherBatchService.createBatch(req); -``` - -### 用户领取券码 -```java -VoucherClaimReq req = new VoucherClaimReq(); -req.setScenicId(1001L); -req.setBrokerId(2001L); -req.setFaceId(3001L); - -VoucherCodeResp result = voucherCodeService.claimVoucher(req); -``` - -## 注意事项 - -1. **唯一性限制**:同一个faceId在同一个scenicId中只能领取一次券码 -2. **批次状态**:只有启用状态的批次才能领取券码 -3. **券码数量**:确保批次有可用券码才能成功领取 -4. **优惠值验证**:除全场免费外,其他优惠类型必须设置优惠值 -5. **删除机制**:使用逻辑删除,deleted字段标记删除状态 - -## 扩展说明 - -本模块设计为独立功能模块,不依赖支付系统,为后续接入其他优惠策略预留了扩展空间。所有接口都提供了完整的错误处理和参数验证,确保系统的稳定性和可维护性。 \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/entity/VoucherBatchEntity.java b/src/main/java/com/ycwl/basic/voucher/entity/VoucherBatchEntity.java deleted file mode 100644 index 9d2dfef..0000000 --- a/src/main/java/com/ycwl/basic/voucher/entity/VoucherBatchEntity.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ycwl.basic.voucher.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.math.BigDecimal; -import java.util.Date; - -@Data -@TableName("voucher_batch") -public class VoucherBatchEntity { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private String batchName; - private Long scenicId; - private Long brokerId; - private Integer discountType; - private BigDecimal discountValue; - private Integer totalCount; - private Integer usedCount; - private Integer claimedCount; - private Integer status; - private Date createTime; - private Long createBy; - private Integer deleted; - private Date deletedAt; -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/entity/VoucherCodeEntity.java b/src/main/java/com/ycwl/basic/voucher/entity/VoucherCodeEntity.java deleted file mode 100644 index 63a5dab..0000000 --- a/src/main/java/com/ycwl/basic/voucher/entity/VoucherCodeEntity.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.ycwl.basic.voucher.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.util.Date; - -@Data -@TableName("voucher_code") -public class VoucherCodeEntity { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private Long batchId; - private Long scenicId; - private String code; - private Integer status; - private Long faceId; - private Date claimedTime; - private Date usedTime; - private String remark; - private Date createTime; - private Integer deleted; - private Date deletedAt; -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/enums/VoucherCodeStatus.java b/src/main/java/com/ycwl/basic/voucher/enums/VoucherCodeStatus.java deleted file mode 100644 index a55bbca..0000000 --- a/src/main/java/com/ycwl/basic/voucher/enums/VoucherCodeStatus.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.ycwl.basic.voucher.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum VoucherCodeStatus { - UNCLAIMED(0, "未领取"), - CLAIMED_UNUSED(1, "已领取未使用"), - USED(2, "已使用"); - - private final Integer code; - private final String name; - - public static VoucherCodeStatus getByCode(Integer code) { - if (code == null) { - return null; - } - for (VoucherCodeStatus status : values()) { - if (status.getCode().equals(code)) { - return status; - } - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/mapper/VoucherBatchMapper.java b/src/main/java/com/ycwl/basic/voucher/mapper/VoucherBatchMapper.java deleted file mode 100644 index e3f561a..0000000 --- a/src/main/java/com/ycwl/basic/voucher/mapper/VoucherBatchMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ycwl.basic.voucher.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ycwl.basic.voucher.entity.VoucherBatchEntity; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface VoucherBatchMapper extends BaseMapper { -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/voucher/mapper/VoucherCodeMapper.java b/src/main/java/com/ycwl/basic/voucher/mapper/VoucherCodeMapper.java deleted file mode 100644 index f7c6011..0000000 --- a/src/main/java/com/ycwl/basic/voucher/mapper/VoucherCodeMapper.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.ycwl.basic.voucher.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ycwl.basic.voucher.entity.VoucherCodeEntity; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; - -@Mapper -public interface VoucherCodeMapper extends BaseMapper { - - @Select("SELECT COUNT(*) FROM voucher_code WHERE scenic_id = #{scenicId} AND face_id = #{faceId} AND status != 0 AND deleted = 0") - Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); - - @Select("SELECT * FROM voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1") - VoucherCodeEntity findFirstAvailableByBatchId(@Param("batchId") Long batchId); -} \ No newline at end of file From b4b542046f4429ccd110b1e76794b9b176113958 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 21 Aug 2025 10:44:08 +0800 Subject: [PATCH 10/18] =?UTF-8?q?refactor(pricing):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=E7=AE=A1=E7=90=86=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换 Lombok 的 @RequiredArgsConstructor 注解为 Spring 的 @Autowired 和 @Lazy 注解 - 更新 VoucherManagementController、VoucherBatchServiceImpl 和 VoucherCodeServiceImpl 类的依赖注入方式 - 优化代码结构,提高可读性和可维护性 --- .../controller/VoucherManagementController.java | 12 ++++++++---- .../service/impl/VoucherBatchServiceImpl.java | 11 +++++++---- .../service/impl/VoucherCodeServiceImpl.java | 15 ++++++++++----- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java b/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java index dda20ab..573870e 100644 --- a/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java @@ -12,17 +12,21 @@ import com.ycwl.basic.pricing.service.VoucherBatchService; import com.ycwl.basic.pricing.service.VoucherCodeService; import com.ycwl.basic.utils.ApiResponse; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/pricing/voucher") -@RequiredArgsConstructor public class VoucherManagementController { - - private final VoucherBatchService voucherBatchService; - private final VoucherCodeService voucherCodeService; + @Autowired + @Lazy + private VoucherBatchService voucherBatchService; + @Autowired + @Lazy + private VoucherCodeService voucherCodeService; @PostMapping("/batch/create") public ApiResponse createBatch(@RequestBody VoucherBatchCreateReq req) { diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java index 8f1d2de..e430511 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java @@ -15,6 +15,8 @@ import com.ycwl.basic.pricing.service.VoucherBatchService; import com.ycwl.basic.pricing.service.VoucherCodeService; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -22,11 +24,12 @@ import org.springframework.util.StringUtils; import java.util.Date; @Service -@RequiredArgsConstructor public class VoucherBatchServiceImpl implements VoucherBatchService { - - private final PriceVoucherBatchConfigMapper voucherBatchMapper; - private final VoucherCodeService voucherCodeService; + @Autowired + private PriceVoucherBatchConfigMapper voucherBatchMapper; + @Autowired + @Lazy + private VoucherCodeService voucherCodeService; @Override @Transactional diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java index cdb2c54..1ca22e2 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java @@ -16,6 +16,8 @@ import com.ycwl.basic.pricing.service.VoucherBatchService; import com.ycwl.basic.pricing.service.VoucherCodeService; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -26,12 +28,15 @@ import java.util.List; import java.util.UUID; @Service -@RequiredArgsConstructor public class VoucherCodeServiceImpl implements VoucherCodeService { - - private final PriceVoucherCodeMapper voucherCodeMapper; - private final PriceVoucherBatchConfigMapper voucherBatchMapper; - private final VoucherBatchService voucherBatchService; + + @Autowired + private PriceVoucherCodeMapper voucherCodeMapper; + @Autowired + private PriceVoucherBatchConfigMapper voucherBatchMapper; + @Autowired + @Lazy + private VoucherBatchService voucherBatchService; @Override public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) { From 3d49c470067cf2bc42540000eb574e097f28d303 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 21 Aug 2025 14:37:55 +0800 Subject: [PATCH 11/18] =?UTF-8?q?refactor(entity):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E7=B1=BB=E5=B9=B6=E4=BC=98=E5=8C=96=E5=88=B8?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 BaseEntity 类,将通用字段直接集成到各实体类中 - 更新实体类字段名称,如 createdTime 改为 createTime- 在 PriceVoucherCode 实体中添加生成安全券码的逻辑 - 更新相关服务类中的方法调用,以适应新的字段名称 --- .../ycwl/basic/pricing/entity/BaseEntity.java | 29 -------- .../pricing/entity/PriceBundleConfig.java | 26 +++++-- .../entity/PriceCouponClaimRecord.java | 20 +++++- .../pricing/entity/PriceCouponConfig.java | 25 ++++++- .../pricing/entity/PriceProductConfig.java | 25 ++++++- .../basic/pricing/entity/PriceTierConfig.java | 25 ++++++- .../entity/PriceVoucherBatchConfig.java | 20 ++++-- .../pricing/entity/PriceVoucherCode.java | 15 +++- .../service/impl/CouponServiceImpl.java | 2 +- .../impl/PricingManagementServiceImpl.java | 30 ++++---- .../service/impl/VoucherBatchServiceImpl.java | 6 +- .../service/impl/VoucherCodeServiceImpl.java | 68 +++++++++++++++++-- 12 files changed, 213 insertions(+), 78 deletions(-) delete mode 100644 src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java diff --git a/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java b/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java deleted file mode 100644 index 849b036..0000000 --- a/src/main/java/com/ycwl/basic/pricing/entity/BaseEntity.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ycwl.basic.pricing.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import lombok.Data; - -import java.util.Date; - -/** - * 基础实体类 - */ -@Data -public class BaseEntity { - - @TableId(type = IdType.AUTO) - private Long id; - - private Date createdTime; - - private Date updatedTime; - - private Long createBy; - - private Long updateBy; - - private Integer deleted; - - private Date deletedAt; -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java index bd2e705..219a55a 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceBundleConfig.java @@ -1,23 +1,27 @@ package com.ycwl.basic.pricing.entity; -import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; import com.ycwl.basic.pricing.dto.BundleProductItem; import com.ycwl.basic.pricing.handler.BundleProductListTypeHandler; import lombok.Data; -import lombok.EqualsAndHashCode; import org.apache.ibatis.type.JdbcType; import java.math.BigDecimal; +import java.util.Date; import java.util.List; /** * 一口价配置实体 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("price_bundle_config") -public class PriceBundleConfig extends BaseEntity { +public class PriceBundleConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; /** * 套餐名称 @@ -55,4 +59,18 @@ public class PriceBundleConfig extends BaseEntity { * 是否启用 */ private Boolean isActive; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java index a48511d..dd5c24b 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponClaimRecord.java @@ -1,9 +1,11 @@ package com.ycwl.basic.pricing.entity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.ycwl.basic.pricing.enums.CouponStatus; import lombok.Data; -import lombok.EqualsAndHashCode; import java.util.Date; @@ -11,9 +13,11 @@ import java.util.Date; * 优惠券领用记录实体 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("price_coupon_claim_record") -public class PriceCouponClaimRecord extends BaseEntity { +public class PriceCouponClaimRecord { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; /** * 优惠券ID @@ -49,4 +53,14 @@ public class PriceCouponClaimRecord extends BaseEntity { * 景区ID - 记录优惠券在哪个景区被领取/使用 */ private String scenicId; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Integer deleted; + + private Date deletedAt; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java index 386e6fe..06be0ce 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceCouponConfig.java @@ -1,20 +1,25 @@ package com.ycwl.basic.pricing.entity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.ycwl.basic.pricing.enums.CouponType; import lombok.Data; -import lombok.EqualsAndHashCode; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.Date; /** * 优惠券配置实体 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("price_coupon_config") -public class PriceCouponConfig extends BaseEntity { +public class PriceCouponConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; /** * 优惠券名称 @@ -75,4 +80,18 @@ public class PriceCouponConfig extends BaseEntity { * 景区ID - 限制优惠券只能在该景区使用 */ private String scenicId; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java index 8fec5d8..4677774 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceProductConfig.java @@ -1,18 +1,23 @@ package com.ycwl.basic.pricing.entity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import lombok.EqualsAndHashCode; import java.math.BigDecimal; +import java.util.Date; /** * 商品价格配置实体 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("price_product_config") -public class PriceProductConfig extends BaseEntity { +public class PriceProductConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; /** * 商品类型 @@ -53,4 +58,18 @@ public class PriceProductConfig extends BaseEntity { * 是否启用 */ private Boolean isActive; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java index 4c76593..f3a3e95 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceTierConfig.java @@ -1,18 +1,23 @@ package com.ycwl.basic.pricing.entity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import lombok.EqualsAndHashCode; import java.math.BigDecimal; +import java.util.Date; /** * 阶梯定价配置实体 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("price_tier_config") -public class PriceTierConfig extends BaseEntity { +public class PriceTierConfig { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; /** * 商品类型 @@ -63,4 +68,18 @@ public class PriceTierConfig extends BaseEntity { * 是否启用 */ private Boolean isActive; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java index a68510c..49e4724 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java @@ -1,10 +1,10 @@ package com.ycwl.basic.pricing.entity; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import lombok.EqualsAndHashCode; import java.math.BigDecimal; import java.util.Date; @@ -13,9 +13,8 @@ import java.util.Date; * 券码批次配置实体 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("price_voucher_batch_config") -public class PriceVoucherBatchConfig extends BaseEntity { +public class PriceVoucherBatchConfig { @TableId(value = "id", type = IdType.AUTO) private Long id; @@ -65,8 +64,17 @@ public class PriceVoucherBatchConfig extends BaseEntity { */ private Integer status; - /** - * 创建人ID - */ + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + private Long createBy; + + private Long updateBy; + + private Integer deleted; + + private Date deletedAt; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java index 6efcbdc..1b59dc2 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java @@ -1,10 +1,10 @@ package com.ycwl.basic.pricing.entity; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import lombok.EqualsAndHashCode; import java.util.Date; @@ -12,9 +12,8 @@ import java.util.Date; * 券码实体 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("price_voucher_code") -public class PriceVoucherCode extends BaseEntity { +public class PriceVoucherCode { @TableId(value = "id", type = IdType.AUTO) private Long id; @@ -58,4 +57,14 @@ public class PriceVoucherCode extends BaseEntity { * 使用备注 */ private String remark; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Integer deleted; + + private Date deletedAt; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java index c28335c..82bbc13 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java @@ -152,7 +152,7 @@ public class CouponServiceImpl implements ICouponService { record.setStatus(CouponStatus.USED); record.setUseTime(useTime); record.setOrderId(request.getOrderId()); - record.setUpdatedTime(new Date()); + record.setUpdateTime(new Date()); // 如果请求中包含景区ID,记录到使用记录中 if (request.getScenicId() != null && !request.getScenicId().isEmpty()) { diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java index 20f0fcc..ec402f4 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java @@ -35,8 +35,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService { } } - config.setCreatedTime(new Date()); - config.setUpdatedTime(new Date()); + config.setCreateTime(new Date()); + config.setUpdateTime(new Date()); productConfigMapper.insertProductConfig(config); return config.getId(); } @@ -44,7 +44,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateProductConfig(PriceProductConfig config) { - config.setUpdatedTime(new Date()); + config.setUpdateTime(new Date()); return productConfigMapper.updateProductConfig(config) > 0; } @@ -58,8 +58,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService { config.getProductType(), config.getMinQuantity(), config.getMaxQuantity()); } - config.setCreatedTime(new Date()); - config.setUpdatedTime(new Date()); + config.setCreateTime(new Date()); + config.setUpdateTime(new Date()); tierConfigMapper.insertTierConfig(config); return config.getId(); } @@ -67,15 +67,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateTierConfig(PriceTierConfig config) { - config.setUpdatedTime(new Date()); + config.setUpdateTime(new Date()); return tierConfigMapper.updateTierConfig(config) > 0; } @Override @Transactional public Long createCouponConfig(PriceCouponConfig config) { - config.setCreatedTime(new Date()); - config.setUpdatedTime(new Date()); + config.setCreateTime(new Date()); + config.setUpdateTime(new Date()); couponConfigMapper.insertCoupon(config); return config.getId(); } @@ -83,7 +83,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateCouponConfig(PriceCouponConfig config) { - config.setUpdatedTime(new Date()); + config.setUpdateTime(new Date()); return couponConfigMapper.updateCoupon(config) > 0; } @@ -91,8 +91,8 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Transactional public Long createCouponClaimRecord(PriceCouponClaimRecord record) { record.setClaimTime(new Date()); - record.setCreatedTime(new Date()); - record.setUpdatedTime(new Date()); + record.setCreateTime(new Date()); + record.setUpdateTime(new Date()); couponClaimRecordMapper.insertClaimRecord(record); return record.getId(); } @@ -100,15 +100,15 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateCouponClaimRecord(PriceCouponClaimRecord record) { - record.setUpdatedTime(new Date()); + record.setUpdateTime(new Date()); return couponClaimRecordMapper.updateClaimRecord(record) > 0; } @Override @Transactional public Long createBundleConfig(PriceBundleConfig config) { - config.setCreatedTime(new Date()); - config.setUpdatedTime(new Date()); + config.setCreateTime(new Date()); + config.setUpdateTime(new Date()); bundleConfigMapper.insertBundleConfig(config); return config.getId(); } @@ -116,7 +116,7 @@ public class PricingManagementServiceImpl implements IPricingManagementService { @Override @Transactional public boolean updateBundleConfig(PriceBundleConfig config) { - config.setUpdatedTime(new Date()); + config.setUpdateTime(new Date()); return bundleConfigMapper.updateBundleConfig(config) > 0; } diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java index e430511..805fc5b 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java @@ -64,7 +64,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { batch.setUsedCount(0); batch.setClaimedCount(0); batch.setStatus(1); - batch.setCreatedTime(new Date()); + batch.setCreateTime(new Date()); String userIdStr = BaseContextHandler.getUserId(); if (userIdStr != null) { batch.setCreateBy(Long.valueOf(userIdStr)); @@ -88,7 +88,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { .eq(req.getBrokerId() != null, PriceVoucherBatchConfig::getBrokerId, req.getBrokerId()) .eq(req.getStatus() != null, PriceVoucherBatchConfig::getStatus, req.getStatus()) .like(StringUtils.hasText(req.getBatchName()), PriceVoucherBatchConfig::getBatchName, req.getBatchName()) - .orderByDesc(PriceVoucherBatchConfig::getCreatedTime); + .orderByDesc(PriceVoucherBatchConfig::getCreateTime); Page entityPage = voucherBatchMapper.selectPage(page, wrapper); @@ -174,7 +174,7 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { .eq(PriceVoucherBatchConfig::getStatus, 1) .eq(PriceVoucherBatchConfig::getDeleted, 0) .apply("claimed_count < total_count") - .orderByDesc(PriceVoucherBatchConfig::getCreatedTime); + .orderByDesc(PriceVoucherBatchConfig::getCreateTime); return voucherBatchMapper.selectOne(wrapper); } diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java index 1ca22e2..d3710b2 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java @@ -15,6 +15,7 @@ import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; import com.ycwl.basic.pricing.service.VoucherBatchService; import com.ycwl.basic.pricing.service.VoucherCodeService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -22,14 +23,23 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.UUID; +import java.util.Objects; +@Slf4j @Service public class VoucherCodeServiceImpl implements VoucherCodeService { + // 券码生成相关常量 + private static final String SAFE_CHARS = "23456789ABCDEFGHJKMNPQRTUVWXYZ"; + private static final char[] SAFE_CHARS_ARRAY = SAFE_CHARS.toCharArray(); + private static final int CODE_LENGTH = 6; + private static final int MAX_RETRY = 10; + private static final SecureRandom RANDOM = new SecureRandom(); + @Autowired private PriceVoucherCodeMapper voucherCodeMapper; @Autowired @@ -48,7 +58,7 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { code.setScenicId(scenicId); code.setCode(generateVoucherCode()); code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode()); - code.setCreatedTime(new Date()); + code.setCreateTime(new Date()); code.setDeleted(0); codes.add(code); } @@ -107,7 +117,7 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { .eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId()) .eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus()) .like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode()) - .orderByDesc(PriceVoucherCode::getCreatedTime); + .orderByDesc(PriceVoucherCode::getId); Page entityPage = voucherCodeMapper.selectPage(page, wrapper); @@ -150,7 +160,7 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { throw new BizException(404, "券码不存在"); } - if (code.getStatus() != VoucherCodeStatus.CLAIMED_UNUSED.getCode()) { + if (!Objects.equals(code.getStatus(), VoucherCodeStatus.CLAIMED_UNUSED.getCode())) { throw new BizException(400, "券码状态异常,无法使用"); } @@ -169,8 +179,56 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { return count == 0; } + /** + * 生成6位安全券码(去除易混淆字符) + * 字符集:数字2-9 + 大写字母(去除0,1,I,L,O,S) + * + * @return 6位券码 + */ private String generateVoucherCode() { - return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(); + for (int attempt = 0; attempt < MAX_RETRY; attempt++) { + String code = generateRandomCode(); + log.debug("生成券码候选: {} (尝试第{}次)", code, attempt + 1); + + if (!isCodeExists(code)) { + log.info("成功生成券码: {} (字符集大小: {}, 理论组合数: {})", + code, SAFE_CHARS.length(), Math.pow(SAFE_CHARS.length(), CODE_LENGTH)); + return code; + } + + log.warn("券码重复,重新生成: {}", code); + } + + // 如果重试次数用完仍有重复,抛出异常 + log.error("券码生成失败:达到最大重试次数 {}", MAX_RETRY); + throw new RuntimeException("券码生成失败:达到最大重试次数,请稍后重试"); + } + + /** + * 生成随机6位字符 + * + * @return 随机6位字符 + */ + private String generateRandomCode() { + StringBuilder code = new StringBuilder(CODE_LENGTH); + for (int i = 0; i < CODE_LENGTH; i++) { + int randomIndex = RANDOM.nextInt(SAFE_CHARS_ARRAY.length); + code.append(SAFE_CHARS_ARRAY[randomIndex]); + } + return code.toString(); + } + + /** + * 检查券码是否已存在 + * + * @param code 券码 + * @return 是否存在 + */ + private boolean isCodeExists(String code) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherCode::getCode, code) + .eq(PriceVoucherCode::getDeleted, 0); + return voucherCodeMapper.selectCount(wrapper) > 0; } private VoucherCodeResp convertToResp(PriceVoucherCode code, PriceVoucherBatchConfig batch) { From 2c0b7a094d3858db52e7234c1d6bd9092ed617dc Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 21 Aug 2025 18:04:41 +0800 Subject: [PATCH 12/18] =?UTF-8?q?refactor(mapper):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=AD=97=E6=AE=B5=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 created_time 修改为 create_time -将 updated_time 修改为 update_time - 调整相关 SQL 查询和插入语句中的字段名称 --- .../mapper/PriceBundleConfigMapper.java | 12 ++++++------ .../mapper/PriceCouponClaimRecordMapper.java | 18 +++++++++--------- .../mapper/PriceCouponConfigMapper.java | 18 +++++++++--------- .../mapper/PriceProductConfigMapper.java | 6 +++--- .../pricing/mapper/PriceTierConfigMapper.java | 6 +++--- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java index 0bc1ce3..f65e4f0 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceBundleConfigMapper.java @@ -17,7 +17,7 @@ public interface PriceBundleConfigMapper extends BaseMapper { */ @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + "included_products, excluded_products, " + - "description, is_active, created_time, updated_time " + + "description, is_active, create_time, update_time " + "FROM price_bundle_config WHERE is_active = 1") @Results({ @Result(column = "included_products", property = "includedProducts", @@ -32,7 +32,7 @@ public interface PriceBundleConfigMapper extends BaseMapper { */ @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + "included_products, excluded_products, " + - "description, is_active, created_time, updated_time " + + "description, is_active, create_time, update_time " + "FROM price_bundle_config WHERE id = #{id} AND is_active = 1") @Results({ @Result(column = "included_products", property = "includedProducts", @@ -49,7 +49,7 @@ public interface PriceBundleConfigMapper extends BaseMapper { */ @Select("SELECT id, bundle_name, scenic_id, bundle_price, " + "included_products, excluded_products, " + - "description, is_active, created_time, updated_time " + + "description, is_active, create_time, update_time " + "FROM price_bundle_config ORDER BY is_active DESC, bundle_name ASC") @Results({ @Result(column = "included_products", property = "includedProducts", @@ -63,7 +63,7 @@ public interface PriceBundleConfigMapper extends BaseMapper { * 插入一口价配置 */ @Insert("INSERT INTO price_bundle_config (bundle_name, scenic_id, bundle_price, included_products, excluded_products, " + - "description, is_active, created_time, updated_time) VALUES " + + "description, is_active, create_time, update_time) VALUES " + "(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " + "#{description}, #{isActive}, NOW(), NOW())") int insertBundleConfig(PriceBundleConfig config); @@ -73,12 +73,12 @@ public interface PriceBundleConfigMapper extends BaseMapper { */ @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, scenic_id = #{scenicId}, bundle_price = #{bundlePrice}, " + "included_products = #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, excluded_products = #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " + - "description = #{description}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + "description = #{description}, is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") int updateBundleConfig(PriceBundleConfig config); /** * 更新一口价配置状态 */ - @Update("UPDATE price_bundle_config SET is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + @Update("UPDATE price_bundle_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") int updateBundleConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java index 725c9f7..ec83ac9 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponClaimRecordMapper.java @@ -40,7 +40,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper selectAllForAdmin(); /** @@ -100,7 +100,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper" + "" + - "ORDER BY r.created_time DESC" + + "ORDER BY r.create_time DESC" + "") List selectByConditionsForAdmin(@Param("userId") Long userId, @Param("couponId") Long couponId, @@ -116,7 +116,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper selectByUserIdForAdmin(@Param("userId") Long userId); /** @@ -126,7 +126,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper selectByCouponIdForAdmin(@Param("couponId") Long couponId); /** @@ -136,7 +136,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper selectByStatusForAdmin(@Param("status") CouponStatus status); /** @@ -191,7 +191,7 @@ public interface PriceCouponClaimRecordMapper extends BaseMapper selectByScenicIdForAdmin(@Param("scenicId") String scenicId); /** diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java index d1075a5..077e470 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceCouponConfigMapper.java @@ -36,7 +36,7 @@ public interface PriceCouponConfigMapper extends BaseMapper { * 增加优惠券使用数量 */ @Update("UPDATE price_coupon_config SET used_quantity = used_quantity + 1, " + - "updated_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity") + "update_time = NOW() WHERE id = #{couponId} AND used_quantity < total_quantity") int incrementUsedQuantity(Long couponId); /** @@ -44,7 +44,7 @@ public interface PriceCouponConfigMapper extends BaseMapper { */ @Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " + "max_discount, applicable_products, total_quantity, used_quantity, valid_from, valid_until, " + - "is_active, scenic_id, created_time, updated_time) VALUES " + + "is_active, scenic_id, create_time, update_time) VALUES " + "(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " + "#{applicableProducts}, #{totalQuantity}, #{usedQuantity}, #{validFrom}, #{validUntil}, " + "#{isActive}, #{scenicId}, NOW(), NOW())") @@ -57,7 +57,7 @@ public interface PriceCouponConfigMapper extends BaseMapper { "discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " + "applicable_products = #{applicableProducts}, total_quantity = #{totalQuantity}, " + "valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " + - "scenic_id = #{scenicId}, updated_time = NOW() WHERE id = #{id}") + "scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}") int updateCoupon(PriceCouponConfig coupon); // ==================== 管理端接口 ==================== @@ -65,7 +65,7 @@ public interface PriceCouponConfigMapper extends BaseMapper { /** * 管理端:查询所有优惠券配置(包含禁用的) */ - @Select("SELECT * FROM price_coupon_config ORDER BY created_time DESC") + @Select("SELECT * FROM price_coupon_config ORDER BY create_time DESC") List selectAllForAdmin(); /** @@ -84,7 +84,7 @@ public interface PriceCouponConfigMapper extends BaseMapper { "AND scenic_id = #{scenicId}" + "" + "" + - "ORDER BY created_time DESC" + + "ORDER BY create_time DESC" + "") List selectByConditionsForAdmin(@Param("isActive") Boolean isActive, @Param("couponName") String couponName, @@ -93,19 +93,19 @@ public interface PriceCouponConfigMapper extends BaseMapper { /** * 管理端:根据状态查询优惠券配置 */ - @Select("SELECT * FROM price_coupon_config WHERE is_active = #{isActive} ORDER BY created_time DESC") + @Select("SELECT * FROM price_coupon_config WHERE is_active = #{isActive} ORDER BY create_time DESC") List selectByStatusForAdmin(@Param("isActive") Boolean isActive); /** * 管理端:更新优惠券状态 */ - @Update("UPDATE price_coupon_config SET is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + @Update("UPDATE price_coupon_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") int updateCouponStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); /** * 管理端:删除优惠券配置 */ - @Update("UPDATE price_coupon_config SET deleted = 1, updated_time = NOW() WHERE id = #{id}") + @Update("UPDATE price_coupon_config SET deleted = 1, update_time = NOW() WHERE id = #{id}") int deleteCoupon(Long id); /** @@ -120,7 +120,7 @@ public interface PriceCouponConfigMapper extends BaseMapper { /** * 管理端:根据景区ID查询优惠券配置 */ - @Select("SELECT * FROM price_coupon_config WHERE scenic_id = #{scenicId} ORDER BY created_time DESC") + @Select("SELECT * FROM price_coupon_config WHERE scenic_id = #{scenicId} ORDER BY create_time DESC") List selectByScenicIdForAdmin(@Param("scenicId") String scenicId); /** 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 d200921..e4769a0 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceProductConfigMapper.java @@ -57,7 +57,7 @@ public interface PriceProductConfigMapper extends BaseMapper /** * 插入商品价格配置 */ - @Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, created_time, updated_time) " + + @Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, create_time, update_time) " + "VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, NOW(), NOW())") int insertProductConfig(PriceProductConfig config); @@ -65,12 +65,12 @@ public interface PriceProductConfigMapper extends BaseMapper * 更新商品价格配置 */ @Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " + - "original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + "original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") int updateProductConfig(PriceProductConfig config); /** * 更新商品配置状态 */ - @Update("UPDATE price_product_config SET is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + @Update("UPDATE price_product_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") int updateProductConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); } \ No newline at end of file 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 8921078..edbc30d 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceTierConfigMapper.java @@ -82,7 +82,7 @@ public interface PriceTierConfigMapper extends BaseMapper { * 插入阶梯定价配置 */ @Insert("INSERT INTO price_tier_config (product_type, product_id, scenic_id, min_quantity, max_quantity, price, " + - "original_price, unit, sort_order, is_active, created_time, updated_time) VALUES " + + "original_price, unit, sort_order, is_active, create_time, update_time) VALUES " + "(#{productType}, #{productId}, #{scenicId}, #{minQuantity}, #{maxQuantity}, #{price}, " + "#{originalPrice}, #{unit}, #{sortOrder}, #{isActive}, NOW(), NOW())") int insertTierConfig(PriceTierConfig config); @@ -92,12 +92,12 @@ public interface PriceTierConfigMapper extends BaseMapper { */ @Update("UPDATE price_tier_config SET product_id = #{productId}, scenic_id = #{scenicId}, min_quantity = #{minQuantity}, " + "max_quantity = #{maxQuantity}, price = #{price}, original_price = #{originalPrice}, unit = #{unit}, sort_order = #{sortOrder}, " + - "is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + "is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") int updateTierConfig(PriceTierConfig config); /** * 更新阶梯配置状态 */ - @Update("UPDATE price_tier_config SET is_active = #{isActive}, updated_time = NOW() WHERE id = #{id}") + @Update("UPDATE price_tier_config SET is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") int updateTierConfigStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); } \ No newline at end of file From dc8d9448e60acf1ffdc2d472bd5187508f9ccbc6 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 22 Aug 2025 17:59:15 +0800 Subject: [PATCH 13/18] =?UTF-8?q?feat(voucher):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E7=94=A8=E6=88=B7=E5=88=B8=E7=A0=81=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=88=97=E8=A1=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 getVoucherDetails 方法,用于获取指定用户在指定景区的券码详情列表 - 方法会根据券码状态设置可用性状态和不可用原因 - 优化了券码信息的构建过程,提高了代码复用性 --- .../pricing/service/IVoucherService.java | 8 +++ .../service/impl/VoucherServiceImpl.java | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java index 51be47e..ef0f4aa 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java @@ -43,6 +43,14 @@ public interface IVoucherService { */ boolean canClaimVoucher(Long faceId, Long scenicId); + /** + * 获取该faceId在scenicId下的券码详情列表 + * @param faceId 用户面部ID + * @param scenicId 景区ID + * @return 券码详情列表,包含所有状态的券码(已领取未使用、已使用等),如果没有券码则返回空列表 + */ + List getVoucherDetails(Long faceId, Long scenicId); + /** * 计算券码优惠金额 * @param voucherInfo 券码信息 diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java index 943267d..b983b39 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java @@ -133,6 +133,56 @@ public class VoucherServiceImpl implements IVoucherService { return count == 0; } + /** + * 获取该faceId在scenicId下的券码详情列表 + * @param faceId 用户面部ID + * @param scenicId 景区ID + * @return 券码详情列表,包含所有状态的券码(已领取未使用、已使用等),如果没有券码则返回空列表 + */ + public List getVoucherDetails(Long faceId, Long scenicId) { + if (faceId == null || scenicId == null) { + return new ArrayList<>(); + } + + List voucherCodes = voucherCodeMapper.selectUserVouchers(faceId, scenicId); + List voucherInfos = new ArrayList<>(); + + for (PriceVoucherCode voucherCode : voucherCodes) { + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCode.getBatchId()); + if (batchConfig != null && batchConfig.getDeleted() == 0) { + VoucherInfo voucherInfo = buildVoucherInfo(voucherCode, batchConfig); + + // 设置可用性状态 + VoucherCodeStatus statusEnum = VoucherCodeStatus.getByCode(voucherCode.getStatus()); + if (statusEnum != null) { + switch (statusEnum) { + case CLAIMED_UNUSED: + voucherInfo.setAvailable(true); + break; + case USED: + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码已使用"); + break; + case UNCLAIMED: + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码未领取"); + break; + default: + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码状态异常"); + } + } else { + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码状态未知"); + } + + voucherInfos.add(voucherInfo); + } + } + + return voucherInfos; + } + @Override public BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context) { if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable()) || From a04e7b51830287d260a8e83f11e58a442f258ce3 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 23 Aug 2025 14:34:34 +0800 Subject: [PATCH 14/18] =?UTF-8?q?feat(VideoRepository):=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=87=AD=E8=AF=81=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在判断视频是否购买的逻辑中添加了凭证检测 - 使用 DiscountDetectionContext 和 IVoucherService 来获取用户凭证信息 - 如果用户拥有全场免费的凭证,则认为已购买视频 --- .../ycwl/basic/repository/VideoRepository.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/ycwl/basic/repository/VideoRepository.java b/src/main/java/com/ycwl/basic/repository/VideoRepository.java index 5d3c5ab..c0d4ae6 100644 --- a/src/main/java/com/ycwl/basic/repository/VideoRepository.java +++ b/src/main/java/com/ycwl/basic/repository/VideoRepository.java @@ -3,6 +3,10 @@ package com.ycwl.basic.repository; import com.ycwl.basic.biz.PriceBiz; import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO; import com.ycwl.basic.model.mobile.order.IsBuyRespVO; +import com.ycwl.basic.pricing.dto.DiscountDetectionContext; +import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.service.IVoucherService; import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity; @@ -15,6 +19,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.util.List; import java.util.concurrent.TimeUnit; @Component @@ -29,6 +34,8 @@ public class VideoRepository { @Autowired @Lazy private PriceBiz priceBiz; + @Autowired + private IVoucherService iVoucherService; public VideoEntity getVideo(Long videoId) { if (redisTemplate.hasKey(String.format(VIDEO_CACHE_KEY, videoId))) { @@ -106,6 +113,14 @@ public class VideoRepository { if (buy.isBuy()) { return true; } + // 确认人员faceId是否有券码 + List voucherDetails = iVoucherService.getVoucherDetails(memberVideo.getFaceId(), memberVideo.getScenicId()); + if (voucherDetails != null && !voucherDetails.isEmpty()) { + VoucherInfo voucherInfo = voucherDetails.getFirst(); + if (voucherInfo.getDiscountType().equals(VoucherDiscountType.FREE_ALL)) { + isBuy = true; + } + } return isBuy; } From 4c794cdda25cc9eb93723f4c98ddcda00fd58e55 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 23 Aug 2025 14:38:24 +0800 Subject: [PATCH 15/18] =?UTF-8?q?feat(repository):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 SourceRepository 中添加了 IVoucherService 和 FaceRepository 的依赖 - 在 getUserIsBuy 方法中增加了对优惠券的验证逻辑 - 如果用户拥有全场免费的优惠券,则直接返回 true - 优化了代码结构,增加了日志记录 --- .../basic/repository/SourceRepository.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/com/ycwl/basic/repository/SourceRepository.java b/src/main/java/com/ycwl/basic/repository/SourceRepository.java index 1d1b9be..b0965dd 100644 --- a/src/main/java/com/ycwl/basic/repository/SourceRepository.java +++ b/src/main/java/com/ycwl/basic/repository/SourceRepository.java @@ -1,8 +1,13 @@ package com.ycwl.basic.repository; import com.ycwl.basic.mapper.SourceMapper; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity; +import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.enums.VoucherDiscountType; +import com.ycwl.basic.pricing.service.IVoucherService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @@ -10,12 +15,17 @@ import org.springframework.stereotype.Component; import java.util.List; import java.util.Objects; +@Slf4j @Component public class SourceRepository { @Autowired private SourceMapper sourceMapper; @Autowired private RedisTemplate redisTemplate; + @Autowired + private IVoucherService iVoucherService; + @Autowired + private FaceRepository faceRepository; public void addSource(SourceEntity source) { sourceMapper.add(source); @@ -42,6 +52,19 @@ public class SourceRepository { } public boolean getUserIsBuy(Long userId, int type, Long faceId) { + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + log.info("faceId:{} is not exist", faceId); + return false; + } + // 确认人员faceId是否有券码 + List voucherDetails = iVoucherService.getVoucherDetails(faceId, face.getScenicId()); + if (voucherDetails != null && !voucherDetails.isEmpty()) { + VoucherInfo voucherInfo = voucherDetails.getFirst(); + if (voucherInfo.getDiscountType().equals(VoucherDiscountType.FREE_ALL)) { + return true; + } + } switch (type) { case 1: List videoSourceList = sourceMapper.listVideoByFaceRelation(userId, faceId); From 0204b3bc230b453e79d7783322b6bb8f16290f2f Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 24 Aug 2025 01:16:16 +0800 Subject: [PATCH 16/18] =?UTF-8?q?feat(pricing):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=E6=89=93=E5=8D=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AppVoucherController 控制器处理打印请求 - 实现 VoucherPrintService 接口和 VoucherPrintServiceImpl 实现类 - 添加 VoucherPrintReq 请求对象和 VoucherPrintResp 响应对象 - 创建 VoucherPrintRecord 实体和对应的 Mapper - 更新 PriceVoucherCodeMapper 接口,添加随机获取未打印券码的方法 - 实现分布式锁机制防止重复打印- 生成流水号并记录打印状态 --- .../mobile/AppVoucherController.java | 40 ++++ .../pricing/dto/req/VoucherClaimReq.java | 1 + .../pricing/dto/req/VoucherPrintReq.java | 16 ++ .../pricing/dto/resp/VoucherPrintResp.java | 37 ++++ .../pricing/entity/VoucherPrintRecord.java | 70 +++++++ .../mapper/PriceVoucherCodeMapper.java | 7 + .../mapper/VoucherPrintRecordMapper.java | 42 +++++ .../pricing/service/VoucherPrintService.java | 17 ++ .../service/impl/VoucherCodeServiceImpl.java | 42 +++-- .../service/impl/VoucherPrintServiceImpl.java | 177 ++++++++++++++++++ .../mapper/PriceVoucherCodeMapper.xml | 126 +++++++++++++ .../mapper/VoucherPrintRecordMapper.xml | 52 +++++ 12 files changed, 616 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java create mode 100644 src/main/resources/mapper/PriceVoucherCodeMapper.xml create mode 100644 src/main/resources/mapper/VoucherPrintRecordMapper.xml diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java new file mode 100644 index 0000000..b3dc2f0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java @@ -0,0 +1,40 @@ +package com.ycwl.basic.controller.mobile; + +import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; +import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; +import com.ycwl.basic.pricing.service.VoucherPrintService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@Slf4j +@RestController +@RequestMapping("/api/mobile/voucher/v1") +public class AppVoucherController { + + @Autowired + private VoucherPrintService voucherPrintService; + + /** + * 打印小票 + * @param request 打印请求 + * @return 打印结果 + */ + @PostMapping("/print") + public ApiResponse printVoucherTicket(@RequestBody VoucherPrintReq request) { + log.info("收到打印小票请求: faceId={}, brokerId={}, scenicId={}", + request.getFaceId(), request.getBrokerId(), request.getScenicId()); + + VoucherPrintResp response = voucherPrintService.printVoucherTicket(request); + + log.info("打印小票完成: code={}, voucherCode={}, status={}", + response.getCode(), response.getVoucherCode(), response.getPrintStatus()); + + return ApiResponse.success(response); + } +} diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java index 385b1ac..9680742 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java @@ -7,4 +7,5 @@ public class VoucherClaimReq { private Long scenicId; private Long brokerId; private Long faceId; + private String code; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java new file mode 100644 index 0000000..dcfe54e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java @@ -0,0 +1,16 @@ +package com.ycwl.basic.pricing.dto.req; + +import lombok.Data; + +/** + * 打印小票请求 + */ +@Data +public class VoucherPrintReq { + + private Long faceId; + + private Long brokerId; + + private Long scenicId; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java new file mode 100644 index 0000000..4a9e552 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java @@ -0,0 +1,37 @@ +package com.ycwl.basic.pricing.dto.resp; + +import lombok.Data; + +import java.util.Date; + +/** + * 打印小票响应 + */ +@Data +public class VoucherPrintResp { + + /** + * 流水号 + */ + private String code; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 打印状态:0=待打印,1=打印成功,2=打印失败 + */ + private Integer printStatus; + + /** + * 错误信息 + */ + private String errorMessage; + + /** + * 创建时间 + */ + private Date createTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java new file mode 100644 index 0000000..49311c1 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * 优惠券打印记录实体 + */ +@Data +@TableName("voucher_print_record") +public class VoucherPrintRecord { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 流水号 + */ + private String code; + + /** + * 用户faceId + */ + private Long faceId; + + /** + * 经纪人ID + */ + private Long brokerId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 券码ID + */ + private Long voucherCodeId; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 打印状态:0=待打印,1=打印成功,2=打印失败 + */ + private Integer printStatus; + + /** + * 错误信息 + */ + private String errorMessage; + + @TableField("create_time") + private Date createTime; + + @TableField("update_time") + private Date updateTime; + + private Integer deleted; + + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java index 732d414..d7b0048 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java @@ -91,4 +91,11 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @return 可用券码 */ PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId); + + /** + * 随机获取一个未被打印过的可用券码 + * @param scenicId 景区ID + * @return 可用券码 + */ + PriceVoucherCode findRandomUnprintedVoucher(@Param("scenicId") Long scenicId); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java new file mode 100644 index 0000000..505f326 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java @@ -0,0 +1,42 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.pricing.entity.VoucherPrintRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 优惠券打印记录Mapper + */ +@Mapper +public interface VoucherPrintRecordMapper extends BaseMapper { + + /** + * 根据faceId、brokerId、scenicId查询已存在的打印记录 + * @param faceId 用户faceId + * @param brokerId 经纪人ID + * @param scenicId 景区ID + * @return 打印记录 + */ + VoucherPrintRecord selectByFaceBrokerScenic(@Param("faceId") Long faceId, + @Param("brokerId") Long brokerId, + @Param("scenicId") Long scenicId); + + /** + * 根据券码ID查询是否已被打印 + * @param voucherCodeId 券码ID + * @return 打印记录 + */ + VoucherPrintRecord selectByVoucherCodeId(@Param("voucherCodeId") Long voucherCodeId); + + /** + * 更新打印状态 + * @param id 记录ID + * @param printStatus 打印状态 + * @param errorMessage 错误信息(可为null) + * @return 影响行数 + */ + int updatePrintStatus(@Param("id") Long id, + @Param("printStatus") Integer printStatus, + @Param("errorMessage") String errorMessage); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java new file mode 100644 index 0000000..c90e65f --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java @@ -0,0 +1,17 @@ +package com.ycwl.basic.pricing.service; + +import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; +import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; + +/** + * 优惠券打印服务 + */ +public interface VoucherPrintService { + + /** + * 打印小票 + * @param request 打印请求 + * @return 打印响应 + */ + VoucherPrintResp printVoucherTicket(VoucherPrintReq request); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java index d3710b2..b2cabfc 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java @@ -80,30 +80,50 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { if (req.getFaceId() == null) { throw new BizException(400, "用户faceId不能为空"); } + if (!StringUtils.hasText(req.getCode())) { + throw new BizException(400, "券码不能为空"); + } + + // 验证券码是否存在且未被领取 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PriceVoucherCode::getCode, req.getCode()) + .eq(PriceVoucherCode::getScenicId, req.getScenicId()) + .eq(PriceVoucherCode::getDeleted, 0); + + PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper); + if (voucherCode == null) { + throw new BizException(400, "券码不存在或不属于该景区"); + } + + if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) { + throw new BizException(400, "券码已被领取或已使用"); + } if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) { throw new BizException(400, "该用户在此景区已领取过券码"); } - PriceVoucherBatchConfig batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId()); - if (batch == null) { - throw new BizException(400, "暂无可用券码批次"); + // 获取券码所属批次 + PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId()); + if (batch == null || batch.getDeleted() == 1) { + throw new BizException(400, "券码批次不存在"); } - PriceVoucherCode availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId()); - if (availableCode == null) { - throw new BizException(400, "券码已领完"); + // 验证批次是否可用于该推客 + if (!Objects.equals(batch.getBrokerId(), req.getBrokerId())) { + throw new BizException(400, "券码不属于该推客"); } - availableCode.setFaceId(req.getFaceId()); - availableCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode()); - availableCode.setClaimedTime(new Date()); + // 更新券码状态 + voucherCode.setFaceId(req.getFaceId()); + voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode()); + voucherCode.setClaimedTime(new Date()); - voucherCodeMapper.updateById(availableCode); + voucherCodeMapper.updateById(voucherCode); voucherBatchService.updateBatchClaimedCount(batch.getId()); - return convertToResp(availableCode, batch); + return convertToResp(voucherCode, batch); } @Override diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java new file mode 100644 index 0000000..56858ae --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java @@ -0,0 +1,177 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; +import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import com.ycwl.basic.pricing.entity.VoucherPrintRecord; +import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; +import com.ycwl.basic.pricing.mapper.VoucherPrintRecordMapper; +import com.ycwl.basic.pricing.service.VoucherPrintService; +import com.ycwl.basic.repository.FaceRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * 优惠券打印服务实现 + */ +@Slf4j +@Service +public class VoucherPrintServiceImpl implements VoucherPrintService { + + @Autowired + private VoucherPrintRecordMapper voucherPrintRecordMapper; + + @Autowired + private PriceVoucherCodeMapper priceVoucherCodeMapper; + + @Autowired + private FaceRepository faceRepository; + + @Autowired + private RedisTemplate redisTemplate; + + private static final String PRINT_LOCK_KEY = "voucher_print_lock:%s:%s:%s"; // faceId:brokerId:scenicId + private static final String CODE_PREFIX = "VT"; // Voucher Ticket + + @Override + @Transactional(rollbackFor = Exception.class) + public VoucherPrintResp printVoucherTicket(VoucherPrintReq request) { + // 参数验证 + if (request.getFaceId() == null) { + throw new BaseException("用户faceId不能为空"); + } + if (request.getBrokerId() == null) { + throw new BaseException("经纪人ID不能为空"); + } + if (request.getScenicId() == null) { + throw new BaseException("景区ID不能为空"); + } + + Long currentUserId = Long.valueOf(BaseContextHandler.getUserId()); + + // 验证faceId是否属于当前用户 + validateFaceOwnership(request.getFaceId(), currentUserId); + + // 使用Redis分布式锁防止重复打印 + String lockKey = String.format(PRINT_LOCK_KEY, request.getFaceId(), request.getBrokerId(), request.getScenicId()); + String lockValue = UUID.randomUUID().toString(); + + try { + // 尝试获取锁,超时时间30秒 + Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); + if (!lockAcquired) { + throw new BaseException("请求处理中,请稍后再试"); + } + + // 检查是否已存在相同的打印记录 + VoucherPrintRecord existingRecord = voucherPrintRecordMapper.selectByFaceBrokerScenic( + request.getFaceId(), request.getBrokerId(), request.getScenicId()); + + if (existingRecord != null) { + log.info("找到已存在的打印记录,返回该记录: {}", existingRecord.getId()); + return buildResponse(existingRecord); + } + + // 获取一个可用的券码(未被打印过的) + PriceVoucherCode availableVoucher = getAvailableUnprintedVoucher(request.getScenicId()); + if (availableVoucher == null) { + throw new BaseException("暂无可用优惠券"); + } + + // 生成流水号 + String code = generateCode(); + + // 创建打印记录 + VoucherPrintRecord printRecord = new VoucherPrintRecord(); + printRecord.setCode(code); + printRecord.setFaceId(request.getFaceId()); + printRecord.setBrokerId(request.getBrokerId()); + printRecord.setScenicId(request.getScenicId()); + printRecord.setVoucherCodeId(availableVoucher.getId()); + printRecord.setVoucherCode(availableVoucher.getCode()); + printRecord.setPrintStatus(0); // 待打印 + printRecord.setCreateTime(new Date()); + printRecord.setUpdateTime(new Date()); + printRecord.setDeleted(0); + + voucherPrintRecordMapper.insert(printRecord); + + // TODO: 调用打印机接口打印小票 + // printTicket(printRecord); + + // 暂时标记为打印成功状态(实际应该在打印成功回调中更新) + printRecord.setPrintStatus(1); + voucherPrintRecordMapper.updatePrintStatus(printRecord.getId(), 1, null); + + log.info("成功创建打印记录: {}, 券码: {}", code, availableVoucher.getCode()); + return buildResponse(printRecord); + + } finally { + // 释放锁 + if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { + redisTemplate.delete(lockKey); + } + } + } + + /** + * 验证faceId是否属于当前用户 + */ + private void validateFaceOwnership(Long faceId, Long currentUserId) { + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + throw new BaseException("用户人脸信息不存在"); + } + + if (!currentUserId.equals(face.getMemberId())) { + throw new BaseException("无权限操作该人脸信息"); + } + } + + /** + * 获取可用且未被打印过的券码 + */ + private PriceVoucherCode getAvailableUnprintedVoucher(Long scenicId) { + return priceVoucherCodeMapper.findRandomUnprintedVoucher(scenicId); + } + + /** + * 生成流水号 + */ + private String generateCode() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + String timestamp = sdf.format(new Date()); + String randomSuffix = String.valueOf((int)(Math.random() * 1000)).formatted("%03d"); + return CODE_PREFIX + timestamp + randomSuffix; + } + + /** + * 构建响应对象 + */ + private VoucherPrintResp buildResponse(VoucherPrintRecord record) { + VoucherPrintResp response = new VoucherPrintResp(); + BeanUtils.copyProperties(record, response); + return response; + } + + /** + * 调用打印机接口(待实现) + */ + private void printTicket(VoucherPrintRecord record) { + // TODO: 实现打印机接口调用逻辑 + log.info("TODO: 调用打印机打印小票,记录ID: {}, 券码: {}", record.getId(), record.getVoucherCode()); + } +} \ No newline at end of file diff --git a/src/main/resources/mapper/PriceVoucherCodeMapper.xml b/src/main/resources/mapper/PriceVoucherCodeMapper.xml new file mode 100644 index 0000000..2084c0d --- /dev/null +++ b/src/main/resources/mapper/PriceVoucherCodeMapper.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, + remark, create_time, update_time, deleted, deleted_at + + + + + + + + + + + + UPDATE price_voucher_code + SET status = 1, + face_id = #{faceId}, + claimed_time = #{claimedTime}, + update_time = NOW() + WHERE id = #{id} + AND status = 0 + AND deleted = 0 + + + + UPDATE price_voucher_code + SET status = 2, + used_time = #{usedTime}, + remark = #{remark}, + update_time = NOW() + WHERE code = #{code} + AND status = 1 + AND deleted = 0 + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/VoucherPrintRecordMapper.xml b/src/main/resources/mapper/VoucherPrintRecordMapper.xml new file mode 100644 index 0000000..87a8dfa --- /dev/null +++ b/src/main/resources/mapper/VoucherPrintRecordMapper.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + id, code, face_id, broker_id, scenic_id, voucher_code_id, voucher_code, + print_status, error_message, create_time, update_time, deleted, deleted_at + + + + + + + + UPDATE voucher_print_record + SET print_status = #{printStatus}, + error_message = #{errorMessage}, + update_time = NOW() + WHERE id = #{id} + + + \ No newline at end of file From ea9945b9e02721efbcbc380ec2a1c7ecfebb7109 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 24 Aug 2025 15:43:14 +0800 Subject: [PATCH 17/18] =?UTF-8?q?fix(pricing):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=E6=89=93=E5=8D=B0=E6=B5=81=E6=B0=B4?= =?UTF-8?q?=E5=8F=B7=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -引入 AtomicLong 作为原子计数器,确保流水号的唯一性 - 修改生成流水号的方法,使用毫秒级时间戳和原子计数器组合 - 新方案解决了原方法在高并发情况下可能出现的重复流水号问题 - 优化了 FaceEntity 查询逻辑,确保 faceId 属于当前用户 --- .../service/impl/VoucherPrintServiceImpl.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java index 56858ae..99790bc 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java @@ -23,6 +23,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; /** * 优惠券打印服务实现 @@ -44,7 +45,9 @@ public class VoucherPrintServiceImpl implements VoucherPrintService { private RedisTemplate redisTemplate; private static final String PRINT_LOCK_KEY = "voucher_print_lock:%s:%s:%s"; // faceId:brokerId:scenicId - private static final String CODE_PREFIX = "VT"; // Voucher Ticket + + // 原子计数器,确保流水号唯一性 + private static final AtomicLong counter = new AtomicLong(0); @Override @Transactional(rollbackFor = Exception.class) @@ -56,10 +59,12 @@ public class VoucherPrintServiceImpl implements VoucherPrintService { if (request.getBrokerId() == null) { throw new BaseException("经纪人ID不能为空"); } - if (request.getScenicId() == null) { - throw new BaseException("景区ID不能为空"); + FaceEntity face = faceRepository.getFace(request.getFaceId()); + if (face == null) { + throw new BaseException("请上传人脸"); } - + request.setScenicId(face.getScenicId()); + Long currentUserId = Long.valueOf(BaseContextHandler.getUserId()); // 验证faceId是否属于当前用户 @@ -149,13 +154,16 @@ public class VoucherPrintServiceImpl implements VoucherPrintService { } /** - * 生成流水号 + * 生成流水号(优化版本) + * 使用原子计数器确保唯一性,解决原方法在高并发下的重复问题 */ private String generateCode() { - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + // 方案:使用毫秒级时间戳 + 原子计数器 + // 格式:5位计数器 + SSS + SimpleDateFormat sdf = new SimpleDateFormat("SSS"); String timestamp = sdf.format(new Date()); - String randomSuffix = String.valueOf((int)(Math.random() * 1000)).formatted("%03d"); - return CODE_PREFIX + timestamp + randomSuffix; + long count = counter.incrementAndGet() % 100000; // 5位计数,循环使用 + return String.format("%05d", count) + timestamp; } /** From 6b20e700f030444f0fd88218f213732a58715177 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 25 Aug 2025 09:36:40 +0800 Subject: [PATCH 18/18] =?UTF-8?q?feat(voucher):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=B7=B2=E6=89=93=E5=8D=B0=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E5=92=8C=E8=87=AA=E5=8A=A8=E9=A2=86=E5=88=B8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 queryPrintedVoucher 方法查询已打印的凭证 - 新增 claimVoucher 方法实现自动领取凭证 - 优化 printVoucherTicket 方法,移除冗余参数 - 更新相关 mapper 和 XML 文件以支持新功能 --- .../mobile/AppVoucherController.java | 37 ++++++++++++++++++- .../mapper/VoucherPrintRecordMapper.java | 4 +- .../pricing/service/VoucherPrintService.java | 2 + .../service/impl/VoucherPrintServiceImpl.java | 28 ++++++++++++-- .../mapper/VoucherPrintRecordMapper.xml | 1 - 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java index b3dc2f0..7fcd369 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java @@ -1,14 +1,23 @@ package com.ycwl.basic.controller.mobile; +import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; import com.ycwl.basic.pricing.dto.req.VoucherPrintReq; +import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp; import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp; +import com.ycwl.basic.pricing.service.VoucherCodeService; import com.ycwl.basic.pricing.service.VoucherPrintService; +import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.utils.ApiResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -19,7 +28,11 @@ public class AppVoucherController { @Autowired private VoucherPrintService voucherPrintService; - + @Autowired + private VoucherCodeService voucherCodeService; + @Autowired + private FaceRepository faceRepository; + /** * 打印小票 * @param request 打印请求 @@ -37,4 +50,26 @@ public class AppVoucherController { return ApiResponse.success(response); } + + @GetMapping("/printed") + public ApiResponse queryPrintedVoucher( + @RequestParam Long faceId + ) { + return ApiResponse.success(voucherPrintService.queryPrintedVoucher(faceId)); + } + + @PostMapping("/claim") + public ApiResponse claimVoucher(@RequestBody VoucherClaimReq req) { + FaceEntity face = faceRepository.getFace(req.getFaceId()); + if (face == null) { + throw new BaseException("请选择人脸"); + } + if (!face.getMemberId().equals(Long.valueOf(BaseContextHandler.getUserId()))) { + throw new BaseException("自动领取失败"); + } + req.setScenicId(face.getScenicId()); + VoucherCodeResp result = voucherCodeService.claimVoucher(req); + return ApiResponse.success(result); + } + } diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java index 505f326..61a6105 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java @@ -14,12 +14,10 @@ public interface VoucherPrintRecordMapper extends BaseMapper /** * 根据faceId、brokerId、scenicId查询已存在的打印记录 * @param faceId 用户faceId - * @param brokerId 经纪人ID * @param scenicId 景区ID * @return 打印记录 */ - VoucherPrintRecord selectByFaceBrokerScenic(@Param("faceId") Long faceId, - @Param("brokerId") Long brokerId, + VoucherPrintRecord selectByFaceBrokerScenic(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); /** diff --git a/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java index c90e65f..a36cdad 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java @@ -14,4 +14,6 @@ public interface VoucherPrintService { * @return 打印响应 */ VoucherPrintResp printVoucherTicket(VoucherPrintReq request); + + VoucherPrintResp queryPrintedVoucher(Long faceId); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java index 99790bc..e57ccf2 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java @@ -1,6 +1,5 @@ package com.ycwl.basic.pricing.service.impl; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.model.pc.face.entity.FaceEntity; @@ -77,13 +76,13 @@ public class VoucherPrintServiceImpl implements VoucherPrintService { try { // 尝试获取锁,超时时间30秒 Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); - if (!lockAcquired) { + if (Boolean.FALSE.equals(lockAcquired)) { throw new BaseException("请求处理中,请稍后再试"); } // 检查是否已存在相同的打印记录 VoucherPrintRecord existingRecord = voucherPrintRecordMapper.selectByFaceBrokerScenic( - request.getFaceId(), request.getBrokerId(), request.getScenicId()); + request.getFaceId(), request.getScenicId()); if (existingRecord != null) { log.info("找到已存在的打印记录,返回该记录: {}", existingRecord.getId()); @@ -131,7 +130,28 @@ public class VoucherPrintServiceImpl implements VoucherPrintService { } } } - + + @Override + public VoucherPrintResp queryPrintedVoucher(Long faceId) { + + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + throw new BaseException("请上传人脸"); + } + + Long currentUserId = Long.valueOf(BaseContextHandler.getUserId()); + + // 验证faceId是否属于当前用户 + validateFaceOwnership(faceId, currentUserId); + // 检查是否已存在相同的打印记录 + VoucherPrintRecord existingRecord = voucherPrintRecordMapper.selectByFaceBrokerScenic( + faceId, face.getScenicId()); + if (existingRecord == null) { + return null; + } + return buildResponse(existingRecord); + } + /** * 验证faceId是否属于当前用户 */ diff --git a/src/main/resources/mapper/VoucherPrintRecordMapper.xml b/src/main/resources/mapper/VoucherPrintRecordMapper.xml index 87a8dfa..8d33bae 100644 --- a/src/main/resources/mapper/VoucherPrintRecordMapper.xml +++ b/src/main/resources/mapper/VoucherPrintRecordMapper.xml @@ -27,7 +27,6 @@ SELECT FROM voucher_print_record WHERE face_id = #{faceId} - AND broker_id = #{brokerId} AND scenic_id = #{scenicId} AND deleted = 0 LIMIT 1