You've already forked FrameTour-BE
feat(voucher): 支持券码重复使用
- 新增VoucherBatchCreateReqV2 请求对象,用于创建支持重复使用的券码批次 - 添加 VoucherUsageController 控制器,实现券码使用记录和统计功能 - 在VoucherInfo 对象中增加与重复使用相关的字段 - 修改 PriceVoucherBatchConfig 和 PriceVoucherCode 实体,支持重复使用相关属性 - 更新 VoucherBatchServiceImpl 和 VoucherServiceImpl,增加处理重复使用逻辑的方法
This commit is contained in:
@@ -2,6 +2,7 @@ package com.ycwl.basic.pricing.controller;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
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.VoucherBatchQueryReq;
|
||||||
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
|
||||||
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
|
||||||
@@ -34,6 +35,12 @@ public class VoucherManagementController {
|
|||||||
return ApiResponse.success(batchId);
|
return ApiResponse.success(batchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/batch/create/v2")
|
||||||
|
public ApiResponse<Long> createBatchV2(@RequestBody VoucherBatchCreateReqV2 req) {
|
||||||
|
Long batchId = voucherBatchService.createBatchV2(req);
|
||||||
|
return ApiResponse.success(batchId);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/batch/list")
|
@PostMapping("/batch/list")
|
||||||
public ApiResponse<Page<VoucherBatchResp>> getBatchList(@RequestBody VoucherBatchQueryReq req) {
|
public ApiResponse<Page<VoucherBatchResp>> getBatchList(@RequestBody VoucherBatchQueryReq req) {
|
||||||
Page<VoucherBatchResp> page = voucherBatchService.queryBatchList(req);
|
Page<VoucherBatchResp> page = voucherBatchService.queryBatchList(req);
|
||||||
|
@@ -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<Page<VoucherUsageRecordResp>> getUsageHistory(@RequestBody VoucherUsageHistoryReq req) {
|
||||||
|
try {
|
||||||
|
Page<VoucherUsageRecordResp> 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<List<VoucherUsageRecordResp>> getRecordsByCode(
|
||||||
|
@Parameter(description = "券码") @PathVariable String voucherCode) {
|
||||||
|
try {
|
||||||
|
List<VoucherUsageRecordResp> 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<VoucherUsageStatsResp> 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<List<VoucherUsageRecordResp>> getUserUsageRecords(
|
||||||
|
@Parameter(description = "用户faceId") @PathVariable Long faceId,
|
||||||
|
@Parameter(description = "景区ID") @PathVariable Long scenicId) {
|
||||||
|
try {
|
||||||
|
List<VoucherUsageRecordResp> 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<List<VoucherUsageStatsResp>> getBatchUsageStats(
|
||||||
|
@Parameter(description = "批次ID") @PathVariable Long batchId) {
|
||||||
|
try {
|
||||||
|
List<VoucherUsageStatsResp> statsList = voucherUsageService.getBatchUsageStats(batchId);
|
||||||
|
return ApiResponse.success(statsList);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询批次券码统计信息失败: batchId={}", batchId, e);
|
||||||
|
return ApiResponse.fail("查询失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -89,4 +89,34 @@ public class VoucherInfo {
|
|||||||
* null表示适用所有商品类型
|
* null表示适用所有商品类型
|
||||||
*/
|
*/
|
||||||
private List<ProductType> applicableProducts;
|
private List<ProductType> applicableProducts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前使用次数
|
||||||
|
*/
|
||||||
|
private Integer currentUseCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大使用次数
|
||||||
|
*/
|
||||||
|
private Integer maxUseCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个用户最大使用次数
|
||||||
|
*/
|
||||||
|
private Integer maxUsePerUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用间隔小时数
|
||||||
|
*/
|
||||||
|
private Integer useIntervalHours;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 剩余可使用次数
|
||||||
|
*/
|
||||||
|
private Integer remainingUseCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后使用时间
|
||||||
|
*/
|
||||||
|
private Date lastUsedTime;
|
||||||
}
|
}
|
@@ -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<ProductType> applicableProducts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 券码总数量
|
||||||
|
*/
|
||||||
|
@NotNull(message = "券码数量不能为空")
|
||||||
|
@Min(value = 1, message = "券码数量必须大于0")
|
||||||
|
private Integer totalCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个券码最大使用次数(NULL表示无限次,1表示单次使用)
|
||||||
|
*/
|
||||||
|
private Integer maxUseCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个用户最大使用次数(NULL表示无限次)
|
||||||
|
*/
|
||||||
|
private Integer maxUsePerUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 两次使用间隔小时数(NULL表示无间隔限制)
|
||||||
|
*/
|
||||||
|
private Integer useIntervalHours;
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
@@ -96,6 +96,21 @@ public class PriceVoucherBatchConfig {
|
|||||||
|
|
||||||
private Long updateBy;
|
private Long updateBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个券码最大使用次数(NULL表示无限次,1表示单次使用兼容原有逻辑)
|
||||||
|
*/
|
||||||
|
private Integer maxUseCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个用户最大使用次数(NULL表示无限次)
|
||||||
|
*/
|
||||||
|
private Integer maxUsePerUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 两次使用间隔小时数(NULL表示无间隔限制)
|
||||||
|
*/
|
||||||
|
private Integer useIntervalHours;
|
||||||
|
|
||||||
private Integer deleted;
|
private Integer deleted;
|
||||||
|
|
||||||
private Date deletedAt;
|
private Date deletedAt;
|
||||||
|
@@ -58,6 +58,16 @@ public class PriceVoucherCode {
|
|||||||
*/
|
*/
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前使用次数
|
||||||
|
*/
|
||||||
|
private Integer currentUseCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后使用时间
|
||||||
|
*/
|
||||||
|
private Date lastUsedTime;
|
||||||
|
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
}
|
@@ -16,14 +16,29 @@ public enum VoucherCodeStatus {
|
|||||||
UNCLAIMED(0, "未领取"),
|
UNCLAIMED(0, "未领取"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 已领取未使用
|
* 已领取可使用(可重复使用状态,替代原CLAIMED_UNUSED)
|
||||||
|
*/
|
||||||
|
CLAIMED_AVAILABLE(1, "已领取可使用"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已领取未使用(兼容原有逻辑,等同于CLAIMED_AVAILABLE)
|
||||||
*/
|
*/
|
||||||
CLAIMED_UNUSED(1, "已领取未使用"),
|
CLAIMED_UNUSED(1, "已领取未使用"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 已使用
|
* 已使用(兼容原有逻辑,当达到最大使用次数时为此状态)
|
||||||
*/
|
*/
|
||||||
USED(2, "已使用");
|
USED(2, "已使用"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已领取已用完(达到最大使用次数)
|
||||||
|
*/
|
||||||
|
CLAIMED_EXHAUSTED(3, "已领取已用完"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已过期
|
||||||
|
*/
|
||||||
|
EXPIRED(4, "已过期");
|
||||||
|
|
||||||
private final Integer code;
|
private final Integer code;
|
||||||
private final String name;
|
private final String name;
|
||||||
@@ -55,20 +70,38 @@ public enum VoucherCodeStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否可以使用(已领取未使用状态)
|
* 检查是否可以使用(已领取可使用状态)
|
||||||
* @param code 状态代码
|
* @param code 状态代码
|
||||||
* @return 是否可以使用
|
* @return 是否可以使用
|
||||||
*/
|
*/
|
||||||
public static boolean canUse(Integer code) {
|
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 状态代码
|
* @param code 状态代码
|
||||||
* @return 是否已使用
|
* @return 是否已使用
|
||||||
*/
|
*/
|
||||||
public static boolean isUsed(Integer code) {
|
public static boolean isUsed(Integer code) {
|
||||||
return USED.getCode().equals(code);
|
return USED.getCode().equals(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已过期
|
||||||
|
* @param code 状态代码
|
||||||
|
* @return 是否已过期
|
||||||
|
*/
|
||||||
|
public static boolean isExpired(Integer code) {
|
||||||
|
return EXPIRED.getCode().equals(code);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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<PriceVoucherUsageRecord> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据券码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<PriceVoucherUsageRecord> 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<PriceVoucherUsageRecord> 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<PriceVoucherUsageRecord> 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("<script>" +
|
||||||
|
"SELECT * FROM price_voucher_usage_record WHERE deleted = 0 " +
|
||||||
|
"<if test='batchId != null'>AND batch_id = #{batchId}</if> " +
|
||||||
|
"<if test='voucherCode != null and voucherCode != \"\"'>AND voucher_code LIKE CONCAT('%', #{voucherCode}, '%')</if> " +
|
||||||
|
"<if test='faceId != null'>AND face_id = #{faceId}</if> " +
|
||||||
|
"<if test='scenicId != null'>AND scenic_id = #{scenicId}</if> " +
|
||||||
|
"<if test='startTime != null'>AND use_time >= #{startTime}</if> " +
|
||||||
|
"<if test='endTime != null'>AND use_time <= #{endTime}</if> " +
|
||||||
|
"ORDER BY use_time DESC" +
|
||||||
|
"</script>")
|
||||||
|
Page<PriceVoucherUsageRecord> selectPageWithConditions(Page<PriceVoucherUsageRecord> 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);
|
||||||
|
}
|
@@ -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<VoucherUsageRecordResp> getUsageHistory(VoucherUsageHistoryReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定券码的使用记录
|
||||||
|
*
|
||||||
|
* @param voucherCode 券码
|
||||||
|
* @return 使用记录列表
|
||||||
|
*/
|
||||||
|
List<VoucherUsageRecordResp> getUsageRecordsByCode(String voucherCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户在指定景区的券码使用记录
|
||||||
|
*
|
||||||
|
* @param faceId 用户faceId
|
||||||
|
* @param scenicId 景区ID
|
||||||
|
* @return 使用记录列表
|
||||||
|
*/
|
||||||
|
List<VoucherUsageRecordResp> getUserUsageRecords(Long faceId, Long scenicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取券码使用统计信息
|
||||||
|
*
|
||||||
|
* @param voucherCode 券码
|
||||||
|
* @return 统计信息
|
||||||
|
*/
|
||||||
|
VoucherUsageStatsResp getUsageStats(String voucherCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取批次券码使用统计信息
|
||||||
|
*
|
||||||
|
* @param batchId 批次ID
|
||||||
|
* @return 统计信息列表
|
||||||
|
*/
|
||||||
|
List<VoucherUsageStatsResp> 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);
|
||||||
|
}
|
@@ -2,6 +2,7 @@ package com.ycwl.basic.pricing.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
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.VoucherBatchQueryReq;
|
||||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
||||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
||||||
@@ -11,6 +12,11 @@ public interface VoucherBatchService {
|
|||||||
|
|
||||||
Long createBatch(VoucherBatchCreateReq req);
|
Long createBatch(VoucherBatchCreateReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建券码批次(支持可重复使用)
|
||||||
|
*/
|
||||||
|
Long createBatchV2(VoucherBatchCreateReqV2 req);
|
||||||
|
|
||||||
Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req);
|
Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req);
|
||||||
|
|
||||||
VoucherBatchResp getBatchDetail(Long id);
|
VoucherBatchResp getBatchDetail(Long id);
|
||||||
|
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.ycwl.basic.constant.BaseContextHandler;
|
import com.ycwl.basic.constant.BaseContextHandler;
|
||||||
import com.ycwl.basic.exception.BizException;
|
import com.ycwl.basic.exception.BizException;
|
||||||
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
|
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.VoucherBatchQueryReq;
|
||||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherBatchResp;
|
||||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
import com.ycwl.basic.pricing.dto.resp.VoucherBatchStatsResp;
|
||||||
@@ -78,6 +79,72 @@ public class VoucherBatchServiceImpl implements VoucherBatchService {
|
|||||||
return batch.getId();
|
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
|
@Override
|
||||||
public Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
|
public Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
|
||||||
Page<PriceVoucherBatchConfig> page = new Page<>(req.getPageNum(), req.getPageSize());
|
Page<PriceVoucherBatchConfig> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||||
|
@@ -10,6 +10,8 @@ import com.ycwl.basic.pricing.enums.VoucherCodeStatus;
|
|||||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||||
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
||||||
import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper;
|
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 com.ycwl.basic.pricing.service.IVoucherService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -19,6 +21,8 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -34,6 +38,7 @@ public class VoucherServiceImpl implements IVoucherService {
|
|||||||
|
|
||||||
private final PriceVoucherCodeMapper voucherCodeMapper;
|
private final PriceVoucherCodeMapper voucherCodeMapper;
|
||||||
private final PriceVoucherBatchConfigMapper voucherBatchConfigMapper;
|
private final PriceVoucherBatchConfigMapper voucherBatchConfigMapper;
|
||||||
|
private final PriceVoucherUsageRecordMapper usageRecordMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VoucherInfo validateAndGetVoucherInfo(String voucherCode, Long faceId, Long scenicId) {
|
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 (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) {
|
||||||
// 未领取状态,检查是否可以领取
|
// 未领取状态,检查是否可以领取
|
||||||
if (faceId != null) { // && canClaimVoucher(faceId, voucherCodeEntity.getScenicId())
|
if (faceId != null) {
|
||||||
voucherInfo.setAvailable(true);
|
voucherInfo.setAvailable(true);
|
||||||
} else {
|
} else {
|
||||||
voucherInfo.setAvailable(false);
|
voucherInfo.setAvailable(false);
|
||||||
voucherInfo.setUnavailableReason("您已在该景区领取过券码");
|
voucherInfo.setUnavailableReason("您已在该景区领取过券码");
|
||||||
}
|
}
|
||||||
} else if (VoucherCodeStatus.CLAIMED_UNUSED.getCode().equals(voucherCodeEntity.getStatus())) {
|
} else if (VoucherCodeStatus.canUse(voucherCodeEntity.getStatus())) {
|
||||||
// 已领取未使用,检查是否为当前用户
|
// 已领取可使用状态,检查是否为当前用户且未达到使用限制
|
||||||
if (faceId != null && faceId.equals(voucherCodeEntity.getFaceId())) {
|
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 {
|
} else {
|
||||||
voucherInfo.setAvailable(false);
|
voucherInfo.setAvailable(false);
|
||||||
voucherInfo.setUnavailableReason("券码已被其他用户领取");
|
voucherInfo.setUnavailableReason("券码已被其他用户领取");
|
||||||
}
|
}
|
||||||
} else {
|
} else if (VoucherCodeStatus.isExhausted(voucherCodeEntity.getStatus())) {
|
||||||
// 已使用
|
// 已用完或已使用
|
||||||
voucherInfo.setAvailable(false);
|
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;
|
return voucherInfo;
|
||||||
@@ -109,19 +128,78 @@ public class VoucherServiceImpl implements IVoucherService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void markVoucherAsUsed(String voucherCode, String remark) {
|
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)) {
|
if (!StringUtils.hasText(voucherCode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int result = voucherCodeMapper.useVoucher(voucherCode, LocalDateTime.now(), remark);
|
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
|
||||||
if (result > 0) {
|
if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) {
|
||||||
// 更新批次统计
|
log.warn("券码不存在或已删除: {}", voucherCode);
|
||||||
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
|
return;
|
||||||
if (voucherCodeEntity != null) {
|
|
||||||
voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1);
|
|
||||||
}
|
|
||||||
log.info("券码已标记为使用: {}", voucherCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
@Override
|
||||||
@@ -260,6 +338,48 @@ public class VoucherServiceImpl implements IVoucherService {
|
|||||||
return bestVoucher;
|
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
|
* 构建券码信息DTO
|
||||||
*/
|
*/
|
||||||
@@ -278,6 +398,22 @@ public class VoucherServiceImpl implements IVoucherService {
|
|||||||
voucherInfo.setUsedTime(voucherCode.getUsedTime());
|
voucherInfo.setUsedTime(voucherCode.getUsedTime());
|
||||||
voucherInfo.setApplicableProducts(batchConfig.getApplicableProducts());
|
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;
|
return voucherInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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<VoucherUsageRecordResp> getUsageHistory(VoucherUsageHistoryReq req) {
|
||||||
|
Page<PriceVoucherUsageRecord> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||||
|
|
||||||
|
Page<PriceVoucherUsageRecord> entityPage = usageRecordMapper.selectPageWithConditions(
|
||||||
|
page, req.getBatchId(), req.getVoucherCode(), req.getFaceId(),
|
||||||
|
req.getScenicId(), req.getStartTime(), req.getEndTime());
|
||||||
|
|
||||||
|
Page<VoucherUsageRecordResp> respPage = new Page<>();
|
||||||
|
BeanUtils.copyProperties(entityPage, respPage);
|
||||||
|
|
||||||
|
List<VoucherUsageRecordResp> respList = new ArrayList<>();
|
||||||
|
for (PriceVoucherUsageRecord record : entityPage.getRecords()) {
|
||||||
|
VoucherUsageRecordResp resp = convertToResp(record);
|
||||||
|
respList.add(resp);
|
||||||
|
}
|
||||||
|
respPage.setRecords(respList);
|
||||||
|
|
||||||
|
return respPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<VoucherUsageRecordResp> getUsageRecordsByCode(String voucherCode) {
|
||||||
|
List<PriceVoucherUsageRecord> records = usageRecordMapper.selectByVoucherCode(voucherCode);
|
||||||
|
List<VoucherUsageRecordResp> respList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (PriceVoucherUsageRecord record : records) {
|
||||||
|
respList.add(convertToResp(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
return respList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<VoucherUsageRecordResp> getUserUsageRecords(Long faceId, Long scenicId) {
|
||||||
|
List<PriceVoucherUsageRecord> records = usageRecordMapper.selectByFaceIdAndScenicId(faceId, scenicId);
|
||||||
|
List<VoucherUsageRecordResp> 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<VoucherUsageStatsResp> 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;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user