Merge branch 'refs/heads/voucher_inf'

This commit is contained in:
2025-09-16 17:57:34 +08:00
27 changed files with 1282 additions and 179 deletions

View File

@@ -193,7 +193,7 @@ public class OrderServiceImpl implements IOrderService {
// 标记券码为已使用
try {
String remark = "订单使用,订单号:" + orderNo;
voucherService.markVoucherAsUsed(voucherInfo.getVoucherCode(), remark);
voucherService.markVoucherAsUsed(voucherInfo.getVoucherCode(), remark, String.valueOf(orderId), order.getDiscountAmount(), order.getFaceId());
log.info("券码状态更新成功: voucherCode={}, orderId={}",
voucherInfo.getVoucherCode(), orderNo);
} catch (Exception e) {

View File

@@ -28,7 +28,7 @@ public class PriceCalculationController {
@PostMapping("/calculate")
public ApiResponse<PriceCalculationResult> calculatePrice(@RequestBody PriceCalculationRequest request) {
log.info("价格计算请求: userId={}, products={}", request.getUserId(), request.getProducts().size());
request.setPreviewOnly(true);
PriceCalculationResult result = priceCalculationService.calculatePrice(request);
log.info("价格计算完成: originalAmount={}, finalAmount={}, usedCoupon={}",

View File

@@ -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<Long> createBatchV2(@RequestBody VoucherBatchCreateReqV2 req) {
Long batchId = voucherBatchService.createBatchV2(req);
return ApiResponse.success(batchId);
}
@PostMapping("/batch/list")
public ApiResponse<Page<VoucherBatchResp>> getBatchList(@RequestBody VoucherBatchQueryReq req) {
Page<VoucherBatchResp> page = voucherBatchService.queryBatchList(req);

View File

@@ -0,0 +1,88 @@
package com.ycwl.basic.pricing.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.pricing.dto.req.VoucherUsageHistoryReq;
import com.ycwl.basic.pricing.dto.resp.VoucherUsageRecordResp;
import com.ycwl.basic.pricing.dto.resp.VoucherUsageStatsResp;
import com.ycwl.basic.pricing.service.IVoucherUsageService;
import 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
public class VoucherUsageController {
private final IVoucherUsageService voucherUsageService;
@PostMapping("/history")
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")
public ApiResponse<List<VoucherUsageRecordResp>> getRecordsByCode(
@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")
public ApiResponse<VoucherUsageStatsResp> getUsageStats(
@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}")
public ApiResponse<List<VoucherUsageRecordResp>> getUserUsageRecords(
@PathVariable Long faceId,
@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")
public ApiResponse<List<VoucherUsageStatsResp>> getBatchUsageStats(
@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());
}
}
}

View File

@@ -56,7 +56,7 @@ public class MobilePriceCalculationRequest {
request.setAutoUseCoupon(this.autoUseCoupon);
request.setVoucherCode(this.voucherCode);
request.setAutoUseVoucher(this.autoUseVoucher);
request.setPreviewOnly(this.previewOnly);
request.setPreviewOnly(true);
return request;
}
}

View File

@@ -89,4 +89,34 @@ public class VoucherInfo {
* null表示适用所有商品类型
*/
private List<ProductType> applicableProducts;
/**
* 当前使用次数
*/
private Integer currentUseCount;
/**
* 最大使用次数
*/
private Integer maxUseCount;
/**
* 每个用户最大使用次数
*/
private Integer maxUsePerUser;
/**
* 使用间隔小时数
*/
private Integer useIntervalHours;
/**
* 剩余可使用次数
*/
private Integer remainingUseCount;
/**
* 最后使用时间
*/
private Date lastUsedTime;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -58,6 +58,16 @@ public class PriceVoucherCode {
*/
private String remark;
/**
* 当前使用次数
*/
private Integer currentUseCount;
/**
* 最后使用时间
*/
private Date lastUsedTime;
@TableField("create_time")
private Date createTime;

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,141 @@
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 faceId 用户faceId
* @param batchId 批次ID
* @return 使用次数
*/
@Select("SELECT COUNT(*) FROM price_voucher_usage_record WHERE face_id = #{faceId} AND batch_id = #{batchId} AND deleted = 0")
Integer countByFaceIdAndBatchId(@Param("faceId") Long faceId, @Param("batchId") Long batchId);
/**
* 获取用户在指定批次下最后一次使用时间
*
* @param faceId 用户faceId
* @param batchId 批次ID
* @return 最后使用时间
*/
@Select("SELECT MAX(use_time) FROM price_voucher_usage_record WHERE face_id = #{faceId} AND batch_id = #{batchId} AND deleted = 0")
Date getLastUseTimeByFaceIdAndBatchId(@Param("faceId") Long faceId, @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.length() > 0\">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 &lt;= #{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);
}

View File

@@ -27,14 +27,16 @@ public interface IVoucherService {
* @return 可用券码列表
*/
List<VoucherInfo> getAvailableVouchers(Long faceId, Long scenicId);
/**
* 标记券码为已使用
* @param voucherCode 券码
* @param remark 使用备注
* @param faceId 人脸ID
*/
void markVoucherAsUsed(String voucherCode, String remark);
void markVoucherAsUsed(String voucherCode, String remark, Long faceId);
void markVoucherAsUsed(String voucherCode, String remark, String orderId, BigDecimal discountAmount, Long faceId);
/**
* 检查用户是否可以在指定景区领取券码
* @param faceId 用户faceId

View File

@@ -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);
}

View File

@@ -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<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req);
VoucherBatchResp getBatchDetail(Long id);

View File

@@ -387,7 +387,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
// 标记券码为已使用
if (result.getUsedVoucher() != null && result.getUsedVoucher().getVoucherCode() != null) {
String remark = String.format("价格计算使用 - 订单金额: %s", result.getFinalAmount());
voucherService.markVoucherAsUsed(result.getUsedVoucher().getVoucherCode(), remark);
voucherService.markVoucherAsUsed(result.getUsedVoucher().getVoucherCode(), remark, request.getFaceId());
log.info("已标记券码为使用: {}", result.getUsedVoucher().getVoucherCode());
}

View File

@@ -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<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
Page<PriceVoucherBatchConfig> page = new Page<>(req.getPageNum(), req.getPageSize());

View File

@@ -112,21 +112,34 @@ public class VoucherDiscountProvider implements IDiscountProvider {
return result;
}
// 重新验证券码
// 重新验证券码,包括重复使用权限验证
VoucherInfo voucherInfo = voucherService.validateAndGetVoucherInfo(
voucherCode,
context.getFaceId(),
context.getScenicId()
);
if (voucherInfo == null || !Boolean.TRUE.equals(voucherInfo.getAvailable())) {
if (voucherInfo == null) {
result.setSuccess(false);
result.setFailureReason("券码无效或不可用");
result.setFailureReason("券码不存在或已失效");
return result;
}
if (!Boolean.TRUE.equals(voucherInfo.getAvailable())) {
result.setSuccess(false);
result.setFailureReason(voucherInfo.getUnavailableReason() != null ?
voucherInfo.getUnavailableReason() : "券码不可用");
return result;
}
// 计算实际优惠金额
BigDecimal actualDiscount = voucherService.calculateVoucherDiscount(voucherInfo, context);
if (actualDiscount.compareTo(BigDecimal.ZERO) <= 0) {
result.setSuccess(false);
result.setFailureReason("券码无法应用到当前商品");
return result;
}
BigDecimal finalAmount;
// 对于全场免费券码,最终金额为0
@@ -145,7 +158,11 @@ public class VoucherDiscountProvider implements IDiscountProvider {
result.setFinalAmount(finalAmount);
result.setSuccess(true);
log.info("成功应用券码: {}, 优惠金额: {}", voucherCode, actualDiscount);
// 显示剩余使用次数信息,处理无限次使用的情况
String remainingInfo = voucherInfo.getRemainingUseCount() != null ?
voucherInfo.getRemainingUseCount().toString() : "无限次";
log.info("成功应用券码: {}, 优惠金额: {}, 剩余使用次数: {}",
voucherCode, actualDiscount, remainingInfo);
} catch (Exception e) {
log.error("应用券码失败: " + discountInfo.getVoucherCode(), e);
@@ -217,7 +234,7 @@ public class VoucherDiscountProvider implements IDiscountProvider {
if (productConfig != null) {
if (!Boolean.TRUE.equals(productConfig.getCanUseVoucher())) {
log.debug("商品配置不允许使用券码: productType={}, productId={}",
log.info("商品配置不允许使用券码: productType={}, productId={}",
product.getProductType().getCode(), productId);
return false;
}

View File

@@ -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;
@@ -18,9 +20,9 @@ import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@@ -34,6 +36,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) {
@@ -60,27 +63,63 @@ public class VoucherServiceImpl implements IVoucherService {
VoucherInfo voucherInfo = buildVoucherInfo(voucherCodeEntity, batchConfig);
// 检查券码状态和可用性
// 检查券码状态和可用性,包含完整的重复使用权限验证
if (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) {
// 未领取状态,检查是否可以领取
if (faceId != null) { // && canClaimVoucher(faceId, voucherCodeEntity.getScenicId())
voucherInfo.setAvailable(true);
} else {
// 未领取状态,也需要检查用户的重复使用权限
if (faceId == null) {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("您已在该景区领取过券码");
}
} else if (VoucherCodeStatus.CLAIMED_UNUSED.getCode().equals(voucherCodeEntity.getStatus())) {
// 已领取未使用,检查是否为当前用户
if (faceId != null && faceId.equals(voucherCodeEntity.getFaceId())) {
voucherInfo.setAvailable(true);
voucherInfo.setUnavailableReason("用户信息缺失,无法验证券码权限");
} else {
// 对于未领取的券码,检查用户在该批次下的使用权限
String availabilityCheck = checkUserUsagePermission(voucherCodeEntity, batchConfig, faceId, false);
if (availabilityCheck == null) {
voucherInfo.setAvailable(true);
log.debug("未领取券码验证通过: code={}, faceId={}", voucherCode, faceId);
} else {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason(availabilityCheck);
log.info("未领取券码使用受限: code={}, faceId={}, reason={}",
voucherCode, faceId, availabilityCheck);
}
}
} else if (VoucherCodeStatus.canUse(voucherCodeEntity.getStatus())) {
// 已领取可使用状态,进行完整的权限验证
if (faceId == null) {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("用户信息缺失,无法验证券码权限");
} else if (!faceId.equals(voucherCodeEntity.getFaceId())) {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("券码已被其他用户领取");
} else {
// 执行详细的重复使用权限验证(已领取状态)
String availabilityCheck = checkUserUsagePermission(voucherCodeEntity, batchConfig, faceId, true);
if (availabilityCheck == null) {
voucherInfo.setAvailable(true);
log.debug("券码验证通过: code={}, faceId={}, currentUseCount={}, maxUseCount={}",
voucherCode, faceId, voucherCodeEntity.getCurrentUseCount(), batchConfig.getMaxUseCount());
} else {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason(availabilityCheck);
log.info("券码使用受限: code={}, faceId={}, reason={}",
voucherCode, faceId, availabilityCheck);
}
}
} else {
// 已使用
} else if (VoucherCodeStatus.isExhausted(voucherCodeEntity.getStatus())) {
// 已用完或已使用
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("券码已使用");
if (batchConfig.getMaxUseCount() != null && batchConfig.getMaxUseCount() == 1) {
voucherInfo.setUnavailableReason("券码已使用");
} else {
voucherInfo.setUnavailableReason("券码使用次数已达上限");
}
} else if (VoucherCodeStatus.isExpired(voucherCodeEntity.getStatus())) {
// 已过期
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("券码已过期");
} else {
// 其他状态
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("券码状态异常");
}
return voucherInfo;
@@ -106,22 +145,83 @@ public class VoucherServiceImpl implements IVoucherService {
return voucherInfos;
}
@Override
public void markVoucherAsUsed(String voucherCode, String remark) {
public void markVoucherAsUsed(String voucherCode, String remark, Long faceId) {
markVoucherAsUsed(voucherCode, remark, null, null, faceId);
}
/**
* 标记券码为已使用(支持可重复使用)
*
* @param voucherCode 券码
* @param remark 使用备注
* @param orderId 订单ID
* @param discountAmount 优惠金额
* @param faceId 人脸ID
*/
@Override
public void markVoucherAsUsed(String voucherCode, String remark, String orderId, BigDecimal discountAmount, Long faceId) {
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(faceId != null ? faceId : 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={}, faceId={}",
voucherCode, currentUseCount, maxUseCount, faceId);
}
@Override
@@ -260,6 +360,79 @@ public class VoucherServiceImpl implements IVoucherService {
return bestVoucher;
}
/**
* 统一检查用户券码使用权限
*
* @param voucherCode 券码实体
* @param batchConfig 批次配置
* @param faceId 用户faceId
* @param isClaimed 是否为已领取状态(true=已领取,检查具体券码权限;false=未领取,检查批次权限)
* @return 不可用原因,null表示可用
*/
private String checkUserUsagePermission(PriceVoucherCode voucherCode, PriceVoucherBatchConfig batchConfig, Long faceId, boolean isClaimed) {
if (faceId == null) {
return "用户信息缺失";
}
// 1. 检查券码使用次数限制(仅针对已领取的券码)
if (isClaimed) {
Integer maxUseCount = batchConfig.getMaxUseCount();
Integer currentUseCount = voucherCode.getCurrentUseCount() != null ? voucherCode.getCurrentUseCount() : 0;
if (maxUseCount != null && currentUseCount >= maxUseCount) {
return "券码使用次数已达上限";
}
}
// 2. 检查用户使用次数限制
Integer maxUsePerUser = batchConfig.getMaxUsePerUser();
if (maxUsePerUser != null) {
Integer userUseCount;
if (isClaimed) {
// 已领取:检查用户对该具体券码的使用次数
userUseCount = usageRecordMapper.countByFaceIdAndVoucherCodeId(faceId, voucherCode.getId());
if (userUseCount >= maxUsePerUser) {
return "您使用该券码的次数已达上限";
}
} else {
// 未领取:检查用户在该批次下的总使用次数
userUseCount = usageRecordMapper.countByFaceIdAndBatchId(faceId, batchConfig.getId());
if (userUseCount >= maxUsePerUser) {
return "您在该批次下使用券码的次数已达上限";
}
}
}
// 3. 检查使用间隔时间限制
Integer useIntervalHours = batchConfig.getUseIntervalHours();
if (useIntervalHours != null) {
Date lastUseTime;
if (isClaimed) {
// 已领取:检查该券码的最后使用时间
lastUseTime = usageRecordMapper.getLastUseTimeByFaceIdAndVoucherCodeId(faceId, voucherCode.getId());
if (lastUseTime != null) {
long diffMillis = System.currentTimeMillis() - lastUseTime.getTime();
long diffHours = TimeUnit.MILLISECONDS.toHours(diffMillis);
if (diffHours < useIntervalHours) {
return String.format("请等待%d小时后再次使用该券码", useIntervalHours - diffHours);
}
}
} else {
// 未领取:检查该批次下的最后使用时间
lastUseTime = usageRecordMapper.getLastUseTimeByFaceIdAndBatchId(faceId, batchConfig.getId());
if (lastUseTime != null) {
long diffMillis = System.currentTimeMillis() - lastUseTime.getTime();
long diffHours = TimeUnit.MILLISECONDS.toHours(diffMillis);
if (diffHours < useIntervalHours) {
return String.format("请等待%d小时后再次使用该批次券码", useIntervalHours - diffHours);
}
}
}
}
return null; // 可用
}
/**
* 构建券码信息DTO
*/
@@ -278,6 +451,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;
}

View File

@@ -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;
}
}

View File

@@ -2,19 +2,13 @@ package com.ycwl.basic.repository;
import com.ycwl.basic.pricing.entity.PriceOnePriceConfig;
import com.ycwl.basic.pricing.service.IOnePricePurchaseService;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class PriceRepository {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public static final String PRICE_ID_CACHE = "price:%s";
@Autowired
private IOnePricePurchaseService onePricePurchaseService;
@@ -48,27 +42,7 @@ public class PriceRepository {
priceConfig.setPrice(config.getOnePrice());
return priceConfig;
}
String cacheKey = String.format(PRICE_ID_CACHE, id);
PriceConfigEntity priceConfigEntity = null;
if (redisTemplate.hasKey(cacheKey)) {
priceConfigEntity = JacksonUtil.parseObject(redisTemplate.opsForValue().get(cacheKey), PriceConfigEntity.class);
}
return priceConfigEntity;
return null;
}
public void clearPriceCache(Integer id) {
if (redisTemplate.hasKey(String.format(PRICE_ID_CACHE, id))) {
PriceConfigEntity priceConfig = getPriceConfig(id);
if (priceConfig != null) {
clearPriceScenicCache(priceConfig.getScenicId());
}
}
redisTemplate.delete(String.format(PRICE_ID_CACHE, id));
}
public void clearPriceScenicCache(Long scenicId) {
String cacheKey = String.format("price:s%s:*", scenicId);
Set<String> keys = redisTemplate.keys(cacheKey);
redisTemplate.delete(keys);
}
}

View File

@@ -41,23 +41,17 @@ public class ScenicRepository {
@Autowired
private ScenicConfigIntegrationService scenicConfigIntegrationService;
public static final String SCENIC_CACHE_KEY = "scenic:%s";
public static final String SCENIC_BASIC_CACHE_KEY = "scenic:basic:%s";
public static final String SCENIC_FULL_CACHE_KEY = "scenic:f%s";
public static final String SCENIC_CONFIG_CACHE_KEY = "scenic:%s:config";
public static final String SCENIC_MP_CACHE_KEY = "scenic:%s:mp";
public static final String SCENIC_MP_NOTIFY_CACHE_KEY = "scenic:%s:mpNotify";
@Autowired
private MpNotifyConfigMapper mpNotifyConfigMapper;
public ScenicV2DTO getScenicBasic(Long id) {
String key = String.format(SCENIC_BASIC_CACHE_KEY, id);
ScenicV2DTO scenicDTO = scenicIntegrationService.getScenic(id);
return scenicDTO;
}
public ScenicEntity getScenic(Long id) {
String key = String.format(SCENIC_CACHE_KEY, id);
ScenicV2WithConfigDTO scenicDTO = scenicIntegrationService.getScenicWithConfig(id);
ScenicEntity scenicEntity = convertToScenicEntity(scenicDTO);
return scenicEntity;
@@ -226,27 +220,6 @@ public class ScenicRepository {
}
}
public void clearCache(Long scenicId) {
redisTemplate.delete(String.format(SCENIC_CACHE_KEY, scenicId));
redisTemplate.delete(String.format(SCENIC_BASIC_CACHE_KEY, scenicId));
redisTemplate.delete(String.format(SCENIC_FULL_CACHE_KEY, scenicId));
redisTemplate.delete(String.format(SCENIC_CONFIG_CACHE_KEY, scenicId));
redisTemplate.delete(String.format(SCENIC_MP_CACHE_KEY, scenicId));
redisTemplate.delete(String.format(SCENIC_MP_NOTIFY_CACHE_KEY, scenicId));
}
private ScenicEntity convertToScenicEntity(ScenicV2DTO dto) {
if (dto == null) {
return null;
}
ScenicEntity entity = new ScenicEntity();
entity.setId(Long.parseLong(dto.getId()));
entity.setName(dto.getName());
entity.setMpId(dto.getMpId());
entity.setStatus(dto.getStatus().toString());
return entity;
}
private ScenicEntity convertToScenicEntity(ScenicV2WithConfigDTO dto) {
if (dto == null) {
return null;
@@ -272,61 +245,6 @@ public class ScenicRepository {
return entity;
}
private ScenicConfigEntity convertToScenicConfigEntity(ScenicV2WithConfigDTO dto, Long scenicId) {
if (dto == null || dto.getConfig() == null) {
return null;
}
ScenicConfigEntity entity = new ScenicConfigEntity();
entity.setScenicId(scenicId);
java.util.Map<String, Object> config = dto.getConfig();
entity.setBookRoutine(ConfigValueUtil.getIntValue(config, "bookRoutine"));
entity.setForceFinishTime(ConfigValueUtil.getIntValue(config, "forceFinishTime"));
entity.setTourTime(ConfigValueUtil.getIntValue(config, "tourTime"));
entity.setSampleStoreDay(ConfigValueUtil.getIntValue(config, "sampleStoreDay"));
entity.setFaceStoreDay(ConfigValueUtil.getIntValue(config, "faceStoreDay"));
entity.setVideoStoreDay(ConfigValueUtil.getIntValue(config, "videoStoreDay"));
entity.setAllFree(ConfigValueUtil.getBooleanValue(config, "allFree"));
entity.setDisableSourceVideo(ConfigValueUtil.getBooleanValue(config, "disableSourceVideo"));
entity.setDisableSourceImage(ConfigValueUtil.getBooleanValue(config, "disableSourceImage"));
entity.setTemplateNewVideoType(ConfigValueUtil.getIntValue(config, "templateNewVideoType"));
entity.setAntiScreenRecordType(ConfigValueUtil.getIntValue(config, "antiScreenRecordType"));
entity.setVideoSourceStoreDay(ConfigValueUtil.getIntValue(config, "videoSourceStoreDay"));
entity.setImageSourceStoreDay(ConfigValueUtil.getIntValue(config, "imageSourceStoreDay"));
entity.setUserSourceExpireDay(ConfigValueUtil.getIntValue(config, "userSourceExpireDay"));
entity.setFaceDetectHelperThreshold(ConfigValueUtil.getIntValue(config, "faceDetectHelperThreshold"));
entity.setPhotoFreeNum(ConfigValueUtil.getIntValue(config, "photoFreeNum"));
entity.setVideoFreeNum(ConfigValueUtil.getIntValue(config, "videoFreeNum"));
entity.setVoucherEnable(ConfigValueUtil.getBooleanValue(config, "voucherEnable"));
entity.setFaceScoreThreshold(ConfigValueUtil.getFloatValue(config, "faceScoreThreshold"));
entity.setBrokerDirectRate(ConfigValueUtil.getBigDecimalValue(config, "brokerDirectRate"));
entity.setWatermarkType(ConfigValueUtil.getStringValue(config, "watermarkType"));
entity.setWatermarkScenicText(ConfigValueUtil.getStringValue(config, "watermarkScenicText"));
entity.setWatermarkDtFormat(ConfigValueUtil.getStringValue(config, "watermarkDtFormat"));
entity.setImageSourcePackHint(ConfigValueUtil.getStringValue(config, "imageSourcePackHint"));
entity.setVideoSourcePackHint(ConfigValueUtil.getStringValue(config, "videoSourcePackHint"));
entity.setExtraNotificationTime(ConfigValueUtil.getStringValue(config, "extraNotificationTime"));
entity.setStoreType(ConfigValueUtil.getEnumValue(config, "storeType", StorageType.class));
entity.setStoreConfigJson(ConfigValueUtil.getStringValue(config, "storeConfigJson"));
entity.setTmpStoreType(ConfigValueUtil.getEnumValue(config, "tmpStoreType", StorageType.class));
entity.setTmpStoreConfigJson(ConfigValueUtil.getStringValue(config, "tmpStoreConfigJson"));
entity.setLocalStoreType(ConfigValueUtil.getEnumValue(config, "localStoreType", StorageType.class));
entity.setLocalStoreConfigJson(ConfigValueUtil.getStringValue(config, "localStoreConfigJson"));
entity.setFaceType(ConfigValueUtil.getEnumValue(config, "faceType", FaceBodyAdapterType.class));
entity.setFaceConfigJson(ConfigValueUtil.getStringValue(config, "faceConfigJson"));
entity.setPayType(ConfigValueUtil.getEnumValue(config, "payType", PayAdapterType.class));
entity.setPayConfigJson(ConfigValueUtil.getStringValue(config, "payConfigJson"));
return entity;
}
/**
* 获取景区配置管理器
*
@@ -341,24 +259,6 @@ public class ScenicRepository {
return null;
}
}
/**
* 获取景区配置管理器,带缓存支持
*
* @param scenicId 景区ID
* @return ScenicConfigManager实例,如果获取失败返回null
*/
public ScenicConfigManager getScenicConfigManagerWithCache(Long scenicId) {
String key = String.format(SCENIC_CONFIG_CACHE_KEY + ":manager", scenicId);
List<com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO> configList =
scenicConfigIntegrationService.listConfigs(scenicId);
if (configList != null) {
ScenicConfigManager manager = new ScenicConfigManager(configList);
return manager;
}
return null;
}
/**
* 批量获取景区名称

View File

@@ -874,13 +874,12 @@ public class OrderServiceImpl implements OrderService {
MemberRespVO member = memberMapper.getById(userId);
order.setOpenId(member.getOpenId());
order.setScenicId(face.getScenicId());
order.setSlashPrice(cachedResult.getOriginalAmount());
order.setPrice(cachedResult.getFinalAmount());
// promo code
order.setPayPrice(cachedResult.getFinalAmount());
}
if (order.getPayPrice().equals(BigDecimal.ZERO)) {
order.setSlashPrice(cachedResult.getOriginalAmount());
order.setPrice(cachedResult.getFinalAmount());
// promo code
order.setPayPrice(cachedResult.getFinalAmount());
if (order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) {
order.setStatus(OrderStateEnum.PAID.getState());
order.setPayAt(new Date());
} else {
@@ -907,7 +906,7 @@ public class OrderServiceImpl implements OrderService {
if (cachedResult.getUsedVoucher() != null) {
order.setBrokerId(cachedResult.getUsedVoucher().getBrokerId());
order.setPromoCode(cachedResult.getUsedVoucher().getVoucherCode());
iVoucherService.markVoucherAsUsed(cachedResult.getUsedVoucher().getVoucherCode(), order.getId().toString());
iVoucherService.markVoucherAsUsed(cachedResult.getUsedVoucher().getVoucherCode(), "用户下单", order.getId().toString(), cachedResult.getUsedVoucher().getDiscountValue(), face.getId());
}
List<OrderItemEntity> orderItems = new ArrayList<>();
OrderItemEntity orderItem = new OrderItemEntity();
@@ -930,7 +929,7 @@ public class OrderServiceImpl implements OrderService {
OrderEntity order = orderMapper.get(orderId);
// 检查订单金额是否为0
if (order.getPayPrice() == null || order.getPayPrice().compareTo(BigDecimal.ZERO) == 0) {
if (order.getPayPrice() == null || order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) {
// 零金额订单:设置needPay为false,直接标记为已支付
order.setStatus(1); // 1表示已支付
order.setPayAt(new Date());

View File

@@ -224,6 +224,7 @@
coupon_price = #{couponPrice},
coupon_id = #{couponId},
coupon_record_id = #{couponRecordId},
price = #{price},
pay_price = `price` - `coupon_price`
where id = #{id}
</update>