You've already forked FrameTour-BE
Compare commits
14 Commits
221f0175e6
...
a2348e3692
Author | SHA1 | Date | |
---|---|---|---|
a2348e3692 | |||
6006fe460c | |||
1506ae95b8 | |||
8380b02fbb | |||
c6681a249e | |||
90a21c0933 | |||
57266eb535 | |||
7cfcc44531 | |||
2f51470d43 | |||
a61ecf7646 | |||
dcd5a8f930 | |||
ce3f7aae1e | |||
5531c576e0 | |||
e43809593b |
@@ -44,6 +44,11 @@ public class ProjectEntity {
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 模板ID,用于绑定模板
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
private Date createAt;
|
||||
private Date updateAt;
|
||||
}
|
@@ -28,4 +28,9 @@ public class ProjectReqQuery extends BaseQueryParameterReq {
|
||||
* 状态,0禁用,1启用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
}
|
@@ -45,6 +45,16 @@ public class ProjectRespVO {
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 模板名称
|
||||
*/
|
||||
private String templateName;
|
||||
|
||||
private Date createAt;
|
||||
private Date updateAt;
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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={}",
|
||||
|
@@ -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);
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -89,4 +89,79 @@ public class VoucherInfo {
|
||||
* null表示适用所有商品类型
|
||||
*/
|
||||
private List<ProductType> applicableProducts;
|
||||
|
||||
/**
|
||||
* 当前使用次数
|
||||
*/
|
||||
private Integer currentUseCount;
|
||||
|
||||
/**
|
||||
* 最大使用次数
|
||||
*/
|
||||
private Integer maxUseCount;
|
||||
|
||||
/**
|
||||
* 每个用户最大使用次数
|
||||
*/
|
||||
private Integer maxUsePerUser;
|
||||
|
||||
/**
|
||||
* 使用间隔小时数
|
||||
*/
|
||||
private Integer useIntervalHours;
|
||||
|
||||
/**
|
||||
* 剩余可使用次数
|
||||
*/
|
||||
private Integer remainingUseCount;
|
||||
/**
|
||||
* 是否还能使用
|
||||
*/
|
||||
private Boolean canUseMore;
|
||||
|
||||
/**
|
||||
* 最后使用时间
|
||||
*/
|
||||
private Date lastUsedTime;
|
||||
|
||||
/**
|
||||
* 有效期开始时间
|
||||
*/
|
||||
private Date validStartTime;
|
||||
|
||||
/**
|
||||
* 有效期结束时间
|
||||
*/
|
||||
private Date validEndTime;
|
||||
|
||||
/**
|
||||
* 检查指定时间是否在有效期内
|
||||
* @param checkTime 待检查的时间
|
||||
* @return true-在有效期内,false-不在有效期内
|
||||
*/
|
||||
public boolean isWithinValidTimeRange(Date checkTime) {
|
||||
if (checkTime == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查开始时间
|
||||
if (validStartTime != null && checkTime.before(validStartTime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查结束时间
|
||||
if (validEndTime != null && checkTime.after(validEndTime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前时间是否在有效期内
|
||||
* @return true-在有效期内,false-不在有效期内
|
||||
*/
|
||||
public boolean isWithinValidTimeRange() {
|
||||
return isWithinValidTimeRange(new Date());
|
||||
}
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
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.Date;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 有效期开始时间(NULL表示无开始时间限制)
|
||||
*/
|
||||
private Date validStartTime;
|
||||
|
||||
/**
|
||||
* 有效期结束时间(NULL表示无结束时间限制)
|
||||
*/
|
||||
private Date validEndTime;
|
||||
}
|
@@ -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,80 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 券码批次概览响应
|
||||
*/
|
||||
@Data
|
||||
public class VoucherBatchOverviewResp {
|
||||
|
||||
/**
|
||||
* 批次ID
|
||||
*/
|
||||
private Long batchId;
|
||||
|
||||
/**
|
||||
* 批次名称
|
||||
*/
|
||||
private String batchName;
|
||||
|
||||
/**
|
||||
* 券码总数
|
||||
*/
|
||||
private Integer totalCodes;
|
||||
|
||||
/**
|
||||
* 已领取券码数
|
||||
*/
|
||||
private Integer claimedCodes;
|
||||
|
||||
/**
|
||||
* 已使用券码数(至少使用过一次)
|
||||
*/
|
||||
private Integer usedCodes;
|
||||
|
||||
/**
|
||||
* 已达到最大使用次数的券码数
|
||||
*/
|
||||
private Integer exhaustedCodes;
|
||||
|
||||
/**
|
||||
* 总使用记录数(支持重复使用)
|
||||
*/
|
||||
private Integer totalUsageRecords;
|
||||
|
||||
/**
|
||||
* 平均每个券码使用次数
|
||||
*/
|
||||
private BigDecimal averageUsagePerCode;
|
||||
|
||||
/**
|
||||
* 重复使用率 (重复使用的券码数/已使用券码数 * 100)
|
||||
*/
|
||||
private BigDecimal repeatUsageRate;
|
||||
|
||||
/**
|
||||
* 可重复使用设置
|
||||
*/
|
||||
private RepeatableSettings repeatableSettings;
|
||||
|
||||
@Data
|
||||
public static class RepeatableSettings {
|
||||
/**
|
||||
* 每个券码最大使用次数
|
||||
*/
|
||||
private Integer maxUseCount;
|
||||
|
||||
/**
|
||||
* 每个用户最大使用次数
|
||||
*/
|
||||
private Integer maxUsePerUser;
|
||||
|
||||
/**
|
||||
* 使用间隔小时数
|
||||
*/
|
||||
private Integer useIntervalHours;
|
||||
}
|
||||
}
|
@@ -30,4 +30,14 @@ public class VoucherBatchResp {
|
||||
* null表示适用所有商品类型
|
||||
*/
|
||||
private List<ProductType> applicableProducts;
|
||||
|
||||
/**
|
||||
* 有效期开始时间
|
||||
*/
|
||||
private Date validStartTime;
|
||||
|
||||
/**
|
||||
* 有效期结束时间
|
||||
*/
|
||||
private Date validEndTime;
|
||||
}
|
@@ -24,4 +24,30 @@ public class VoucherCodeResp {
|
||||
private String discountTypeName;
|
||||
private String discountDescription;
|
||||
private BigDecimal discountValue;
|
||||
|
||||
// 可重复使用券码相关字段
|
||||
/**
|
||||
* 当前使用次数
|
||||
*/
|
||||
private Integer currentUseCount;
|
||||
|
||||
/**
|
||||
* 最大使用次数
|
||||
*/
|
||||
private Integer maxUseCount;
|
||||
|
||||
/**
|
||||
* 剩余使用次数
|
||||
*/
|
||||
private Integer remainingUseCount;
|
||||
|
||||
/**
|
||||
* 是否还能使用
|
||||
*/
|
||||
private Boolean canUseMore;
|
||||
|
||||
/**
|
||||
* 最后使用时间
|
||||
*/
|
||||
private Date lastUsedTime;
|
||||
}
|
@@ -0,0 +1,142 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 券码详情响应
|
||||
*/
|
||||
@Data
|
||||
public class VoucherDetailResp {
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 批次信息
|
||||
*/
|
||||
private BatchInfo batchInfo;
|
||||
|
||||
/**
|
||||
* 使用信息
|
||||
*/
|
||||
private UsageInfo usageInfo;
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
private UserInfo userInfo;
|
||||
|
||||
/**
|
||||
* 状态信息
|
||||
*/
|
||||
private StatusInfo statusInfo;
|
||||
|
||||
@Data
|
||||
public static class BatchInfo {
|
||||
/**
|
||||
* 批次ID
|
||||
*/
|
||||
private Long batchId;
|
||||
|
||||
/**
|
||||
* 批次名称
|
||||
*/
|
||||
private String batchName;
|
||||
|
||||
/**
|
||||
* 优惠类型 (0: 赠品兑换, 1: 金额抵扣, 2: 百分比折扣)
|
||||
*/
|
||||
private Integer discountType;
|
||||
|
||||
/**
|
||||
* 优惠值
|
||||
*/
|
||||
private BigDecimal discountValue;
|
||||
|
||||
/**
|
||||
* 适用商品类型
|
||||
*/
|
||||
private List<ProductType> applicableProducts;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UsageInfo {
|
||||
/**
|
||||
* 最大使用次数
|
||||
*/
|
||||
private Integer maxUseCount;
|
||||
|
||||
/**
|
||||
* 当前已使用次数
|
||||
*/
|
||||
private Integer currentUseCount;
|
||||
|
||||
/**
|
||||
* 剩余使用次数
|
||||
*/
|
||||
private Integer remainingUseCount;
|
||||
|
||||
/**
|
||||
* 每个用户最大使用次数
|
||||
*/
|
||||
private Integer maxUsePerUser;
|
||||
|
||||
/**
|
||||
* 使用间隔小时数
|
||||
*/
|
||||
private Integer useIntervalHours;
|
||||
|
||||
/**
|
||||
* 是否还能使用
|
||||
*/
|
||||
private Boolean canUseMore;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UserInfo {
|
||||
/**
|
||||
* 用户人脸ID
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 该用户已使用此券码的次数
|
||||
*/
|
||||
private Integer userUsageCount;
|
||||
|
||||
/**
|
||||
* 最后使用时间
|
||||
*/
|
||||
private Date lastUsedTime;
|
||||
|
||||
/**
|
||||
* 下次可用时间
|
||||
*/
|
||||
private Date nextAvailableTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class StatusInfo {
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 状态名称
|
||||
*/
|
||||
private String statusName;
|
||||
|
||||
/**
|
||||
* 领取时间
|
||||
*/
|
||||
private Date claimedTime;
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 使用序号(该券码的第几次使用)
|
||||
*/
|
||||
private Integer usageSequence;
|
||||
|
||||
/**
|
||||
* 使用时间
|
||||
*/
|
||||
@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;
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 券码使用统计摘要响应
|
||||
*/
|
||||
@Data
|
||||
public class VoucherUsageSummaryResp {
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 批次ID
|
||||
*/
|
||||
private Long batchId;
|
||||
|
||||
/**
|
||||
* 批次名称
|
||||
*/
|
||||
private String batchName;
|
||||
|
||||
/**
|
||||
* 总使用记录数(重复使用产生多条记录)
|
||||
*/
|
||||
private Integer totalUsageRecords;
|
||||
|
||||
/**
|
||||
* 使用过的独立用户数
|
||||
*/
|
||||
private Integer uniqueUsers;
|
||||
|
||||
/**
|
||||
* 使用统计
|
||||
*/
|
||||
private UsageStatistics usageStatistics;
|
||||
|
||||
/**
|
||||
* 使用时间轴
|
||||
*/
|
||||
private List<UsageTimeline> usageTimeline;
|
||||
|
||||
@Data
|
||||
public static class UsageStatistics {
|
||||
/**
|
||||
* 最大可使用次数
|
||||
*/
|
||||
private Integer maxUseCount;
|
||||
|
||||
/**
|
||||
* 当前已使用次数
|
||||
*/
|
||||
private Integer currentUseCount;
|
||||
|
||||
/**
|
||||
* 剩余使用次数
|
||||
*/
|
||||
private Integer remainingUseCount;
|
||||
|
||||
/**
|
||||
* 使用率
|
||||
*/
|
||||
private BigDecimal usageRate;
|
||||
|
||||
/**
|
||||
* 是否还能使用
|
||||
*/
|
||||
private Boolean canUseMore;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UsageTimeline {
|
||||
/**
|
||||
* 使用时间
|
||||
*/
|
||||
private Date useTime;
|
||||
|
||||
/**
|
||||
* 用户人脸ID
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 优惠金额
|
||||
*/
|
||||
private BigDecimal discountAmount;
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 券码验证响应
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class VoucherValidationResp {
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 是否可用
|
||||
*/
|
||||
private Boolean available;
|
||||
|
||||
/**
|
||||
* 不可用原因
|
||||
*/
|
||||
private String unavailableReason;
|
||||
|
||||
/**
|
||||
* 券码详细信息
|
||||
*/
|
||||
private VoucherInfo voucherInfo;
|
||||
}
|
@@ -96,10 +96,68 @@ 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;
|
||||
|
||||
/**
|
||||
* 有效期开始时间(NULL表示无开始时间限制)
|
||||
*/
|
||||
@TableField("valid_start_time")
|
||||
private Date validStartTime;
|
||||
|
||||
/**
|
||||
* 有效期结束时间(NULL表示无结束时间限制)
|
||||
*/
|
||||
@TableField("valid_end_time")
|
||||
private Date validEndTime;
|
||||
|
||||
/**
|
||||
* 检查当前时间是否在有效期内
|
||||
* @return true-在有效期内,false-不在有效期内
|
||||
*/
|
||||
public boolean isWithinValidTimeRange() {
|
||||
return isWithinValidTimeRange(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定时间是否在有效期内
|
||||
* @param checkTime 待检查的时间
|
||||
* @return true-在有效期内,false-不在有效期内
|
||||
*/
|
||||
public boolean isWithinValidTimeRange(Date checkTime) {
|
||||
if (checkTime == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查开始时间
|
||||
if (validStartTime != null && checkTime.before(validStartTime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查结束时间
|
||||
if (validEndTime != null && checkTime.after(validEndTime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适用商品类型列表
|
||||
*/
|
||||
|
@@ -58,6 +58,16 @@ public class PriceVoucherCode {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 当前使用次数
|
||||
*/
|
||||
private Integer currentUseCount;
|
||||
|
||||
/**
|
||||
* 最后使用时间
|
||||
*/
|
||||
private Date lastUsedTime;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
|
@@ -0,0 +1,81 @@
|
||||
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 Integer usageSequence;
|
||||
|
||||
/**
|
||||
* 使用时间
|
||||
*/
|
||||
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, "未领取"),
|
||||
|
||||
/**
|
||||
* 已领取未使用
|
||||
* 已领取可使用(可重复使用状态,替代原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);
|
||||
}
|
||||
}
|
@@ -58,17 +58,6 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
|
||||
@Param("faceId") Long faceId,
|
||||
@Param("claimedTime") LocalDateTime claimedTime);
|
||||
|
||||
/**
|
||||
* 使用券码(更新状态为已使用)
|
||||
* @param code 券码
|
||||
* @param usedTime 使用时间
|
||||
* @param remark 使用备注
|
||||
* @return 影响行数
|
||||
*/
|
||||
int useVoucher(@Param("code") String code,
|
||||
@Param("usedTime") LocalDateTime usedTime,
|
||||
@Param("remark") String remark);
|
||||
|
||||
/**
|
||||
* 根据批次ID查询券码列表(支持分页)
|
||||
* @param batchId 批次ID
|
||||
|
@@ -0,0 +1,152 @@
|
||||
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);
|
||||
|
||||
|
||||
/**
|
||||
* 根据券码ID和用户ID查询使用记录
|
||||
*
|
||||
* @param voucherCodeId 券码ID
|
||||
* @param faceId 用户faceId
|
||||
* @return 使用记录列表
|
||||
*/
|
||||
@Select("SELECT * FROM price_voucher_usage_record WHERE voucher_code_id = #{voucherCodeId} AND face_id = #{faceId} AND deleted = 0 ORDER BY use_time DESC")
|
||||
List<PriceVoucherUsageRecord> selectByVoucherCodeAndFaceId(@Param("voucherCodeId") Long voucherCodeId, @Param("faceId") Long faceId);
|
||||
|
||||
/**
|
||||
* 根据用户和景区查询使用记录
|
||||
*
|
||||
* @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 <= #{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);
|
||||
}
|
@@ -2,6 +2,9 @@ package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchOverviewResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherDetailResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherUsageSummaryResp;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
@@ -27,14 +30,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
|
||||
@@ -59,6 +64,28 @@ public interface IVoucherService {
|
||||
*/
|
||||
BigDecimal calculateVoucherDiscount(VoucherInfo voucherInfo, DiscountDetectionContext context);
|
||||
|
||||
/**
|
||||
* 获取券码详细信息
|
||||
* @param voucherCode 券码
|
||||
* @param faceId 用户人脸ID(可选)
|
||||
* @return 券码详细信息
|
||||
*/
|
||||
VoucherDetailResp getVoucherDetail(String voucherCode, Long faceId);
|
||||
|
||||
/**
|
||||
* 获取券码使用统计摘要
|
||||
* @param voucherCode 券码
|
||||
* @return 券码使用统计摘要
|
||||
*/
|
||||
VoucherUsageSummaryResp getVoucherUsageSummary(String voucherCode);
|
||||
|
||||
/**
|
||||
* 获取批次券码概览
|
||||
* @param batchId 批次ID
|
||||
* @return 批次概览信息
|
||||
*/
|
||||
VoucherBatchOverviewResp getBatchOverview(Long batchId);
|
||||
|
||||
/**
|
||||
* 获取最优的券码(如果用户有多个可用券码)
|
||||
* @param faceId 用户faceId
|
||||
|
@@ -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.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);
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
|
@@ -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,79 @@ 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, "使用间隔小时数不能为负数");
|
||||
}
|
||||
|
||||
// 验证时间范围参数
|
||||
if (req.getValidStartTime() != null && req.getValidEndTime() != null) {
|
||||
if (req.getValidStartTime().after(req.getValidEndTime())) {
|
||||
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());
|
||||
|
@@ -49,74 +49,79 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
||||
private VoucherBatchService voucherBatchService;
|
||||
|
||||
@Override
|
||||
public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
|
||||
List<PriceVoucherCode> codes = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
PriceVoucherCode code = new PriceVoucherCode();
|
||||
code.setBatchId(batchId);
|
||||
code.setScenicId(scenicId);
|
||||
code.setCode(generateVoucherCode());
|
||||
code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode());
|
||||
code.setCreateTime(new Date());
|
||||
code.setDeleted(0);
|
||||
codes.add(code);
|
||||
}
|
||||
|
||||
for (PriceVoucherCode code : codes) {
|
||||
voucherCodeMapper.insert(code);
|
||||
}
|
||||
public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
|
||||
List<PriceVoucherCode> codes = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
PriceVoucherCode code = new PriceVoucherCode();
|
||||
code.setBatchId(batchId);
|
||||
code.setScenicId(scenicId);
|
||||
code.setCode(generateVoucherCode());
|
||||
code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode());
|
||||
code.setCurrentUseCount(0); // 初始化使用次数为0
|
||||
code.setCreateTime(new Date());
|
||||
code.setDeleted(0);
|
||||
codes.add(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
|
||||
if (req.getScenicId() == null) {
|
||||
throw new BizException(400, "景区ID不能为空");
|
||||
}
|
||||
if (req.getFaceId() == null) {
|
||||
throw new BizException(400, "用户faceId不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(req.getCode())) {
|
||||
throw new BizException(400, "券码不能为空");
|
||||
}
|
||||
|
||||
// 验证券码是否存在且未被领取
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getCode, req.getCode())
|
||||
.eq(PriceVoucherCode::getScenicId, req.getScenicId())
|
||||
.eq(PriceVoucherCode::getDeleted, 0);
|
||||
|
||||
PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper);
|
||||
if (voucherCode == null) {
|
||||
throw new BizException(400, "券码不存在或不属于该景区");
|
||||
}
|
||||
|
||||
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
|
||||
throw new BizException(400, "券码已被领取或已使用");
|
||||
}
|
||||
|
||||
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
|
||||
throw new BizException(400, "该用户在此景区已领取过券码");
|
||||
}
|
||||
|
||||
// 获取券码所属批次
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId());
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(400, "券码批次不存在");
|
||||
}
|
||||
|
||||
// 更新券码状态
|
||||
voucherCode.setFaceId(req.getFaceId());
|
||||
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
|
||||
voucherCode.setClaimedTime(new Date());
|
||||
|
||||
voucherCodeMapper.updateById(voucherCode);
|
||||
|
||||
voucherBatchService.updateBatchClaimedCount(batch.getId());
|
||||
|
||||
return convertToResp(voucherCode, batch);
|
||||
for (PriceVoucherCode code : codes) {
|
||||
voucherCodeMapper.insert(code);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
|
||||
if (req.getScenicId() == null) {
|
||||
throw new BizException(400, "景区ID不能为空");
|
||||
}
|
||||
if (req.getFaceId() == null) {
|
||||
throw new BizException(400, "用户faceId不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(req.getCode())) {
|
||||
throw new BizException(400, "券码不能为空");
|
||||
}
|
||||
|
||||
// 验证券码是否存在且未被领取
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getCode, req.getCode())
|
||||
.eq(PriceVoucherCode::getScenicId, req.getScenicId())
|
||||
.eq(PriceVoucherCode::getDeleted, 0);
|
||||
|
||||
PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper);
|
||||
if (voucherCode == null) {
|
||||
throw new BizException(400, "券码不存在或不属于该景区");
|
||||
}
|
||||
|
||||
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
|
||||
throw new BizException(400, "券码已被领取或已使用");
|
||||
}
|
||||
|
||||
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
|
||||
throw new BizException(400, "该用户在此景区已领取过券码");
|
||||
}
|
||||
|
||||
// 获取券码所属批次
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId());
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(400, "券码批次不存在");
|
||||
}
|
||||
|
||||
// 更新券码状态
|
||||
voucherCode.setFaceId(req.getFaceId());
|
||||
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
|
||||
voucherCode.setClaimedTime(new Date());
|
||||
// 确保currentUseCount被初始化
|
||||
if (voucherCode.getCurrentUseCount() == null) {
|
||||
voucherCode.setCurrentUseCount(0);
|
||||
}
|
||||
|
||||
voucherCodeMapper.updateById(voucherCode);
|
||||
|
||||
voucherBatchService.updateBatchClaimedCount(batch.getId());
|
||||
|
||||
return convertToResp(voucherCode, batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) {
|
||||
@@ -165,26 +170,31 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void markCodeAsUsed(Long codeId, String remark) {
|
||||
PriceVoucherCode code = voucherCodeMapper.selectById(codeId);
|
||||
if (code == null || code.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码不存在");
|
||||
}
|
||||
|
||||
if (!Objects.equals(code.getStatus(), VoucherCodeStatus.CLAIMED_UNUSED.getCode())) {
|
||||
throw new BizException(400, "券码状态异常,无法使用");
|
||||
}
|
||||
|
||||
code.setStatus(VoucherCodeStatus.USED.getCode());
|
||||
code.setUsedTime(new Date());
|
||||
code.setRemark(remark);
|
||||
|
||||
voucherCodeMapper.updateById(code);
|
||||
|
||||
voucherBatchService.updateBatchUsedCount(code.getBatchId());
|
||||
@Transactional
|
||||
public void markCodeAsUsed(Long codeId, String remark) {
|
||||
PriceVoucherCode code = voucherCodeMapper.selectById(codeId);
|
||||
if (code == null || code.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码不存在");
|
||||
}
|
||||
|
||||
if (!Objects.equals(code.getStatus(), VoucherCodeStatus.CLAIMED_UNUSED.getCode())) {
|
||||
throw new BizException(400, "券码状态异常,无法使用");
|
||||
}
|
||||
|
||||
// 更新使用计数和时间
|
||||
Integer currentUseCount = code.getCurrentUseCount() != null ? code.getCurrentUseCount() : 0;
|
||||
code.setCurrentUseCount(currentUseCount + 1);
|
||||
code.setLastUsedTime(new Date());
|
||||
|
||||
code.setStatus(VoucherCodeStatus.USED.getCode());
|
||||
code.setUsedTime(new Date());
|
||||
code.setRemark(remark);
|
||||
|
||||
voucherCodeMapper.updateById(code);
|
||||
|
||||
voucherBatchService.updateBatchUsedCount(code.getBatchId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
||||
@@ -257,6 +267,24 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
||||
resp.setDiscountTypeName(discountType.getName());
|
||||
resp.setDiscountDescription(discountType.getDescription());
|
||||
}
|
||||
|
||||
// 设置可重复使用相关字段
|
||||
Integer currentUseCount = code.getCurrentUseCount() != null ? code.getCurrentUseCount() : 0;
|
||||
resp.setCurrentUseCount(currentUseCount);
|
||||
resp.setMaxUseCount(batch.getMaxUseCount());
|
||||
resp.setLastUsedTime(code.getLastUsedTime());
|
||||
|
||||
// 计算剩余使用次数
|
||||
if (batch.getMaxUseCount() != null) {
|
||||
int remaining = batch.getMaxUseCount() - currentUseCount;
|
||||
resp.setRemainingUseCount(Math.max(0, remaining));
|
||||
|
||||
// 计算是否还能使用
|
||||
resp.setCanUseMore(currentUseCount < batch.getMaxUseCount());
|
||||
} else {
|
||||
resp.setRemainingUseCount(null); // 无限制
|
||||
resp.setCanUseMore(true);
|
||||
}
|
||||
}
|
||||
|
||||
VoucherCodeStatus status = VoucherCodeStatus.getByCode(code.getStatus());
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -1,5 +1,34 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherDetailResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherUsageSummaryResp;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherBatchOverviewResp;
|
||||
import com.ycwl.basic.pricing.enums.ProductType;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherBatchConfig;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherCode;
|
||||
import com.ycwl.basic.pricing.enums.VoucherCodeStatus;
|
||||
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherBatchConfigMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherUsageRecordMapper;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherUsageRecord;
|
||||
import com.ycwl.basic.pricing.service.IVoucherService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
@@ -10,6 +39,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 +49,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 +65,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) {
|
||||
@@ -53,34 +85,84 @@ public class VoucherServiceImpl implements IVoucherService {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoucherInfo voucherInfo = buildVoucherInfo(voucherCodeEntity, batchConfig);
|
||||
|
||||
// 检查券码批次的时间范围,提供详细的时间范围信息
|
||||
if (!batchConfig.isWithinValidTimeRange()) {
|
||||
voucherInfo.setAvailable(false);
|
||||
Date now = new Date();
|
||||
if (batchConfig.getValidStartTime() != null && now.before(batchConfig.getValidStartTime())) {
|
||||
voucherInfo.setUnavailableReason("券码尚未生效");
|
||||
} else if (batchConfig.getValidEndTime() != null && now.after(batchConfig.getValidEndTime())) {
|
||||
voucherInfo.setUnavailableReason("券码已过期");
|
||||
} else {
|
||||
voucherInfo.setUnavailableReason("券码不在有效期内");
|
||||
}
|
||||
return voucherInfo;
|
||||
}
|
||||
|
||||
// 验证景区匹配
|
||||
if (scenicId != null && !scenicId.equals(voucherCodeEntity.getScenicId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoucherInfo voucherInfo = buildVoucherInfo(voucherCodeEntity, batchConfig);
|
||||
|
||||
// 检查券码状态和可用性
|
||||
// 检查券码状态和可用性,包含完整的重复使用权限验证
|
||||
if (VoucherCodeStatus.UNCLAIMED.getCode().equals(voucherCodeEntity.getStatus())) {
|
||||
// 未领取状态,检查是否可以领取
|
||||
if (faceId != null) { // && canClaimVoucher(faceId, voucherCodeEntity.getScenicId())
|
||||
voucherInfo.setAvailable(true);
|
||||
} else {
|
||||
// 未领取状态,也需要检查用户的重复使用权限
|
||||
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,24 +188,89 @@ public class VoucherServiceImpl implements IVoucherService {
|
||||
|
||||
return voucherInfos;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void markVoucherAsUsed(String voucherCode, String remark) {
|
||||
if (!StringUtils.hasText(voucherCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int result = voucherCodeMapper.useVoucher(voucherCode, LocalDateTime.now(), remark);
|
||||
if (result > 0) {
|
||||
// 更新批次统计
|
||||
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
|
||||
if (voucherCodeEntity != null) {
|
||||
voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1);
|
||||
}
|
||||
log.info("券码已标记为使用: {}", voucherCode);
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 计算新的使用次数和序号
|
||||
Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ?
|
||||
voucherCodeEntity.getCurrentUseCount() : 0;
|
||||
Integer newUseCount = currentUseCount + 1;
|
||||
|
||||
// 创建使用记录
|
||||
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.setUsageSequence(newUseCount); // 设置使用序号,表示这是该券码的第几次使用
|
||||
usageRecord.setUseTime(now);
|
||||
usageRecord.setOrderId(orderId);
|
||||
usageRecord.setDiscountAmount(discountAmount);
|
||||
usageRecord.setRemark(remark);
|
||||
usageRecord.setCreateTime(now);
|
||||
usageRecord.setDeleted(0);
|
||||
|
||||
usageRecordMapper.insert(usageRecord);
|
||||
|
||||
// 更新券码使用次数和状态
|
||||
voucherCodeEntity.setCurrentUseCount(newUseCount);
|
||||
voucherCodeEntity.setLastUsedTime(now);
|
||||
|
||||
// 检查是否达到最大使用次数
|
||||
Integer maxUseCount = batchConfig.getMaxUseCount();
|
||||
if (maxUseCount != null && newUseCount >= 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={}, sequence={}",
|
||||
voucherCode, newUseCount, maxUseCount, faceId, newUseCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
||||
if (faceId == null || scenicId == null) {
|
||||
@@ -236,6 +383,232 @@ public class VoucherServiceImpl implements IVoucherService {
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherDetailResp getVoucherDetail(String voucherCode, Long faceId) {
|
||||
if (!StringUtils.hasText(voucherCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
|
||||
if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId());
|
||||
if (batchConfig == null || batchConfig.getDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoucherDetailResp detail = new VoucherDetailResp();
|
||||
detail.setVoucherCode(voucherCode);
|
||||
|
||||
// 设置批次信息
|
||||
VoucherDetailResp.BatchInfo batchInfo = new VoucherDetailResp.BatchInfo();
|
||||
batchInfo.setBatchId(batchConfig.getId());
|
||||
batchInfo.setBatchName(batchConfig.getBatchName());
|
||||
batchInfo.setDiscountType(batchConfig.getDiscountType());
|
||||
batchInfo.setDiscountValue(batchConfig.getDiscountValue());
|
||||
batchInfo.setApplicableProducts(batchConfig.getApplicableProducts());
|
||||
detail.setBatchInfo(batchInfo);
|
||||
|
||||
// 设置使用信息
|
||||
VoucherDetailResp.UsageInfo usageInfo = new VoucherDetailResp.UsageInfo();
|
||||
Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ? voucherCodeEntity.getCurrentUseCount() : 0;
|
||||
usageInfo.setMaxUseCount(batchConfig.getMaxUseCount());
|
||||
usageInfo.setCurrentUseCount(currentUseCount);
|
||||
usageInfo.setMaxUsePerUser(batchConfig.getMaxUsePerUser());
|
||||
usageInfo.setUseIntervalHours(batchConfig.getUseIntervalHours());
|
||||
|
||||
// 计算剩余使用次数和是否还能使用
|
||||
if (batchConfig.getMaxUseCount() != null) {
|
||||
int remaining = batchConfig.getMaxUseCount() - currentUseCount;
|
||||
usageInfo.setRemainingUseCount(Math.max(0, remaining));
|
||||
usageInfo.setCanUseMore(currentUseCount < batchConfig.getMaxUseCount());
|
||||
} else {
|
||||
usageInfo.setRemainingUseCount(null);
|
||||
usageInfo.setCanUseMore(true);
|
||||
}
|
||||
detail.setUsageInfo(usageInfo);
|
||||
|
||||
// 设置用户信息
|
||||
if (faceId != null) {
|
||||
VoucherDetailResp.UserInfo userInfo = new VoucherDetailResp.UserInfo();
|
||||
userInfo.setFaceId(faceId);
|
||||
|
||||
// 计算该用户使用此券码的次数
|
||||
List<PriceVoucherUsageRecord> userUsageRecords = usageRecordMapper.selectByVoucherCodeAndFaceId(voucherCodeEntity.getId(), faceId);
|
||||
userInfo.setUserUsageCount(userUsageRecords.size());
|
||||
|
||||
if (voucherCodeEntity.getLastUsedTime() != null) {
|
||||
userInfo.setLastUsedTime(voucherCodeEntity.getLastUsedTime());
|
||||
|
||||
// 计算下次可用时间
|
||||
if (batchConfig.getUseIntervalHours() != null) {
|
||||
Date nextAvailableTime = new Date(voucherCodeEntity.getLastUsedTime().getTime() +
|
||||
batchConfig.getUseIntervalHours() * 60 * 60 * 1000L);
|
||||
userInfo.setNextAvailableTime(nextAvailableTime);
|
||||
}
|
||||
}
|
||||
detail.setUserInfo(userInfo);
|
||||
}
|
||||
|
||||
// 设置状态信息
|
||||
VoucherDetailResp.StatusInfo statusInfo = new VoucherDetailResp.StatusInfo();
|
||||
statusInfo.setStatus(voucherCodeEntity.getStatus());
|
||||
VoucherCodeStatus statusEnum = VoucherCodeStatus.getByCode(voucherCodeEntity.getStatus());
|
||||
if (statusEnum != null) {
|
||||
statusInfo.setStatusName(statusEnum.getName());
|
||||
}
|
||||
statusInfo.setClaimedTime(voucherCodeEntity.getClaimedTime());
|
||||
detail.setStatusInfo(statusInfo);
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherUsageSummaryResp getVoucherUsageSummary(String voucherCode) {
|
||||
if (!StringUtils.hasText(voucherCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PriceVoucherCode voucherCodeEntity = voucherCodeMapper.selectByCode(voucherCode);
|
||||
if (voucherCodeEntity == null || voucherCodeEntity.getDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(voucherCodeEntity.getBatchId());
|
||||
if (batchConfig == null || batchConfig.getDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoucherUsageSummaryResp summary = new VoucherUsageSummaryResp();
|
||||
summary.setVoucherCode(voucherCode);
|
||||
summary.setBatchId(batchConfig.getId());
|
||||
summary.setBatchName(batchConfig.getBatchName());
|
||||
|
||||
// 获取使用记录
|
||||
List<PriceVoucherUsageRecord> usageRecords = usageRecordMapper.selectByVoucherCodeId(voucherCodeEntity.getId());
|
||||
summary.setTotalUsageRecords(usageRecords.size());
|
||||
|
||||
// 计算独立用户数
|
||||
long uniqueUsers = usageRecords.stream()
|
||||
.mapToLong(PriceVoucherUsageRecord::getFaceId)
|
||||
.distinct()
|
||||
.count();
|
||||
summary.setUniqueUsers((int) uniqueUsers);
|
||||
|
||||
// 设置使用统计
|
||||
VoucherUsageSummaryResp.UsageStatistics statistics = new VoucherUsageSummaryResp.UsageStatistics();
|
||||
Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ? voucherCodeEntity.getCurrentUseCount() : 0;
|
||||
statistics.setMaxUseCount(batchConfig.getMaxUseCount());
|
||||
statistics.setCurrentUseCount(currentUseCount);
|
||||
|
||||
if (batchConfig.getMaxUseCount() != null) {
|
||||
int remaining = batchConfig.getMaxUseCount() - currentUseCount;
|
||||
statistics.setRemainingUseCount(Math.max(0, remaining));
|
||||
statistics.setCanUseMore(currentUseCount < batchConfig.getMaxUseCount());
|
||||
|
||||
// 计算使用率
|
||||
if (batchConfig.getMaxUseCount() > 0) {
|
||||
BigDecimal usageRate = new BigDecimal(currentUseCount)
|
||||
.divide(new BigDecimal(batchConfig.getMaxUseCount()), 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal(100));
|
||||
statistics.setUsageRate(usageRate);
|
||||
}
|
||||
} else {
|
||||
statistics.setRemainingUseCount(null);
|
||||
statistics.setCanUseMore(true);
|
||||
statistics.setUsageRate(null);
|
||||
}
|
||||
summary.setUsageStatistics(statistics);
|
||||
|
||||
// 设置使用时间轴
|
||||
List<VoucherUsageSummaryResp.UsageTimeline> timeline = usageRecords.stream()
|
||||
.map(record -> {
|
||||
VoucherUsageSummaryResp.UsageTimeline item = new VoucherUsageSummaryResp.UsageTimeline();
|
||||
item.setUseTime(record.getUseTime());
|
||||
item.setFaceId(record.getFaceId());
|
||||
item.setOrderId(record.getOrderId());
|
||||
item.setDiscountAmount(record.getDiscountAmount());
|
||||
return item;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
summary.setUsageTimeline(timeline);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherBatchOverviewResp getBatchOverview(Long batchId) {
|
||||
if (batchId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PriceVoucherBatchConfig batchConfig = voucherBatchConfigMapper.selectById(batchId);
|
||||
if (batchConfig == null || batchConfig.getDeleted() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoucherBatchOverviewResp overview = new VoucherBatchOverviewResp();
|
||||
overview.setBatchId(batchId);
|
||||
overview.setBatchName(batchConfig.getBatchName());
|
||||
overview.setTotalCodes(batchConfig.getTotalCount());
|
||||
overview.setClaimedCodes(batchConfig.getClaimedCount());
|
||||
|
||||
// 查询批次下的券码统计
|
||||
List<PriceVoucherCode> allCodes = voucherCodeMapper.selectByBatchId(batchId);
|
||||
|
||||
// 计算已使用券码数(至少使用过一次)
|
||||
int usedCodes = (int) allCodes.stream()
|
||||
.filter(code -> code.getCurrentUseCount() != null && code.getCurrentUseCount() > 0)
|
||||
.count();
|
||||
overview.setUsedCodes(usedCodes);
|
||||
|
||||
// 计算已达到最大使用次数的券码数
|
||||
int exhaustedCodes = 0;
|
||||
if (batchConfig.getMaxUseCount() != null) {
|
||||
exhaustedCodes = (int) allCodes.stream()
|
||||
.filter(code -> code.getCurrentUseCount() != null &&
|
||||
code.getCurrentUseCount() >= batchConfig.getMaxUseCount())
|
||||
.count();
|
||||
}
|
||||
overview.setExhaustedCodes(exhaustedCodes);
|
||||
|
||||
// 设置总使用记录数
|
||||
overview.setTotalUsageRecords(batchConfig.getUsedCount());
|
||||
|
||||
// 计算平均每个券码使用次数
|
||||
if (usedCodes > 0) {
|
||||
BigDecimal averageUsage = new BigDecimal(batchConfig.getUsedCount())
|
||||
.divide(new BigDecimal(usedCodes), 2, RoundingMode.HALF_UP);
|
||||
overview.setAverageUsagePerCode(averageUsage);
|
||||
} else {
|
||||
overview.setAverageUsagePerCode(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
// 计算重复使用率
|
||||
if (usedCodes > 0) {
|
||||
int repeatUsedCodes = (int) allCodes.stream()
|
||||
.filter(code -> code.getCurrentUseCount() != null && code.getCurrentUseCount() > 1)
|
||||
.count();
|
||||
BigDecimal repeatUsageRate = new BigDecimal(repeatUsedCodes)
|
||||
.divide(new BigDecimal(usedCodes), 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal(100));
|
||||
overview.setRepeatUsageRate(repeatUsageRate);
|
||||
} else {
|
||||
overview.setRepeatUsageRate(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
// 设置可重复使用配置
|
||||
VoucherBatchOverviewResp.RepeatableSettings settings = new VoucherBatchOverviewResp.RepeatableSettings();
|
||||
settings.setMaxUseCount(batchConfig.getMaxUseCount());
|
||||
settings.setMaxUsePerUser(batchConfig.getMaxUsePerUser());
|
||||
settings.setUseIntervalHours(batchConfig.getUseIntervalHours());
|
||||
overview.setRepeatableSettings(settings);
|
||||
|
||||
return overview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherInfo getBestVoucher(Long faceId, Long scenicId, DiscountDetectionContext context) {
|
||||
List<VoucherInfo> availableVouchers = getAvailableVouchers(faceId, scenicId);
|
||||
@@ -260,6 +633,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 +724,33 @@ 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());
|
||||
|
||||
// 设置时间范围信息
|
||||
voucherInfo.setValidStartTime(batchConfig.getValidStartTime());
|
||||
voucherInfo.setValidEndTime(batchConfig.getValidEndTime());
|
||||
|
||||
// 计算剩余可使用次数
|
||||
if (batchConfig.getMaxUseCount() != null) {
|
||||
int remaining = batchConfig.getMaxUseCount() - currentUseCount;
|
||||
voucherInfo.setRemainingUseCount(Math.max(0, remaining));
|
||||
} else {
|
||||
voucherInfo.setRemainingUseCount(null); // 无限次
|
||||
}
|
||||
|
||||
// 计算是否还能使用
|
||||
boolean canUseMore = true;
|
||||
if (batchConfig.getMaxUseCount() != null) {
|
||||
canUseMore = currentUseCount < batchConfig.getMaxUseCount();
|
||||
}
|
||||
voucherInfo.setCanUseMore(canUseMore);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取景区名称
|
||||
|
@@ -2,6 +2,7 @@ package com.ycwl.basic.service.mobile.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
|
||||
import com.ycwl.basic.utils.JacksonUtil;
|
||||
import com.ycwl.basic.biz.CouponBiz;
|
||||
@@ -147,27 +148,32 @@ public class GoodsServiceImpl implements GoodsService {
|
||||
sourceReqQuery.setMemberId(Long.valueOf(BaseContextHandler.getUserId()));
|
||||
//查询源素材
|
||||
List<SourceRespVO> sourceList = sourceMapper.queryByRelation(sourceReqQuery);
|
||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||
List<GoodsPageVO> sourceGoods = sourceList.stream().collect(Collectors.groupingBy(SourceRespVO::getFaceId)).entrySet().stream().flatMap((faceEntry) -> {
|
||||
Long faceId = faceEntry.getKey();
|
||||
List<SourceRespVO> goods = faceEntry.getValue();
|
||||
return goods.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).keySet().stream().filter(type -> {
|
||||
if (Integer.valueOf(1).equals(type)) {
|
||||
return !Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo());
|
||||
return !Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_video"));
|
||||
} else if (Integer.valueOf(2).equals(type)) {
|
||||
return !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage());
|
||||
return !Boolean.TRUE.equals(scenicConfig.getBoolean("disable_source_image"));
|
||||
}
|
||||
return true;
|
||||
}).map(type -> {
|
||||
GoodsPageVO goodsPageVO = new GoodsPageVO();
|
||||
goodsPageVO.setTemplateCoverUrl(goods.getFirst().getUrl());
|
||||
goodsPageVO.setFaceId(faceId);
|
||||
goodsPageVO.setGoodsType(type);
|
||||
if (type == 1) {
|
||||
goodsPageVO.setGoodsName("录像集");
|
||||
goodsPageVO.setGoodsType(1);
|
||||
} else {
|
||||
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("video_cover_url"));
|
||||
} else if (type == 2) {
|
||||
goodsPageVO.setGoodsName("照片集");
|
||||
goodsPageVO.setGoodsType(2);
|
||||
goodsPageVO.setTemplateCoverUrl(scenicConfig.getString("photo_cover_url"));
|
||||
} else {
|
||||
goodsPageVO.setGoodsName("未知商品");
|
||||
}
|
||||
if (StringUtils.isBlank(goodsPageVO.getTemplateCoverUrl())) {
|
||||
goodsPageVO.setTemplateCoverUrl(goods.getFirst().getUrl());
|
||||
}
|
||||
goodsPageVO.setScenicId(query.getScenicId());
|
||||
return goodsPageVO;
|
||||
|
@@ -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());
|
||||
|
@@ -7,10 +7,13 @@ import com.ycwl.basic.model.pc.project.entity.ProjectEntity;
|
||||
import com.ycwl.basic.model.pc.project.req.ProjectReqQuery;
|
||||
import com.ycwl.basic.model.pc.project.resp.ProjectRespVO;
|
||||
import com.ycwl.basic.repository.ScenicRepository;
|
||||
import com.ycwl.basic.repository.TemplateRepository;
|
||||
import com.ycwl.basic.service.pc.ProjectService;
|
||||
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -30,6 +33,9 @@ public class ProjectServiceImpl implements ProjectService {
|
||||
|
||||
@Autowired
|
||||
private ScenicRepository scenicRepository;
|
||||
|
||||
@Autowired
|
||||
private TemplateRepository templateRepository;
|
||||
|
||||
@Override
|
||||
public PageInfo<ProjectRespVO> pageQuery(ProjectReqQuery projectReqQuery) {
|
||||
@@ -44,11 +50,28 @@ public class ProjectServiceImpl implements ProjectService {
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
|
||||
|
||||
// 设置景区名称
|
||||
// 批量获取模板名称
|
||||
List<Long> templateIds = list.stream()
|
||||
.map(ProjectRespVO::getTemplateId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, String> templateNames = new HashMap<>();
|
||||
for (Long templateId : templateIds) {
|
||||
TemplateRespVO template = templateRepository.getTemplate(templateId);
|
||||
if (template != null) {
|
||||
templateNames.put(templateId, template.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 设置景区名称和模板名称
|
||||
list.forEach(item -> {
|
||||
if (item.getScenicId() != null) {
|
||||
item.setScenicName(scenicNames.get(item.getScenicId()));
|
||||
}
|
||||
if (item.getTemplateId() != null) {
|
||||
item.setTemplateName(templateNames.get(item.getTemplateId()));
|
||||
}
|
||||
});
|
||||
|
||||
PageInfo<ProjectRespVO> pageInfo = new PageInfo(list);
|
||||
@@ -67,11 +90,28 @@ public class ProjectServiceImpl implements ProjectService {
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
|
||||
|
||||
// 设置景区名称
|
||||
// 批量获取模板名称
|
||||
List<Long> templateIds = list.stream()
|
||||
.map(ProjectRespVO::getTemplateId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, String> templateNames = new HashMap<>();
|
||||
for (Long templateId : templateIds) {
|
||||
TemplateRespVO template = templateRepository.getTemplate(templateId);
|
||||
if (template != null) {
|
||||
templateNames.put(templateId, template.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 设置景区名称和模板名称
|
||||
list.forEach(item -> {
|
||||
if (item.getScenicId() != null) {
|
||||
item.setScenicName(scenicNames.get(item.getScenicId()));
|
||||
}
|
||||
if (item.getTemplateId() != null) {
|
||||
item.setTemplateName(templateNames.get(item.getTemplateId()));
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
@@ -79,7 +119,22 @@ public class ProjectServiceImpl implements ProjectService {
|
||||
|
||||
@Override
|
||||
public ProjectRespVO getById(Long id) {
|
||||
return projectMapper.getById(id);
|
||||
ProjectRespVO project = projectMapper.getById(id);
|
||||
if (project != null) {
|
||||
// 设置景区名称
|
||||
if (project.getScenicId() != null) {
|
||||
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(List.of(project.getScenicId()));
|
||||
project.setScenicName(scenicNames.get(project.getScenicId()));
|
||||
}
|
||||
// 设置模板名称
|
||||
if (project.getTemplateId() != null) {
|
||||
TemplateRespVO template = templateRepository.getTemplate(project.getTemplateId());
|
||||
if (template != null) {
|
||||
project.setTemplateName(template.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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>
|
||||
|
@@ -11,6 +11,8 @@
|
||||
<result column="face_id" property="faceId" jdbcType="BIGINT"/>
|
||||
<result column="claimed_time" property="claimedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="used_time" property="usedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="current_use_count" property="currentUseCount" jdbcType="INTEGER"/>
|
||||
<result column="last_used_time" property="lastUsedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="remark" property="remark" jdbcType="VARCHAR"/>
|
||||
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
|
||||
@@ -20,7 +22,7 @@
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time,
|
||||
remark, create_time, update_time, deleted, deleted_at
|
||||
current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at
|
||||
</sql>
|
||||
|
||||
<select id="selectByCode" resultMap="BaseResultMap">
|
||||
@@ -69,16 +71,7 @@
|
||||
AND deleted = 0
|
||||
</update>
|
||||
|
||||
<update id="useVoucher">
|
||||
UPDATE price_voucher_code
|
||||
SET status = 2,
|
||||
used_time = #{usedTime},
|
||||
remark = #{remark},
|
||||
update_time = NOW()
|
||||
WHERE code = #{code}
|
||||
AND (status = 1 OR status = 0)
|
||||
AND deleted = 0
|
||||
</update>
|
||||
|
||||
|
||||
<select id="selectByBatchId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
|
@@ -2,8 +2,8 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ycwl.basic.mapper.ProjectMapper">
|
||||
<insert id="add">
|
||||
insert into project(scenic_id, `name`, min_play_time, max_play_time, status, create_at, update_at)
|
||||
values (#{scenicId}, #{name}, #{minPlayTime}, #{maxPlayTime}, #{status}, now(), now())
|
||||
insert into project(scenic_id, `name`, min_play_time, max_play_time, status, template_id, create_at, update_at)
|
||||
values (#{scenicId}, #{name}, #{minPlayTime}, #{maxPlayTime}, #{status}, #{templateId}, now(), now())
|
||||
</insert>
|
||||
|
||||
<update id="update">
|
||||
@@ -12,6 +12,7 @@
|
||||
min_play_time = #{minPlayTime},
|
||||
max_play_time = #{maxPlayTime},
|
||||
status = #{status},
|
||||
template_id = #{templateId},
|
||||
update_at = now()
|
||||
where id = #{id}
|
||||
</update>
|
||||
@@ -35,7 +36,7 @@
|
||||
</delete>
|
||||
|
||||
<select id="list" resultType="com.ycwl.basic.model.pc.project.resp.ProjectRespVO">
|
||||
select p.id, p.scenic_id, p.`name`, p.min_play_time, p.max_play_time, p.status, p.create_at, p.update_at
|
||||
select p.id, p.scenic_id, p.`name`, p.min_play_time, p.max_play_time, p.status, p.template_id, p.create_at, p.update_at
|
||||
from project p
|
||||
<where>
|
||||
<if test="scenicId != null">
|
||||
@@ -47,12 +48,15 @@
|
||||
<if test="status != null">
|
||||
and p.`status` = #{status}
|
||||
</if>
|
||||
<if test="templateId != null">
|
||||
and p.template_id = #{templateId}
|
||||
</if>
|
||||
</where>
|
||||
order by p.create_at desc
|
||||
</select>
|
||||
|
||||
<select id="getById" resultType="com.ycwl.basic.model.pc.project.resp.ProjectRespVO">
|
||||
select p.id, p.scenic_id, p.`name`, p.min_play_time, p.max_play_time, p.status, p.create_at, p.update_at
|
||||
select p.id, p.scenic_id, p.`name`, p.min_play_time, p.max_play_time, p.status, p.template_id, p.create_at, p.update_at
|
||||
from project p
|
||||
where p.id = #{id}
|
||||
</select>
|
||||
|
Reference in New Issue
Block a user