diff --git a/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java index 88f7ce89..3fc10e6d 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/VoucherInfo.java @@ -114,9 +114,54 @@ public class VoucherInfo { * 剩余可使用次数 */ 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()); + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReqV2.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReqV2.java index 42a05fe6..159adb1e 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReqV2.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherBatchCreateReqV2.java @@ -7,6 +7,7 @@ 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; /** @@ -70,4 +71,14 @@ public class VoucherBatchCreateReqV2 { * 两次使用间隔小时数(NULL表示无间隔限制) */ private Integer useIntervalHours; + + /** + * 有效期开始时间(NULL表示无开始时间限制) + */ + private Date validStartTime; + + /** + * 有效期结束时间(NULL表示无结束时间限制) + */ + private Date validEndTime; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchOverviewResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchOverviewResp.java new file mode 100644 index 00000000..7644f1a9 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchOverviewResp.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java index 684d31e8..06059a0b 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherBatchResp.java @@ -30,4 +30,14 @@ public class VoucherBatchResp { * null表示适用所有商品类型 */ private List applicableProducts; + + /** + * 有效期开始时间 + */ + private Date validStartTime; + + /** + * 有效期结束时间 + */ + private Date validEndTime; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java index b7f0f8ad..ee9de462 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherCodeResp.java @@ -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; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherDetailResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherDetailResp.java new file mode 100644 index 00000000..62ec1d7d --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherDetailResp.java @@ -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 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageRecordResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageRecordResp.java index 59b75a90..f9324710 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageRecordResp.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageRecordResp.java @@ -47,6 +47,11 @@ public class VoucherUsageRecordResp { */ private String batchName; + /** + * 使用序号(该券码的第几次使用) + */ + private Integer usageSequence; + /** * 使用时间 */ diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageSummaryResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageSummaryResp.java new file mode 100644 index 00000000..811687a2 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherUsageSummaryResp.java @@ -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; + + @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; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherValidationResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherValidationResp.java new file mode 100644 index 00000000..8cbfcccc --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherValidationResp.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java index 99c74f47..93256ab5 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherBatchConfig.java @@ -115,6 +115,49 @@ public class PriceVoucherBatchConfig { 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; + } + /** * 获取适用商品类型列表 */ diff --git a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherUsageRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherUsageRecord.java index b8c8c937..288851ef 100644 --- a/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherUsageRecord.java +++ b/src/main/java/com/ycwl/basic/pricing/entity/PriceVoucherUsageRecord.java @@ -44,6 +44,11 @@ public class PriceVoucherUsageRecord { */ private Long batchId; + /** + * 使用序号(该券码的第几次使用) + */ + private Integer usageSequence; + /** * 使用时间 */ diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java index d7b00483..2ed8a99f 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java @@ -58,17 +58,6 @@ public interface PriceVoucherCodeMapper extends BaseMapper { @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 diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java index 66dc111e..01a58c1a 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherUsageRecordMapper.java @@ -33,6 +33,17 @@ public interface PriceVoucherUsageRecordMapper extends BaseMapper 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 selectByVoucherCodeAndFaceId(@Param("voucherCodeId") Long voucherCodeId, @Param("faceId") Long faceId); /** * 根据用户和景区查询使用记录 diff --git a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java index 4f05658c..ee2179ee 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IVoucherService.java @@ -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; @@ -61,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 diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java index 383dc2c8..8cc1ca7d 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherBatchServiceImpl.java @@ -118,6 +118,13 @@ public class VoucherBatchServiceImpl implements VoucherBatchService { 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); diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java index d1a90a65..818d01db 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java @@ -49,74 +49,79 @@ public class VoucherCodeServiceImpl implements VoucherCodeService { private VoucherBatchService voucherBatchService; @Override - public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) { - List 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 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 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 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 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()); diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java index c5612a5b..c83f7996 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherServiceImpl.java @@ -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; @@ -56,13 +85,27 @@ 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())) { // 未领取状态,也需要检查用户的重复使用权限 @@ -152,78 +195,82 @@ public class VoucherServiceImpl implements IVoucherService { } /** - * 标记券码为已使用(支持可重复使用) - * - * @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(); - - // 创建使用记录 - PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord(); - usageRecord.setVoucherCodeId(voucherCodeEntity.getId()); - usageRecord.setVoucherCode(voucherCode); - usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getFaceId()); - usageRecord.setScenicId(voucherCodeEntity.getScenicId()); - usageRecord.setBatchId(voucherCodeEntity.getBatchId()); - usageRecord.setUseTime(now); - usageRecord.setOrderId(orderId); - usageRecord.setDiscountAmount(discountAmount); - usageRecord.setRemark(remark); - usageRecord.setCreateTime(now); - usageRecord.setDeleted(0); - - usageRecordMapper.insert(usageRecord); - - // 更新券码使用次数和状态 - Integer currentUseCount = voucherCodeEntity.getCurrentUseCount() != null ? - voucherCodeEntity.getCurrentUseCount() + 1 : 1; - voucherCodeEntity.setCurrentUseCount(currentUseCount); - voucherCodeEntity.setLastUsedTime(now); - - // 检查是否达到最大使用次数 - Integer maxUseCount = batchConfig.getMaxUseCount(); - if (maxUseCount != null && currentUseCount >= maxUseCount) { - if (maxUseCount == 1) { - // 兼容原有逻辑,单次使用设为USED状态 - voucherCodeEntity.setStatus(VoucherCodeStatus.USED.getCode()); - voucherCodeEntity.setUsedTime(now); - } else { - // 多次使用达到上限设为CLAIMED_EXHAUSTED状态 - voucherCodeEntity.setStatus(VoucherCodeStatus.CLAIMED_EXHAUSTED.getCode()); - } - } - - voucherCodeMapper.updateById(voucherCodeEntity); - - // 更新批次统计(使用记录数,不是使用券码数) - voucherBatchConfigMapper.updateUsedCount(voucherCodeEntity.getBatchId(), 1); - - log.info("券码使用记录已创建: code={}, useCount={}, maxUseCount={}, faceId={}", - voucherCode, currentUseCount, maxUseCount, faceId); + * 标记券码为已使用(支持可重复使用) + * + * @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) { @@ -336,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 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 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 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 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 availableVouchers = getAvailableVouchers(faceId, scenicId); @@ -459,6 +732,10 @@ public class VoucherServiceImpl implements IVoucherService { 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; @@ -467,6 +744,13 @@ public class VoucherServiceImpl implements IVoucherService { voucherInfo.setRemainingUseCount(null); // 无限次 } + // 计算是否还能使用 + boolean canUseMore = true; + if (batchConfig.getMaxUseCount() != null) { + canUseMore = currentUseCount < batchConfig.getMaxUseCount(); + } + voucherInfo.setCanUseMore(canUseMore); + return voucherInfo; } diff --git a/src/main/resources/mapper/PriceVoucherCodeMapper.xml b/src/main/resources/mapper/PriceVoucherCodeMapper.xml index d3bc5293..ea20b164 100644 --- a/src/main/resources/mapper/PriceVoucherCodeMapper.xml +++ b/src/main/resources/mapper/PriceVoucherCodeMapper.xml @@ -11,6 +11,8 @@ + + @@ -20,7 +22,7 @@ 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 SELECT