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