From e43809593b714029449c331c326765a5aa9b59a2 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 13 Sep 2025 23:42:28 +0800 Subject: [PATCH 1/6] =?UTF-8?q?refactor(basic):=20=E7=A7=BB=E9=99=A4=20Pri?= =?UTF-8?q?ceRepository=20=E4=B8=AD=E7=9A=84=E7=BC=93=E5=AD=98=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了与 Redis 缓存相关的字段和方法 -移除了 clearPriceCache 和 clearPriceScenicCache 方法 -简化了 getPriceConfig 方法,移除缓存逻辑 --- .../basic/repository/PriceRepository.java | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/main/java/com/ycwl/basic/repository/PriceRepository.java b/src/main/java/com/ycwl/basic/repository/PriceRepository.java index 376dbad4..048a7508 100644 --- a/src/main/java/com/ycwl/basic/repository/PriceRepository.java +++ b/src/main/java/com/ycwl/basic/repository/PriceRepository.java @@ -2,19 +2,13 @@ package com.ycwl.basic.repository; import com.ycwl.basic.pricing.entity.PriceOnePriceConfig; import com.ycwl.basic.pricing.service.IOnePricePurchaseService; -import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; -import java.util.Set; - @Component public class PriceRepository { - @Autowired - private RedisTemplate redisTemplate; - public static final String PRICE_ID_CACHE = "price:%s"; @Autowired private IOnePricePurchaseService onePricePurchaseService; @@ -48,27 +42,7 @@ public class PriceRepository { priceConfig.setPrice(config.getOnePrice()); return priceConfig; } - String cacheKey = String.format(PRICE_ID_CACHE, id); - PriceConfigEntity priceConfigEntity = null; - if (redisTemplate.hasKey(cacheKey)) { - priceConfigEntity = JacksonUtil.parseObject(redisTemplate.opsForValue().get(cacheKey), PriceConfigEntity.class); - } - return priceConfigEntity; + return null; } - public void clearPriceCache(Integer id) { - if (redisTemplate.hasKey(String.format(PRICE_ID_CACHE, id))) { - PriceConfigEntity priceConfig = getPriceConfig(id); - if (priceConfig != null) { - clearPriceScenicCache(priceConfig.getScenicId()); - } - } - redisTemplate.delete(String.format(PRICE_ID_CACHE, id)); - } - - public void clearPriceScenicCache(Long scenicId) { - String cacheKey = String.format("price:s%s:*", scenicId); - Set keys = redisTemplate.keys(cacheKey); - redisTemplate.delete(keys); - } } From 5531c576e02bd2d9a168279161ffce4eb807d93e Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 13 Sep 2025 23:44:15 +0800 Subject: [PATCH 2/6] =?UTF-8?q?refactor(basic):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=99=AF=E5=8C=BA=E7=BC=93=E5=AD=98=E7=9B=B8=E5=85=B3=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了 ScenicRepository 中的缓存键常量 - 移除了 getScenicBasic 和 getScenic 方法中的缓存逻辑 - 删除了 clearCache 方法 - 移除了与缓存相关的工具方法 --- .../basic/repository/ScenicRepository.java | 100 ------------------ 1 file changed, 100 deletions(-) diff --git a/src/main/java/com/ycwl/basic/repository/ScenicRepository.java b/src/main/java/com/ycwl/basic/repository/ScenicRepository.java index 436c6ca3..a2e5365b 100644 --- a/src/main/java/com/ycwl/basic/repository/ScenicRepository.java +++ b/src/main/java/com/ycwl/basic/repository/ScenicRepository.java @@ -41,23 +41,17 @@ public class ScenicRepository { @Autowired private ScenicConfigIntegrationService scenicConfigIntegrationService; - public static final String SCENIC_CACHE_KEY = "scenic:%s"; - public static final String SCENIC_BASIC_CACHE_KEY = "scenic:basic:%s"; - public static final String SCENIC_FULL_CACHE_KEY = "scenic:f%s"; - public static final String SCENIC_CONFIG_CACHE_KEY = "scenic:%s:config"; public static final String SCENIC_MP_CACHE_KEY = "scenic:%s:mp"; public static final String SCENIC_MP_NOTIFY_CACHE_KEY = "scenic:%s:mpNotify"; @Autowired private MpNotifyConfigMapper mpNotifyConfigMapper; public ScenicV2DTO getScenicBasic(Long id) { - String key = String.format(SCENIC_BASIC_CACHE_KEY, id); ScenicV2DTO scenicDTO = scenicIntegrationService.getScenic(id); return scenicDTO; } public ScenicEntity getScenic(Long id) { - String key = String.format(SCENIC_CACHE_KEY, id); ScenicV2WithConfigDTO scenicDTO = scenicIntegrationService.getScenicWithConfig(id); ScenicEntity scenicEntity = convertToScenicEntity(scenicDTO); return scenicEntity; @@ -226,27 +220,6 @@ public class ScenicRepository { } } - public void clearCache(Long scenicId) { - redisTemplate.delete(String.format(SCENIC_CACHE_KEY, scenicId)); - redisTemplate.delete(String.format(SCENIC_BASIC_CACHE_KEY, scenicId)); - redisTemplate.delete(String.format(SCENIC_FULL_CACHE_KEY, scenicId)); - redisTemplate.delete(String.format(SCENIC_CONFIG_CACHE_KEY, scenicId)); - redisTemplate.delete(String.format(SCENIC_MP_CACHE_KEY, scenicId)); - redisTemplate.delete(String.format(SCENIC_MP_NOTIFY_CACHE_KEY, scenicId)); - } - - private ScenicEntity convertToScenicEntity(ScenicV2DTO dto) { - if (dto == null) { - return null; - } - ScenicEntity entity = new ScenicEntity(); - entity.setId(Long.parseLong(dto.getId())); - entity.setName(dto.getName()); - entity.setMpId(dto.getMpId()); - entity.setStatus(dto.getStatus().toString()); - return entity; - } - private ScenicEntity convertToScenicEntity(ScenicV2WithConfigDTO dto) { if (dto == null) { return null; @@ -272,61 +245,6 @@ public class ScenicRepository { return entity; } - private ScenicConfigEntity convertToScenicConfigEntity(ScenicV2WithConfigDTO dto, Long scenicId) { - if (dto == null || dto.getConfig() == null) { - return null; - } - - ScenicConfigEntity entity = new ScenicConfigEntity(); - entity.setScenicId(scenicId); - - java.util.Map config = dto.getConfig(); - - entity.setBookRoutine(ConfigValueUtil.getIntValue(config, "bookRoutine")); - entity.setForceFinishTime(ConfigValueUtil.getIntValue(config, "forceFinishTime")); - entity.setTourTime(ConfigValueUtil.getIntValue(config, "tourTime")); - entity.setSampleStoreDay(ConfigValueUtil.getIntValue(config, "sampleStoreDay")); - entity.setFaceStoreDay(ConfigValueUtil.getIntValue(config, "faceStoreDay")); - entity.setVideoStoreDay(ConfigValueUtil.getIntValue(config, "videoStoreDay")); - entity.setAllFree(ConfigValueUtil.getBooleanValue(config, "allFree")); - entity.setDisableSourceVideo(ConfigValueUtil.getBooleanValue(config, "disableSourceVideo")); - entity.setDisableSourceImage(ConfigValueUtil.getBooleanValue(config, "disableSourceImage")); - entity.setTemplateNewVideoType(ConfigValueUtil.getIntValue(config, "templateNewVideoType")); - entity.setAntiScreenRecordType(ConfigValueUtil.getIntValue(config, "antiScreenRecordType")); - entity.setVideoSourceStoreDay(ConfigValueUtil.getIntValue(config, "videoSourceStoreDay")); - entity.setImageSourceStoreDay(ConfigValueUtil.getIntValue(config, "imageSourceStoreDay")); - entity.setUserSourceExpireDay(ConfigValueUtil.getIntValue(config, "userSourceExpireDay")); - entity.setFaceDetectHelperThreshold(ConfigValueUtil.getIntValue(config, "faceDetectHelperThreshold")); - entity.setPhotoFreeNum(ConfigValueUtil.getIntValue(config, "photoFreeNum")); - entity.setVideoFreeNum(ConfigValueUtil.getIntValue(config, "videoFreeNum")); - entity.setVoucherEnable(ConfigValueUtil.getBooleanValue(config, "voucherEnable")); - - entity.setFaceScoreThreshold(ConfigValueUtil.getFloatValue(config, "faceScoreThreshold")); - entity.setBrokerDirectRate(ConfigValueUtil.getBigDecimalValue(config, "brokerDirectRate")); - - entity.setWatermarkType(ConfigValueUtil.getStringValue(config, "watermarkType")); - entity.setWatermarkScenicText(ConfigValueUtil.getStringValue(config, "watermarkScenicText")); - entity.setWatermarkDtFormat(ConfigValueUtil.getStringValue(config, "watermarkDtFormat")); - entity.setImageSourcePackHint(ConfigValueUtil.getStringValue(config, "imageSourcePackHint")); - entity.setVideoSourcePackHint(ConfigValueUtil.getStringValue(config, "videoSourcePackHint")); - entity.setExtraNotificationTime(ConfigValueUtil.getStringValue(config, "extraNotificationTime")); - - entity.setStoreType(ConfigValueUtil.getEnumValue(config, "storeType", StorageType.class)); - entity.setStoreConfigJson(ConfigValueUtil.getStringValue(config, "storeConfigJson")); - entity.setTmpStoreType(ConfigValueUtil.getEnumValue(config, "tmpStoreType", StorageType.class)); - entity.setTmpStoreConfigJson(ConfigValueUtil.getStringValue(config, "tmpStoreConfigJson")); - entity.setLocalStoreType(ConfigValueUtil.getEnumValue(config, "localStoreType", StorageType.class)); - entity.setLocalStoreConfigJson(ConfigValueUtil.getStringValue(config, "localStoreConfigJson")); - - entity.setFaceType(ConfigValueUtil.getEnumValue(config, "faceType", FaceBodyAdapterType.class)); - entity.setFaceConfigJson(ConfigValueUtil.getStringValue(config, "faceConfigJson")); - - entity.setPayType(ConfigValueUtil.getEnumValue(config, "payType", PayAdapterType.class)); - entity.setPayConfigJson(ConfigValueUtil.getStringValue(config, "payConfigJson")); - - return entity; - } - /** * 获取景区配置管理器 * @@ -341,24 +259,6 @@ public class ScenicRepository { return null; } } - - /** - * 获取景区配置管理器,带缓存支持 - * - * @param scenicId 景区ID - * @return ScenicConfigManager实例,如果获取失败返回null - */ - public ScenicConfigManager getScenicConfigManagerWithCache(Long scenicId) { - String key = String.format(SCENIC_CONFIG_CACHE_KEY + ":manager", scenicId); - - List configList = - scenicConfigIntegrationService.listConfigs(scenicId); - if (configList != null) { - ScenicConfigManager manager = new ScenicConfigManager(configList); - return manager; - } - return null; - } /** * 批量获取景区名称 From ce3f7aae1e073c8876dd8a2317132efd6fae20eb Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 16 Sep 2025 01:08:54 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(voucher):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=88=B8=E7=A0=81=E9=87=8D=E5=A4=8D=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增VoucherBatchCreateReqV2 请求对象,用于创建支持重复使用的券码批次 - 添加 VoucherUsageController 控制器,实现券码使用记录和统计功能 - 在VoucherInfo 对象中增加与重复使用相关的字段 - 修改 PriceVoucherBatchConfig 和 PriceVoucherCode 实体,支持重复使用相关属性 - 更新 VoucherBatchServiceImpl 和 VoucherServiceImpl,增加处理重复使用逻辑的方法 --- .../VoucherManagementController.java | 7 + .../controller/VoucherUsageController.java | 97 +++++++++ .../ycwl/basic/pricing/dto/VoucherInfo.java | 30 +++ .../dto/req/VoucherBatchCreateReqV2.java | 73 +++++++ .../dto/req/VoucherUsageHistoryReq.java | 55 +++++ .../dto/resp/VoucherUsageRecordResp.java | 76 +++++++ .../dto/resp/VoucherUsageStatsResp.java | 85 ++++++++ .../entity/PriceVoucherBatchConfig.java | 15 ++ .../pricing/entity/PriceVoucherCode.java | 10 + .../entity/PriceVoucherUsageRecord.java | 76 +++++++ .../pricing/enums/VoucherCodeStatus.java | 45 +++- .../mapper/PriceVoucherUsageRecordMapper.java | 121 +++++++++++ .../pricing/service/IVoucherUsageService.java | 67 ++++++ .../pricing/service/VoucherBatchService.java | 6 + .../service/impl/VoucherBatchServiceImpl.java | 67 ++++++ .../service/impl/VoucherServiceImpl.java | 166 +++++++++++++-- .../service/impl/VoucherUsageServiceImpl.java | 192 ++++++++++++++++++ 17 files changed, 1167 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/pricing/controller/VoucherUsageController.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReqV2.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/req/VoucherUsageHistoryReq.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageRecordResp.java create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageStatsResp.java create mode 100644 src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherUsageRecord.java create mode 100644 src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IVoucherUsageService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/VoucherUsageServiceImpl.java 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 573870ec..4e135693 100644 --- a/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/VoucherManagementController.java @@ -2,6 +2,7 @@ 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.VoucherBatchCreateReqV2; import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; import com.ycwl.basic.pricing.dto.req.VoucherClaimReq; import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq; @@ -34,6 +35,12 @@ public class VoucherManagementController { return ApiResponse.success(batchId); } + @PostMapping("/batch/create/v2") + public ApiResponse createBatchV2(@RequestBody VoucherBatchCreateReqV2 req) { + Long batchId = voucherBatchService.createBatchV2(req); + return ApiResponse.success(batchId); + } + @PostMapping("/batch/list") public ApiResponse> getBatchList(@RequestBody VoucherBatchQueryReq req) { Page page = voucherBatchService.queryBatchList(req); diff --git a/src/main/java/com/ycwl/basic/pricing/controller/VoucherUsageController.java b/src/main/java/com/ycwl/basic/pricing/controller/VoucherUsageController.java new file mode 100644 index 00000000..b4836415 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/controller/VoucherUsageController.java @@ -0,0 +1,97 @@ +package com.ycwl.basic.pricing.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.common.ApiResponse; +import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp; +import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp; +import com.ycwl.basic.pricing.service.IVoucherUsageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 券码使用记录管理控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/pricing/voucher/usage") +@RequiredArgsConstructor +@Tag(name = "券码使用记录管理", description = "券码使用历史和统计API") +public class VoucherUsageController { + + private final IVoucherUsageService voucherUsageService; + + @PostMapping("/history") + @Operation(summary = "分页查询券码使用记录") + public ApiResponse> getUsageHistory(@RequestBody VoucherUsageHistoryReq req) { + try { + Page result = voucherUsageService.getUsageHistory(req); + return ApiResponse.success(result); + } catch (Exception e) { + log.error("查询券码使用记录失败", e); + return ApiResponse.fail("查询失败: " + e.getMessage()); + } + } + + @GetMapping("/{voucherCode}/records") + @Operation(summary = "获取指定券码的使用记录") + public ApiResponse> getRecordsByCode( + @Parameter(description = "券码") @PathVariable String voucherCode) { + try { + List records = voucherUsageService.getUsageRecordsByCode(voucherCode); + return ApiResponse.success(records); + } catch (Exception e) { + log.error("查询券码使用记录失败: {}", voucherCode, e); + return ApiResponse.fail("查询失败: " + e.getMessage()); + } + } + + @GetMapping("/{voucherCode}/stats") + @Operation(summary = "获取券码使用统计信息") + public ApiResponse getUsageStats( + @Parameter(description = "券码") @PathVariable String voucherCode) { + try { + VoucherUsageStatsResp stats = voucherUsageService.getUsageStats(voucherCode); + if (stats == null) { + return ApiResponse.fail("券码不存在"); + } + return ApiResponse.success(stats); + } catch (Exception e) { + log.error("查询券码统计信息失败: {}", voucherCode, e); + return ApiResponse.fail("查询失败: " + e.getMessage()); + } + } + + @GetMapping("/user/{faceId}/scenic/{scenicId}") + @Operation(summary = "获取用户在指定景区的券码使用记录") + public ApiResponse> getUserUsageRecords( + @Parameter(description = "用户faceId") @PathVariable Long faceId, + @Parameter(description = "景区ID") @PathVariable Long scenicId) { + try { + List records = voucherUsageService.getUserUsageRecords(faceId, scenicId); + return ApiResponse.success(records); + } catch (Exception e) { + log.error("查询用户券码使用记录失败: faceId={}, scenicId={}", faceId, scenicId, e); + return ApiResponse.fail("查询失败: " + e.getMessage()); + } + } + + @GetMapping("/batch/{batchId}/stats") + @Operation(summary = "获取批次券码使用统计信息") + public ApiResponse> getBatchUsageStats( + @Parameter(description = "批次ID") @PathVariable Long batchId) { + try { + List statsList = voucherUsageService.getBatchUsageStats(batchId); + return ApiResponse.success(statsList); + } catch (Exception e) { + log.error("查询批次券码统计信息失败: batchId={}", batchId, e); + return ApiResponse.fail("查询失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java index 1492af2e..88f7ce89 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java @@ -89,4 +89,34 @@ public class VoucherInfo { * null表示适用所有商品类型 */ private List applicableProducts; + + /** + * 当前使用次数 + */ + private Integer currentUseCount; + + /** + * 最大使用次数 + */ + private Integer maxUseCount; + + /** + * 每个用户最大使用次数 + */ + private Integer maxUsePerUser; + + /** + * 使用间隔小时数 + */ + private Integer useIntervalHours; + + /** + * 剩余可使用次数 + */ + private Integer remainingUseCount; + + /** + * 最后使用时间 + */ + private Date lastUsedTime; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReqV2.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReqV2.java new file mode 100644 index 00000000..42a05fe6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReqV2.java @@ -0,0 +1,73 @@ +package com.ycwl.basic.pricing.dto.req; + +import com.ycwl.basic.pricing.enums.ProductType; +import lombok.Data; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.util.List; + +/** + * 券码批次创建请求(支持可重复使用) + */ +@Data +public class VoucherBatchCreateReqV2 { + + /** + * 券码批次名称 + */ + @NotBlank(message = "券码批次名称不能为空") + private String batchName; + + /** + * 景区ID + */ + @NotNull(message = "景区ID不能为空") + private Long scenicId; + + /** + * 推客ID + */ + @NotNull(message = "推客ID不能为空") + private Long brokerId; + + /** + * 优惠类型:0=全场免费,1=商品降价,2=商品打折 + */ + @NotNull(message = "优惠类型不能为空") + private Integer discountType; + + /** + * 优惠值(降价金额或折扣百分比) + */ + private BigDecimal discountValue; + + /** + * 适用商品类型列表 + */ + private List applicableProducts; + + /** + * 券码总数量 + */ + @NotNull(message = "券码数量不能为空") + @Min(value = 1, message = "券码数量必须大于0") + private Integer totalCount; + + /** + * 每个券码最大使用次数(NULL表示无限次,1表示单次使用) + */ + private Integer maxUseCount; + + /** + * 每个用户最大使用次数(NULL表示无限次) + */ + private Integer maxUsePerUser; + + /** + * 两次使用间隔小时数(NULL表示无间隔限制) + */ + private Integer useIntervalHours; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherUsageHistoryReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherUsageHistoryReq.java new file mode 100644 index 00000000..70c187e9 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherUsageHistoryReq.java @@ -0,0 +1,55 @@ +package com.ycwl.basic.pricing.dto.req; + +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * 券码使用历史查询请求 + */ +@Data +public class VoucherUsageHistoryReq { + + /** + * 券码 + */ + private String voucherCode; + + /** + * 用户faceId + */ + private Long faceId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 批次ID + */ + private Long batchId; + + /** + * 开始时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + /** + * 结束时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + /** + * 页码 + */ + private Integer pageNum = 1; + + /** + * 页面大小 + */ + private Integer pageSize = 10; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageRecordResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageRecordResp.java new file mode 100644 index 00000000..59b75a90 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageRecordResp.java @@ -0,0 +1,76 @@ +package com.ycwl.basic.pricing.dto.resp; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 券码使用记录响应 + */ +@Data +public class VoucherUsageRecordResp { + + /** + * 记录ID + */ + private Long id; + + /** + * 券码ID + */ + private Long voucherCodeId; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 使用用户faceId + */ + private Long faceId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 批次ID + */ + private Long batchId; + + /** + * 批次名称 + */ + private String batchName; + + /** + * 使用时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date useTime; + + /** + * 关联订单ID + */ + private String orderId; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 使用备注 + */ + private String remark; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageStatsResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageStatsResp.java new file mode 100644 index 00000000..4275449c --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageStatsResp.java @@ -0,0 +1,85 @@ +package com.ycwl.basic.pricing.dto.resp; + +import lombok.Data; + +/** + * 券码使用统计响应 + */ +@Data +public class VoucherUsageStatsResp { + + /** + * 券码ID + */ + private Long voucherCodeId; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 批次ID + */ + private Long batchId; + + /** + * 批次名称 + */ + private String batchName; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 券码状态 + */ + private Integer status; + + /** + * 状态名称 + */ + private String statusName; + + /** + * 当前使用次数 + */ + private Integer currentUseCount; + + /** + * 最大使用次数 + */ + private Integer maxUseCount; + + /** + * 每个用户最大使用次数 + */ + private Integer maxUsePerUser; + + /** + * 使用间隔小时数 + */ + private Integer useIntervalHours; + + /** + * 是否还可以使用 + */ + private Boolean canUseMore; + + /** + * 剩余可使用次数 + */ + private Integer remainingUseCount; + + /** + * 总使用记录数 + */ + private Integer totalUsageRecords; + + /** + * 最后使用时间 + */ + private String lastUsedTime; +} \ 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 2539e9b3..99c74f47 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java @@ -96,6 +96,21 @@ public class PriceVoucherBatchConfig { private Long updateBy; + /** + * 每个券码最大使用次数(NULL表示无限次,1表示单次使用兼容原有逻辑) + */ + private Integer maxUseCount; + + /** + * 每个用户最大使用次数(NULL表示无限次) + */ + private Integer maxUsePerUser; + + /** + * 两次使用间隔小时数(NULL表示无间隔限制) + */ + private Integer useIntervalHours; + private Integer deleted; private Date deletedAt; 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 1b59dc2e..f2288cae 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherCode.java @@ -58,6 +58,16 @@ public class PriceVoucherCode { */ private String remark; + /** + * 当前使用次数 + */ + private Integer currentUseCount; + + /** + * 最后使用时间 + */ + private Date lastUsedTime; + @TableField("create_time") private Date createTime; diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherUsageRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherUsageRecord.java new file mode 100644 index 00000000..b8c8c937 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherUsageRecord.java @@ -0,0 +1,76 @@ +package com.ycwl.basic.pricing.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 券码使用记录实体 + */ +@Data +@TableName("price_voucher_usage_record") +public class PriceVoucherUsageRecord { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 券码ID + */ + private Long voucherCodeId; + + /** + * 券码 + */ + private String voucherCode; + + /** + * 使用用户faceId + */ + private Long faceId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 批次ID + */ + private Long batchId; + + /** + * 使用时间 + */ + private Date useTime; + + /** + * 关联订单ID + */ + private String orderId; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 使用备注 + */ + 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/enums/VoucherCodeStatus.java b/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java index b4651bbf..73a0105f 100644 --- a/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java +++ b/src/main/java/com/ycwl/basic/pricing/enums/VoucherCodeStatus.java @@ -16,14 +16,29 @@ public enum VoucherCodeStatus { UNCLAIMED(0, "未领取"), /** - * 已领取未使用 + * 已领取可使用(可重复使用状态,替代原CLAIMED_UNUSED) + */ + CLAIMED_AVAILABLE(1, "已领取可使用"), + + /** + * 已领取未使用(兼容原有逻辑,等同于CLAIMED_AVAILABLE) */ CLAIMED_UNUSED(1, "已领取未使用"), /** - * 已使用 + * 已使用(兼容原有逻辑,当达到最大使用次数时为此状态) */ - USED(2, "已使用"); + USED(2, "已使用"), + + /** + * 已领取已用完(达到最大使用次数) + */ + CLAIMED_EXHAUSTED(3, "已领取已用完"), + + /** + * 已过期 + */ + EXPIRED(4, "已过期"); private final Integer code; private final String name; @@ -55,20 +70,38 @@ public enum VoucherCodeStatus { } /** - * 检查是否可以使用(已领取未使用状态) + * 检查是否可以使用(已领取可使用状态) * @param code 状态代码 * @return 是否可以使用 */ public static boolean canUse(Integer code) { - return CLAIMED_UNUSED.getCode().equals(code); + return CLAIMED_AVAILABLE.getCode().equals(code) || CLAIMED_UNUSED.getCode().equals(code); } /** - * 检查是否已使用 + * 检查是否已完全使用完(已使用或已用完状态) + * @param code 状态代码 + * @return 是否已完全使用完 + */ + public static boolean isExhausted(Integer code) { + return USED.getCode().equals(code) || CLAIMED_EXHAUSTED.getCode().equals(code); + } + + /** + * 检查是否为已使用状态(兼容原有逻辑) * @param code 状态代码 * @return 是否已使用 */ public static boolean isUsed(Integer code) { return USED.getCode().equals(code); } + + /** + * 检查是否已过期 + * @param code 状态代码 + * @return 是否已过期 + */ + public static boolean isExpired(Integer code) { + return EXPIRED.getCode().equals(code); + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java new file mode 100644 index 00000000..324d5a35 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java @@ -0,0 +1,121 @@ +package com.ycwl.basic.pricing.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.pricing.entity.PriceVoucherUsageRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; + +/** + * 券码使用记录Mapper + */ +@Mapper +public interface PriceVoucherUsageRecordMapper extends BaseMapper { + + /** + * 根据券码ID查询使用记录 + * + * @param voucherCodeId 券码ID + * @return 使用记录列表 + */ + @Select("SELECT * FROM price_voucher_usage_record WHERE voucher_code_id = #{voucherCodeId} AND deleted = 0 ORDER BY use_time DESC") + List selectByVoucherCodeId(@Param("voucherCodeId") Long voucherCodeId); + + /** + * 根据券码查询使用记录 + * + * @param voucherCode 券码 + * @return 使用记录列表 + */ + @Select("SELECT * FROM price_voucher_usage_record WHERE voucher_code = #{voucherCode} AND deleted = 0 ORDER BY use_time DESC") + List selectByVoucherCode(@Param("voucherCode") String voucherCode); + + /** + * 根据用户和景区查询使用记录 + * + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 使用记录列表 + */ + @Select("SELECT * FROM price_voucher_usage_record WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND deleted = 0 ORDER BY use_time DESC") + List selectByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId); + + /** + * 统计用户在指定券码上的使用次数 + * + * @param faceId 用户faceId + * @param voucherCodeId 券码ID + * @return 使用次数 + */ + @Select("SELECT COUNT(*) FROM price_voucher_usage_record WHERE face_id = #{faceId} AND voucher_code_id = #{voucherCodeId} AND deleted = 0") + Integer countByFaceIdAndVoucherCodeId(@Param("faceId") Long faceId, @Param("voucherCodeId") Long voucherCodeId); + + /** + * 统计指定时间段内用户的使用次数 + * + * @param faceId 用户faceId + * @param voucherCodeId 券码ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 使用次数 + */ + @Select("SELECT COUNT(*) FROM price_voucher_usage_record WHERE face_id = #{faceId} AND voucher_code_id = #{voucherCodeId} " + + "AND use_time BETWEEN #{startTime} AND #{endTime} AND deleted = 0") + Integer countByFaceIdAndVoucherCodeIdAndTimeRange(@Param("faceId") Long faceId, + @Param("voucherCodeId") Long voucherCodeId, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * 获取用户最后一次使用该券码的时间 + * + * @param faceId 用户faceId + * @param voucherCodeId 券码ID + * @return 最后使用时间 + */ + @Select("SELECT MAX(use_time) FROM price_voucher_usage_record WHERE face_id = #{faceId} AND voucher_code_id = #{voucherCodeId} AND deleted = 0") + Date getLastUseTimeByFaceIdAndVoucherCodeId(@Param("faceId") Long faceId, @Param("voucherCodeId") Long voucherCodeId); + + /** + * 根据批次ID统计使用记录数量 + * + * @param batchId 批次ID + * @return 使用记录数量 + */ + @Select("SELECT COUNT(*) FROM price_voucher_usage_record WHERE batch_id = #{batchId} AND deleted = 0") + Integer countByBatchId(@Param("batchId") Long batchId); + + /** + * 分页查询券码使用记录 + * + * @param page 分页参数 + * @param batchId 批次ID(可选) + * @param voucherCode 券码(可选) + * @param faceId 用户faceId(可选) + * @param scenicId 景区ID(可选) + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @return 分页结果 + */ + @Select("") + Page selectPageWithConditions(Page page, + @Param("batchId") Long batchId, + @Param("voucherCode") String voucherCode, + @Param("faceId") Long faceId, + @Param("scenicId") Long scenicId, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IVoucherUsageService.java b/src/main/java/com/ycwl/basic/pricing/service/IVoucherUsageService.java new file mode 100644 index 00000000..562dd3c9 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IVoucherUsageService.java @@ -0,0 +1,67 @@ +package com.ycwl.basic.pricing.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp; +import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp; + +import java.util.List; + +/** + * 券码使用记录服务接口 + */ +public interface IVoucherUsageService { + + /** + * 分页查询券码使用记录 + * + * @param req 查询请求 + * @return 分页结果 + */ + Page getUsageHistory(VoucherUsageHistoryReq req); + + /** + * 获取指定券码的使用记录 + * + * @param voucherCode 券码 + * @return 使用记录列表 + */ + List getUsageRecordsByCode(String voucherCode); + + /** + * 获取用户在指定景区的券码使用记录 + * + * @param faceId 用户faceId + * @param scenicId 景区ID + * @return 使用记录列表 + */ + List getUserUsageRecords(Long faceId, Long scenicId); + + /** + * 获取券码使用统计信息 + * + * @param voucherCode 券码 + * @return 统计信息 + */ + VoucherUsageStatsResp getUsageStats(String voucherCode); + + /** + * 获取批次券码使用统计信息 + * + * @param batchId 批次ID + * @return 统计信息列表 + */ + List getBatchUsageStats(Long batchId); + + /** + * 记录券码使用(内部方法,由VoucherService调用) + * + * @param voucherCode 券码 + * @param faceId 用户faceId + * @param orderId 订单ID + * @param discountAmount 优惠金额 + * @param remark 备注 + */ + void recordVoucherUsage(String voucherCode, Long faceId, String orderId, + java.math.BigDecimal discountAmount, String remark); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java index f54d9be0..a34aded2 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherBatchService.java @@ -2,6 +2,7 @@ package com.ycwl.basic.pricing.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2; import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp; import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp; @@ -11,6 +12,11 @@ public interface VoucherBatchService { Long createBatch(VoucherBatchCreateReq req); + /** + * 创建券码批次(支持可重复使用) + */ + Long createBatchV2(VoucherBatchCreateReqV2 req); + Page queryBatchList(VoucherBatchQueryReq req); VoucherBatchResp getBatchDetail(Long id); 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 8ce020e0..383dc2c8 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 @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.exception.BizException; import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq; +import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2; import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq; import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp; import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp; @@ -78,6 +79,72 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { return batch.getId(); } + @Override + @Transactional + public Long createBatchV2(VoucherBatchCreateReqV2 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, "优惠金额不能为空"); + } + + // 验证可重复使用参数 + if (req.getMaxUseCount() != null && req.getMaxUseCount() < 1) { + throw new BizException(400, "最大使用次数必须大于0"); + } + if (req.getMaxUsePerUser() != null && req.getMaxUsePerUser() < 1) { + throw new BizException(400, "每用户最大使用次数必须大于0"); + } + if (req.getUseIntervalHours() != null && req.getUseIntervalHours() < 0) { + throw new BizException(400, "使用间隔小时数不能为负数"); + } + + PriceVoucherBatchConfig batch = new PriceVoucherBatchConfig(); + BeanUtils.copyProperties(req, batch); + + // 设置适用商品类型 + if (req.getApplicableProducts() != null) { + batch.setApplicableProducts(req.getApplicableProducts()); + } + + batch.setUsedCount(0); + batch.setClaimedCount(0); + batch.setStatus(1); + batch.setCreateTime(new Date()); + + String userIdStr = BaseContextHandler.getUserId(); + if (userIdStr != null) { + batch.setCreateBy(Long.valueOf(userIdStr)); + } + batch.setDeleted(0); + + voucherBatchMapper.insert(batch); + + // 生成券码,初始状态为UNCLAIMED,currentUseCount为0 + 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()); 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 abf043c0..fb1a205f 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 @@ -10,6 +10,8 @@ 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.mapper.PriceVoucherUsageRecordMapper; +import com.ycwl.basic.pricing.entity.PriceVoucherUsageRecord; import com.ycwl.basic.pricing.service.IVoucherService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,6 +21,8 @@ import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; +import java.util.Date; +import java.util.concurrent.TimeUnit; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -34,6 +38,7 @@ public class VoucherServiceImpl implements IVoucherService { private final PriceVoucherCodeMapper voucherCodeMapper; private final PriceVoucherBatchConfigMapper voucherBatchConfigMapper; + private final PriceVoucherUsageRecordMapper usageRecordMapper; @Override public VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId) { @@ -63,24 +68,38 @@ public class VoucherServiceImpl implements IVoucherService { // 检查券码状态和可用性 if (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) { // 未领取状态,检查是否可以领取 - if (faceId != null) { // && canClaimVoucher(faceId, voucherCodeEntity.getScenicId()) + if (faceId != null) { voucherInfo.setAvailable(true); } else { voucherInfo.setAvailable(false); voucherInfo.setUnavailableReason("您已在该景区领取过券码"); } - } else if (VoucherCodeStatus.CLAIMED_UNUSED.getCode().equals(voucherCodeEntity.getStatus())) { - // 已领取未使用,检查是否为当前用户 + } else if (VoucherCodeStatus.canUse(voucherCodeEntity.getStatus())) { + // 已领取可使用状态,检查是否为当前用户且未达到使用限制 if (faceId != null && faceId.equals(voucherCodeEntity.getFaceId())) { - voucherInfo.setAvailable(true); + String availabilityCheck = checkVoucherAvailability(voucherCodeEntity, batchConfig, faceId); + if (availabilityCheck == null) { + voucherInfo.setAvailable(true); + } else { + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason(availabilityCheck); + } } else { voucherInfo.setAvailable(false); voucherInfo.setUnavailableReason("券码已被其他用户领取"); } - } else { - // 已使用 + } else if (VoucherCodeStatus.isExhausted(voucherCodeEntity.getStatus())) { + // 已用完或已使用 voucherInfo.setAvailable(false); - voucherInfo.setUnavailableReason("券码已使用"); + voucherInfo.setUnavailableReason("券码已用完"); + } else if (VoucherCodeStatus.isExpired(voucherCodeEntity.getStatus())) { + // 已过期 + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码已过期"); + } else { + // 其他状态 + voucherInfo.setAvailable(false); + voucherInfo.setUnavailableReason("券码状态异常"); } return voucherInfo; @@ -109,19 +128,78 @@ public class VoucherServiceImpl implements IVoucherService { @Override public void markVoucherAsUsed(String voucherCode, String remark) { + markVoucherAsUsed(voucherCode, remark, null, null); + } + + /** + * 标记券码为已使用(支持可重复使用) + * + * @param voucherCode 券码 + * @param remark 使用备注 + * @param orderId 订单ID + * @param discountAmount 优惠金额 + */ + public void markVoucherAsUsed(String voucherCode, String remark, String orderId, BigDecimal discountAmount) { 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); + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) { + log.warn("券码不存在或已删除: {}", voucherCode); + return; } + + PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId()); + if (batchConfig == null || batchConfig.getDeleted() == 1) { + log.warn("券码批次不存在或已删除: batchId={}", voucherCodeEntity.getBatchId()); + return; + } + + Date now = new Date(); + + // 创建使用记录 + PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord(); + usageRecord.setVoucherCodeId(voucherCodeEntity.getId()); + usageRecord.setVoucherCode(voucherCode); + usageRecord.setFaceId(voucherCodeEntity.getFaceId()); + usageRecord.setScenicId(voucherCodeEntity.getScenicId()); + usageRecord.setBatchId(voucherCodeEntity.getBatchId()); + usageRecord.setUseTime(now); + usageRecord.setOrderId(orderId); + usageRecord.setDiscountAmount(discountAmount); + usageRecord.setRemark(remark); + usageRecord.setCreateTime(now); + usageRecord.setDeleted(0); + + usageRecordMapper.insert(usageRecord); + + // 更新券码使用次数和状态 + Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ? + voucherCodeEntity.getCurrentUseCount() + 1 : 1; + voucherCodeEntity.setCurrentUseCount(currentUseCount); + voucherCodeEntity.setLastUsedTime(now); + + // 检查是否达到最大使用次数 + Integer maxUseCount = batchConfig.getMaxUseCount(); + if (maxUseCount != null && currentUseCount >= maxUseCount) { + if (maxUseCount == 1) { + // 兼容原有逻辑,单次使用设为USED状态 + voucherCodeEntity.setStatus(VoucherCodeStatus.USED.getCode()); + voucherCodeEntity.setUsedTime(now); + } else { + // 多次使用达到上限设为CLAIMED_EXHAUSTED状态 + voucherCodeEntity.setStatus(VoucherCodeStatus.CLAIMED_EXHAUSTED.getCode()); + } + } + + voucherCodeMapper.updateById(voucherCodeEntity); + + // 更新批次统计(使用记录数,不是使用券码数) + voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1); + + log.info("券码使用记录已创建: code={}, useCount={}, maxUseCount={}", + voucherCode, currentUseCount, maxUseCount); } @Override @@ -260,6 +338,48 @@ public class VoucherServiceImpl implements IVoucherService { return bestVoucher; } + /** + * 检查券码可用性 + * + * @param voucherCode 券码实体 + * @param batchConfig 批次配置 + * @param faceId 用户faceId + * @return 不可用原因,null表示可用 + */ + private String checkVoucherAvailability(PriceVoucherCode voucherCode, PriceVoucherBatchConfig batchConfig, Long faceId) { + // 1. 检查券码使用次数限制 + Integer maxUseCount = batchConfig.getMaxUseCount(); + Integer currentUseCount = voucherCode.getCurrentUseCount() != null ? voucherCode.getCurrentUseCount() : 0; + + if (maxUseCount != null && currentUseCount >= maxUseCount) { + return "券码使用次数已达上限"; + } + + // 2. 检查用户使用次数限制 + Integer maxUsePerUser = batchConfig.getMaxUsePerUser(); + if (maxUsePerUser != null && faceId != null) { + Integer userUseCount = usageRecordMapper.countByFaceIdAndVoucherCodeId(faceId, voucherCode.getId()); + if (userUseCount >= maxUsePerUser) { + return "您使用该券码的次数已达上限"; + } + } + + // 3. 检查使用间隔时间限制 + Integer useIntervalHours = batchConfig.getUseIntervalHours(); + if (useIntervalHours != null && faceId != null) { + Date lastUseTime = usageRecordMapper.getLastUseTimeByFaceIdAndVoucherCodeId(faceId, voucherCode.getId()); + if (lastUseTime != null) { + long diffMillis = System.currentTimeMillis() - lastUseTime.getTime(); + long diffHours = TimeUnit.MILLISECONDS.toHours(diffMillis); + if (diffHours < useIntervalHours) { + return String.format("请等待%d小时后再次使用该券码", useIntervalHours - diffHours); + } + } + } + + return null; // 可用 + } + /** * 构建券码信息DTO */ @@ -278,6 +398,22 @@ public class VoucherServiceImpl implements IVoucherService { voucherInfo.setUsedTime(voucherCode.getUsedTime()); voucherInfo.setApplicableProducts(batchConfig.getApplicableProducts()); + // 设置可重复使用相关信息 + Integer currentUseCount = voucherCode.getCurrentUseCount() != null ? voucherCode.getCurrentUseCount() : 0; + voucherInfo.setCurrentUseCount(currentUseCount); + voucherInfo.setMaxUseCount(batchConfig.getMaxUseCount()); + voucherInfo.setMaxUsePerUser(batchConfig.getMaxUsePerUser()); + voucherInfo.setUseIntervalHours(batchConfig.getUseIntervalHours()); + voucherInfo.setLastUsedTime(voucherCode.getLastUsedTime()); + + // 计算剩余可使用次数 + if (batchConfig.getMaxUseCount() != null) { + int remaining = batchConfig.getMaxUseCount() - currentUseCount; + voucherInfo.setRemainingUseCount(Math.max(0, remaining)); + } else { + voucherInfo.setRemainingUseCount(null); // 无限次 + } + return voucherInfo; } diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherUsageServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherUsageServiceImpl.java new file mode 100644 index 00000000..cfeae1c3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherUsageServiceImpl.java @@ -0,0 +1,192 @@ +package com.ycwl.basic.pricing.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq; +import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp; +import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp; +import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig; +import com.ycwl.basic.pricing.entity.PriceVoucherCode; +import com.ycwl.basic.pricing.entity.PriceVoucherUsageRecord; +import com.ycwl.basic.pricing.enums.VoucherCodeStatus; +import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper; +import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper; +import com.ycwl.basic.pricing.mapper.PriceVoucherUsageRecordMapper; +import com.ycwl.basic.pricing.service.IVoucherUsageService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 券码使用记录服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherUsageServiceImpl implements IVoucherUsageService { + + private final PriceVoucherUsageRecordMapper usageRecordMapper; + private final PriceVoucherCodeMapper voucherCodeMapper; + private final PriceVoucherBatchConfigMapper batchConfigMapper; + + @Override + public Page getUsageHistory(VoucherUsageHistoryReq req) { + Page page = new Page<>(req.getPageNum(), req.getPageSize()); + + Page entityPage = usageRecordMapper.selectPageWithConditions( + page, req.getBatchId(), req.getVoucherCode(), req.getFaceId(), + req.getScenicId(), req.getStartTime(), req.getEndTime()); + + Page respPage = new Page<>(); + BeanUtils.copyProperties(entityPage, respPage); + + List respList = new ArrayList<>(); + for (PriceVoucherUsageRecord record : entityPage.getRecords()) { + VoucherUsageRecordResp resp = convertToResp(record); + respList.add(resp); + } + respPage.setRecords(respList); + + return respPage; + } + + @Override + public List getUsageRecordsByCode(String voucherCode) { + List records = usageRecordMapper.selectByVoucherCode(voucherCode); + List respList = new ArrayList<>(); + + for (PriceVoucherUsageRecord record : records) { + respList.add(convertToResp(record)); + } + + return respList; + } + + @Override + public List getUserUsageRecords(Long faceId, Long scenicId) { + List records = usageRecordMapper.selectByFaceIdAndScenicId(faceId, scenicId); + List respList = new ArrayList<>(); + + for (PriceVoucherUsageRecord record : records) { + respList.add(convertToResp(record)); + } + + return respList; + } + + @Override + public VoucherUsageStatsResp getUsageStats(String voucherCode) { + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) { + return null; + } + + PriceVoucherBatchConfig batchConfig = batchConfigMapper.selectById(voucherCodeEntity.getBatchId()); + if (batchConfig == null || batchConfig.getDeleted() == 1) { + return null; + } + + VoucherUsageStatsResp stats = new VoucherUsageStatsResp(); + stats.setVoucherCodeId(voucherCodeEntity.getId()); + stats.setVoucherCode(voucherCodeEntity.getCode()); + stats.setBatchId(batchConfig.getId()); + stats.setBatchName(batchConfig.getBatchName()); + stats.setScenicId(voucherCodeEntity.getScenicId()); + stats.setStatus(voucherCodeEntity.getStatus()); + + VoucherCodeStatus statusEnum = VoucherCodeStatus.getByCode(voucherCodeEntity.getStatus()); + if (statusEnum != null) { + stats.setStatusName(statusEnum.getName()); + } + + Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ? + voucherCodeEntity.getCurrentUseCount() : 0; + stats.setCurrentUseCount(currentUseCount); + stats.setMaxUseCount(batchConfig.getMaxUseCount()); + stats.setMaxUsePerUser(batchConfig.getMaxUsePerUser()); + stats.setUseIntervalHours(batchConfig.getUseIntervalHours()); + + // 计算是否还能使用 + boolean canUseMore = true; + if (batchConfig.getMaxUseCount() != null) { + canUseMore = currentUseCount < batchConfig.getMaxUseCount(); + } + stats.setCanUseMore(canUseMore); + + // 计算剩余使用次数 + if (batchConfig.getMaxUseCount() != null) { + int remaining = batchConfig.getMaxUseCount() - currentUseCount; + stats.setRemainingUseCount(Math.max(0, remaining)); + } + + // 获取使用记录数 + Integer totalUsageRecords = usageRecordMapper.selectByVoucherCodeId(voucherCodeEntity.getId()).size(); + stats.setTotalUsageRecords(totalUsageRecords); + + // 格式化最后使用时间 + if (voucherCodeEntity.getLastUsedTime() != null) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + stats.setLastUsedTime(sdf.format(voucherCodeEntity.getLastUsedTime())); + } + + return stats; + } + + @Override + public List getBatchUsageStats(Long batchId) { + // 这里可以实现批次统计,暂时返回空列表 + return new ArrayList<>(); + } + + @Override + public void recordVoucherUsage(String voucherCode, Long faceId, String orderId, + BigDecimal discountAmount, String remark) { + PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode); + if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) { + log.warn("券码不存在或已删除: {}", voucherCode); + return; + } + + Date now = new Date(); + PriceVoucherUsageRecord record = new PriceVoucherUsageRecord(); + record.setVoucherCodeId(voucherCodeEntity.getId()); + record.setVoucherCode(voucherCode); + record.setFaceId(faceId); + record.setScenicId(voucherCodeEntity.getScenicId()); + record.setBatchId(voucherCodeEntity.getBatchId()); + record.setUseTime(now); + record.setOrderId(orderId); + record.setDiscountAmount(discountAmount); + record.setRemark(remark); + record.setCreateTime(now); + record.setDeleted(0); + + usageRecordMapper.insert(record); + log.info("券码使用记录已创建: voucherCode={}, faceId={}, orderId={}", + voucherCode, faceId, orderId); + } + + /** + * 转换为响应DTO + */ + private VoucherUsageRecordResp convertToResp(PriceVoucherUsageRecord record) { + VoucherUsageRecordResp resp = new VoucherUsageRecordResp(); + BeanUtils.copyProperties(record, resp); + + // 查询批次名称 + if (record.getBatchId() != null) { + PriceVoucherBatchConfig batchConfig = batchConfigMapper.selectById(record.getBatchId()); + if (batchConfig != null) { + resp.setBatchName(batchConfig.getBatchName()); + } + } + + return resp; + } +} \ No newline at end of file From 7cfcc445318e15d594d07b58b80a11ff306821a3 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 16 Sep 2025 16:43:07 +0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor(pricing):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=88=B8=E7=A0=81=E4=BD=BF=E7=94=A8=E8=AE=B0=E5=BD=95=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=8E=A5=E5=8F=A3=E5=92=8C=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了 VoucherUsageController 中的 Swagger 注解 - 更新了 PriceVoucherUsageRecordMapper 中的 SQL 查询 - 新增了 PriceVoucherUsageRecordMapper.xml 文件,用于定义分页查询 --- .../controller/VoucherUsageController.java | 33 +++++++------------ .../mapper/PriceVoucherUsageRecordMapper.java | 14 ++++---- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/controller/VoucherUsageController.java b/src/main/java/com/ycwl/basic/pricing/controller/VoucherUsageController.java index b4836415..0c4f47cd 100644 --- a/src/main/java/com/ycwl/basic/pricing/controller/VoucherUsageController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/VoucherUsageController.java @@ -1,14 +1,11 @@ package com.ycwl.basic.pricing.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.ycwl.basic.common.ApiResponse; +import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq; import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp; import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp; import com.ycwl.basic.pricing.service.IVoucherUsageService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -22,13 +19,11 @@ import java.util.List; @RestController @RequestMapping("/api/pricing/voucher/usage") @RequiredArgsConstructor -@Tag(name = "券码使用记录管理", description = "券码使用历史和统计API") public class VoucherUsageController { - + private final IVoucherUsageService voucherUsageService; - + @PostMapping("/history") - @Operation(summary = "分页查询券码使用记录") public ApiResponse> getUsageHistory(@RequestBody VoucherUsageHistoryReq req) { try { Page result = voucherUsageService.getUsageHistory(req); @@ -38,11 +33,10 @@ public class VoucherUsageController { return ApiResponse.fail("查询失败: " + e.getMessage()); } } - + @GetMapping("/{voucherCode}/records") - @Operation(summary = "获取指定券码的使用记录") public ApiResponse> getRecordsByCode( - @Parameter(description = "券码") @PathVariable String voucherCode) { + @PathVariable String voucherCode) { try { List records = voucherUsageService.getUsageRecordsByCode(voucherCode); return ApiResponse.success(records); @@ -51,11 +45,10 @@ public class VoucherUsageController { return ApiResponse.fail("查询失败: " + e.getMessage()); } } - + @GetMapping("/{voucherCode}/stats") - @Operation(summary = "获取券码使用统计信息") public ApiResponse getUsageStats( - @Parameter(description = "券码") @PathVariable String voucherCode) { + @PathVariable String voucherCode) { try { VoucherUsageStatsResp stats = voucherUsageService.getUsageStats(voucherCode); if (stats == null) { @@ -67,12 +60,11 @@ public class VoucherUsageController { return ApiResponse.fail("查询失败: " + e.getMessage()); } } - + @GetMapping("/user/{faceId}/scenic/{scenicId}") - @Operation(summary = "获取用户在指定景区的券码使用记录") public ApiResponse> getUserUsageRecords( - @Parameter(description = "用户faceId") @PathVariable Long faceId, - @Parameter(description = "景区ID") @PathVariable Long scenicId) { + @PathVariable Long faceId, + @PathVariable Long scenicId) { try { List records = voucherUsageService.getUserUsageRecords(faceId, scenicId); return ApiResponse.success(records); @@ -81,11 +73,10 @@ public class VoucherUsageController { return ApiResponse.fail("查询失败: " + e.getMessage()); } } - + @GetMapping("/batch/{batchId}/stats") - @Operation(summary = "获取批次券码使用统计信息") public ApiResponse> getBatchUsageStats( - @Parameter(description = "批次ID") @PathVariable Long batchId) { + @PathVariable Long batchId) { try { List statsList = voucherUsageService.getBatchUsageStats(batchId); return ApiResponse.success(statsList); diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java index 324d5a35..4b9c8744 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java @@ -102,13 +102,13 @@ public interface PriceVoucherUsageRecordMapper extends BaseMapper" + - "SELECT * FROM price_voucher_usage_record WHERE deleted = 0 " + - "AND batch_id = #{batchId} " + - "AND voucher_code LIKE CONCAT('%', #{voucherCode}, '%') " + - "AND face_id = #{faceId} " + - "AND scenic_id = #{scenicId} " + - "AND use_time >= #{startTime} " + - "AND use_time <= #{endTime} " + + "SELECT * FROM price_voucher_usage_record WHERE deleted = 0" + + "AND batch_id = #{batchId}" + + " 0\">AND voucher_code LIKE CONCAT('%', #{voucherCode}, '%')" + + "AND face_id = #{faceId}" + + "AND scenic_id = #{scenicId}" + + "AND use_time >= #{startTime}" + + "AND use_time <= #{endTime}" + "ORDER BY use_time DESC" + "") Page selectPageWithConditions(Page page, From 57266eb535105804de5fe60828c367064f04ecb2 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 16 Sep 2025 17:21:30 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor(order):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=88=9B=E5=BB=BA=E5=92=8C=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改订单服务中的券码使用逻辑,增加人脸 ID 参数 - 优化价格计算控制器和服务中的预览模式 - 更新券码使用记录,支持人脸 ID 记录 - 修复零金额订单的处理逻辑 - 优化日志输出级别和内容 --- .../order/service/impl/OrderServiceImpl.java | 2 +- .../controller/PriceCalculationController.java | 2 +- .../dto/MobilePriceCalculationRequest.java | 2 +- .../basic/pricing/service/IVoucherService.java | 8 +++++--- .../impl/PriceCalculationServiceImpl.java | 2 +- .../service/impl/VoucherDiscountProvider.java | 2 +- .../service/impl/VoucherServiceImpl.java | 18 +++++++++--------- .../service/pc/impl/OrderServiceImpl.java | 15 +++++++-------- src/main/resources/mapper/OrderMapper.xml | 1 + 9 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java b/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java index fa14b0af..11f83a7f 100644 --- a/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java +++ b/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java @@ -193,7 +193,7 @@ public class OrderServiceImpl implements IOrderService { // 标记券码为已使用 try { String remark = "订单使用,订单号:" + orderNo; - voucherService.markVoucherAsUsed(voucherInfo.getVoucherCode(), remark); + voucherService.markVoucherAsUsed(voucherInfo.getVoucherCode(), remark, String.valueOf(orderId), order.getDiscountAmount(), order.getFaceId()); log.info("券码状态更新成功: voucherCode={}, orderId={}", voucherInfo.getVoucherCode(), orderNo); } catch (Exception e) { diff --git a/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java b/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java index 8b4cd2eb..279d6b62 100644 --- a/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java +++ b/src/main/java/com/ycwl/basic/pricing/controller/PriceCalculationController.java @@ -28,7 +28,7 @@ public class PriceCalculationController { @PostMapping("/calculate") public ApiResponse calculatePrice(@RequestBody PriceCalculationRequest request) { log.info("价格计算请求: userId={}, products={}", request.getUserId(), request.getProducts().size()); - + request.setPreviewOnly(true); PriceCalculationResult result = priceCalculationService.calculatePrice(request); log.info("价格计算完成: originalAmount={}, finalAmount={}, usedCoupon={}", diff --git a/src/main/java/com/ycwl/basic/pricing/dto/MobilePriceCalculationRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/MobilePriceCalculationRequest.java index d7e49dd7..7d8642ed 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/MobilePriceCalculationRequest.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/MobilePriceCalculationRequest.java @@ -56,7 +56,7 @@ public class MobilePriceCalculationRequest { request.setAutoUseCoupon(this.autoUseCoupon); request.setVoucherCode(this.voucherCode); request.setAutoUseVoucher(this.autoUseVoucher); - request.setPreviewOnly(this.previewOnly); + request.setPreviewOnly(true); return request; } } \ 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 index ef0f4aa4..4f05658c 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java @@ -27,14 +27,16 @@ public interface IVoucherService { * @return 可用券码列表 */ List getAvailableVouchers(Long faceId, Long scenicId); - /** * 标记券码为已使用 * @param voucherCode 券码 * @param remark 使用备注 + * @param faceId 人脸ID */ - void markVoucherAsUsed(String voucherCode, String remark); - + void markVoucherAsUsed(String voucherCode, String remark, Long faceId); + + void markVoucherAsUsed(String voucherCode, String remark, String orderId, BigDecimal discountAmount, Long faceId); + /** * 检查用户是否可以在指定景区领取券码 * @param faceId 用户faceId 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 9f64f6e7..cbfee913 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 @@ -387,7 +387,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { // 标记券码为已使用 if (result.getUsedVoucher() != null && result.getUsedVoucher().getVoucherCode() != null) { String remark = String.format("价格计算使用 - 订单金额: %s", result.getFinalAmount()); - voucherService.markVoucherAsUsed(result.getUsedVoucher().getVoucherCode(), remark); + voucherService.markVoucherAsUsed(result.getUsedVoucher().getVoucherCode(), remark, request.getFaceId()); log.info("已标记券码为使用: {}", result.getUsedVoucher().getVoucherCode()); } diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java index 250713ba..1e99002b 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherDiscountProvider.java @@ -217,7 +217,7 @@ public class VoucherDiscountProvider implements IDiscountProvider { if (productConfig != null) { if (!Boolean.TRUE.equals(productConfig.getCanUseVoucher())) { - log.debug("商品配置不允许使用券码: productType={}, productId={}", + log.info("商品配置不允许使用券码: productType={}, productId={}", product.getProductType().getCode(), productId); return false; } 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 fb1a205f..98427849 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 @@ -20,11 +20,9 @@ import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.math.RoundingMode; -import java.time.LocalDateTime; import java.util.Date; import java.util.concurrent.TimeUnit; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -125,10 +123,10 @@ public class VoucherServiceImpl implements IVoucherService { return voucherInfos; } - + @Override - public void markVoucherAsUsed(String voucherCode, String remark) { - markVoucherAsUsed(voucherCode, remark, null, null); + public void markVoucherAsUsed(String voucherCode, String remark, Long faceId) { + markVoucherAsUsed(voucherCode, remark, null, null, faceId); } /** @@ -138,8 +136,10 @@ public class VoucherServiceImpl implements IVoucherService { * @param remark 使用备注 * @param orderId 订单ID * @param discountAmount 优惠金额 + * @param faceId 人脸ID */ - public void markVoucherAsUsed(String voucherCode, String remark, String orderId, BigDecimal discountAmount) { + @Override + public void markVoucherAsUsed(String voucherCode, String remark, String orderId, BigDecimal discountAmount, Long faceId) { if (!StringUtils.hasText(voucherCode)) { return; } @@ -162,7 +162,7 @@ public class VoucherServiceImpl implements IVoucherService { PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord(); usageRecord.setVoucherCodeId(voucherCodeEntity.getId()); usageRecord.setVoucherCode(voucherCode); - usageRecord.setFaceId(voucherCodeEntity.getFaceId()); + usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getFaceId()); usageRecord.setScenicId(voucherCodeEntity.getScenicId()); usageRecord.setBatchId(voucherCodeEntity.getBatchId()); usageRecord.setUseTime(now); @@ -198,8 +198,8 @@ public class VoucherServiceImpl implements IVoucherService { // 更新批次统计(使用记录数,不是使用券码数) voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1); - log.info("券码使用记录已创建: code={}, useCount={}, maxUseCount={}", - voucherCode, currentUseCount, maxUseCount); + log.info("券码使用记录已创建: code={}, useCount={}, maxUseCount={}, faceId={}", + voucherCode, currentUseCount, maxUseCount, faceId); } @Override diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/OrderServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/OrderServiceImpl.java index 7f4c04af..ca11d230 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/OrderServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/OrderServiceImpl.java @@ -876,13 +876,12 @@ public class OrderServiceImpl implements OrderService { MemberRespVO member = memberMapper.getById(userId); order.setOpenId(member.getOpenId()); order.setScenicId(face.getScenicId()); - - order.setSlashPrice(cachedResult.getOriginalAmount()); - order.setPrice(cachedResult.getFinalAmount()); - // promo code - order.setPayPrice(cachedResult.getFinalAmount()); } - if (order.getPayPrice().equals(BigDecimal.ZERO)) { + order.setSlashPrice(cachedResult.getOriginalAmount()); + order.setPrice(cachedResult.getFinalAmount()); + // promo code + order.setPayPrice(cachedResult.getFinalAmount()); + if (order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) { order.setStatus(OrderStateEnum.PAID.getState()); order.setPayAt(new Date()); } else { @@ -909,7 +908,7 @@ public class OrderServiceImpl implements OrderService { if (cachedResult.getUsedVoucher() != null) { order.setBrokerId(cachedResult.getUsedVoucher().getBrokerId()); order.setPromoCode(cachedResult.getUsedVoucher().getVoucherCode()); - iVoucherService.markVoucherAsUsed(cachedResult.getUsedVoucher().getVoucherCode(), order.getId().toString()); + iVoucherService.markVoucherAsUsed(cachedResult.getUsedVoucher().getVoucherCode(), "用户下单", order.getId().toString(), cachedResult.getUsedVoucher().getDiscountValue(), face.getId()); } List orderItems = new ArrayList<>(); OrderItemEntity orderItem = new OrderItemEntity(); @@ -932,7 +931,7 @@ public class OrderServiceImpl implements OrderService { OrderEntity order = orderMapper.get(orderId); // 检查订单金额是否为0 - if (order.getPayPrice() == null || order.getPayPrice().compareTo(BigDecimal.ZERO) == 0) { + if (order.getPayPrice() == null || order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) { // 零金额订单:设置needPay为false,直接标记为已支付 order.setStatus(1); // 1表示已支付 order.setPayAt(new Date()); diff --git a/src/main/resources/mapper/OrderMapper.xml b/src/main/resources/mapper/OrderMapper.xml index a62111cd..2e7778de 100644 --- a/src/main/resources/mapper/OrderMapper.xml +++ b/src/main/resources/mapper/OrderMapper.xml @@ -224,6 +224,7 @@ coupon_price = #{couponPrice}, coupon_id = #{couponId}, coupon_record_id = #{couponRecordId}, + price = #{price}, pay_price = `price` - `coupon_price` where id = #{id} From 90a21c093366eb6741bb5b99834338974ce1c7ef Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 16 Sep 2025 17:55:24 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix(pricing):=20=E5=AE=8C=E5=96=84=E5=88=B8?= =?UTF-8?q?=E7=A0=81=E9=AA=8C=E8=AF=81=E9=80=BB=E8=BE=91=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BD=BF=E7=94=A8=E6=9D=83=E9=99=90=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增用户在指定批次下的使用次数统计和最后使用时间获取功能 - 重构券码验证逻辑,支持未领取券码的使用权限判断 - 优化已领取券码的使用限制检查,包括使用次数和间隔时间- 改进日志记录,增加剩余使用次数信息 -修复一些潜在的逻辑问题和边界情况处理 --- .../mapper/PriceVoucherUsageRecordMapper.java | 20 +++ .../service/impl/VoucherDiscountProvider.java | 25 +++- .../service/impl/VoucherServiceImpl.java | 115 +++++++++++++----- 3 files changed, 125 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java index 4b9c8744..66dc111e 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java @@ -89,6 +89,26 @@ public interface PriceVoucherUsageRecordMapper extends BaseMapper= maxUseCount) { - return "券码使用次数已达上限"; + // 1. 检查券码使用次数限制(仅针对已领取的券码) + if (isClaimed) { + Integer maxUseCount = batchConfig.getMaxUseCount(); + Integer currentUseCount = voucherCode.getCurrentUseCount() != null ? voucherCode.getCurrentUseCount() : 0; + + if (maxUseCount != null && currentUseCount >= maxUseCount) { + return "券码使用次数已达上限"; + } } // 2. 检查用户使用次数限制 Integer maxUsePerUser = batchConfig.getMaxUsePerUser(); - if (maxUsePerUser != null && faceId != null) { - Integer userUseCount = usageRecordMapper.countByFaceIdAndVoucherCodeId(faceId, voucherCode.getId()); - if (userUseCount >= maxUsePerUser) { - return "您使用该券码的次数已达上限"; + if (maxUsePerUser != null) { + Integer userUseCount; + if (isClaimed) { + // 已领取:检查用户对该具体券码的使用次数 + userUseCount = usageRecordMapper.countByFaceIdAndVoucherCodeId(faceId, voucherCode.getId()); + if (userUseCount >= maxUsePerUser) { + return "您使用该券码的次数已达上限"; + } + } else { + // 未领取:检查用户在该批次下的总使用次数 + userUseCount = usageRecordMapper.countByFaceIdAndBatchId(faceId, batchConfig.getId()); + if (userUseCount >= maxUsePerUser) { + return "您在该批次下使用券码的次数已达上限"; + } } } // 3. 检查使用间隔时间限制 Integer useIntervalHours = batchConfig.getUseIntervalHours(); - if (useIntervalHours != null && faceId != null) { - Date lastUseTime = usageRecordMapper.getLastUseTimeByFaceIdAndVoucherCodeId(faceId, voucherCode.getId()); - if (lastUseTime != null) { - long diffMillis = System.currentTimeMillis() - lastUseTime.getTime(); - long diffHours = TimeUnit.MILLISECONDS.toHours(diffMillis); - if (diffHours < useIntervalHours) { - return String.format("请等待%d小时后再次使用该券码", useIntervalHours - diffHours); + if (useIntervalHours != null) { + Date lastUseTime; + if (isClaimed) { + // 已领取:检查该券码的最后使用时间 + lastUseTime = usageRecordMapper.getLastUseTimeByFaceIdAndVoucherCodeId(faceId, voucherCode.getId()); + if (lastUseTime != null) { + long diffMillis = System.currentTimeMillis() - lastUseTime.getTime(); + long diffHours = TimeUnit.MILLISECONDS.toHours(diffMillis); + if (diffHours < useIntervalHours) { + return String.format("请等待%d小时后再次使用该券码", useIntervalHours - diffHours); + } + } + } else { + // 未领取:检查该批次下的最后使用时间 + lastUseTime = usageRecordMapper.getLastUseTimeByFaceIdAndBatchId(faceId, batchConfig.getId()); + if (lastUseTime != null) { + long diffMillis = System.currentTimeMillis() - lastUseTime.getTime(); + long diffHours = TimeUnit.MILLISECONDS.toHours(diffMillis); + if (diffHours < useIntervalHours) { + return String.format("请等待%d小时后再次使用该批次券码", useIntervalHours - diffHours); + } } } }