package com.talentcard.server.service;
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import tools.jackson.core.type.TypeReference;
|
import tools.jackson.databind.ObjectMapper;
|
import com.talentcard.server.business.BusinessIdGenerator;
|
import com.talentcard.server.business.DatabaseRows;
|
import com.talentcard.server.business.PageResult;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import com.talentcard.server.entity.MerchantCouponClaim;
|
import com.talentcard.server.entity.MerchantCouponManagement;
|
import com.talentcard.server.mapper.MerchantCouponClaimMapper;
|
import com.talentcard.server.mapper.MerchantCouponManagementMapper;
|
import com.talentcard.server.request.CouponClaimIssueRequest;
|
import com.talentcard.server.request.CouponClaimStatusRequest;
|
import com.talentcard.server.request.CouponManagementSaveRequest;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.LocalTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.UUID;
|
|
@Service
|
public class MerchantCouponManagementService {
|
private static final List<String> COUPON_TYPES = List.of("CASH", "FULL_REDUCTION", "DISCOUNT", "EXCHANGE");
|
|
private final MerchantCouponManagementMapper couponMapper;
|
private final MerchantCouponClaimMapper claimMapper;
|
|
private final ObjectMapper objectMapper;
|
|
public MerchantCouponManagementService(MerchantCouponManagementMapper couponMapper,
|
MerchantCouponClaimMapper claimMapper,
|
ObjectMapper objectMapper) {
|
this.couponMapper = couponMapper;
|
this.claimMapper = claimMapper;
|
this.objectMapper = objectMapper;
|
}
|
|
@Transactional(readOnly = true)
|
public PageResult<Map<String, Object>> coupons(Long merchantId, String couponType, String batchNo,
|
Integer status, String keyword, LocalDate validOn,
|
int page, int size) {
|
String type = normalizeTypeFilter(couponType);
|
validateCouponStatus(status);
|
int safePage = Math.max(page, 1);
|
int safeSize = Math.min(Math.max(size, 1), 100);
|
List<Map<String, Object>> records = DatabaseRows.normalize(couponMapper.selectManagementPage(
|
merchantId, type, clean(batchNo), status, clean(keyword), validOn,
|
safeSize, (safePage - 1) * safeSize)).stream().map(this::couponView).toList();
|
Long total = couponMapper.countManagementPage(merchantId, type, clean(batchNo), status, clean(keyword), validOn);
|
return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> coupon(Long id) {
|
return couponView(toMap(requireCoupon(id)));
|
}
|
|
@Transactional
|
public Map<String, Object> createCoupon(CouponManagementSaveRequest request) {
|
validateSave(request, null);
|
MerchantCouponManagement coupon = new MerchantCouponManagement();
|
apply(coupon, request);
|
coupon.setBatchNo(clean(request.batchNo()).isBlank() ? nextBatchNo() : clean(request.batchNo()));
|
coupon.setClaimedQuantity(0);
|
coupon.setUseCount(0);
|
coupon.setProductScope("ALL");
|
coupon.setStoreConsume(1);
|
coupon.setDeleted(0);
|
if (couponMapper.insert(coupon) != 1) {
|
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "优惠券配置保存失败");
|
}
|
return coupon(coupon.getId());
|
}
|
|
@Transactional
|
public Map<String, Object> updateCoupon(Long id, CouponManagementSaveRequest request) {
|
MerchantCouponManagement current = requireCoupon(id);
|
validateSave(request, current);
|
String requestedBatch = clean(request.batchNo());
|
if (current.getClaimedQuantity() != null && current.getClaimedQuantity() > 0
|
&& !requestedBatch.isBlank() && !requestedBatch.equals(current.getBatchNo())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "已有领取记录时不能修改发放批次");
|
}
|
apply(current, request);
|
if (!requestedBatch.isBlank()) current.setBatchNo(requestedBatch);
|
if (couponMapper.updateById(current) != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "优惠券配置已变化,请刷新后重试");
|
}
|
return coupon(id);
|
}
|
|
@Transactional
|
public Map<String, Object> updateCouponStatus(Long id, Integer status) {
|
validateCouponStatus(status);
|
MerchantCouponManagement coupon = requireCoupon(id);
|
coupon.setStatus(status);
|
if (couponMapper.updateById(coupon) != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "优惠券状态更新失败");
|
}
|
return coupon(id);
|
}
|
|
@Transactional
|
public Map<String, Object> issue(Long couponId, CouponClaimIssueRequest request) {
|
MerchantCouponManagement coupon = requireCoupon(couponId);
|
LocalDate today = LocalDate.now();
|
if (!Integer.valueOf(1).equals(coupon.getStatus())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "优惠券已下架,不能发放");
|
}
|
if (coupon.getValidStart() != null && coupon.getValidStart().isAfter(today)) {
|
throw new BusinessException(ErrorCode.CONFLICT, "优惠券尚未开始生效");
|
}
|
if (coupon.getValidEnd() != null && coupon.getValidEnd().isBefore(today)) {
|
throw new BusinessException(ErrorCode.CONFLICT, "优惠券已过期");
|
}
|
if (couponMapper.countTalent(request.talentId()) == 0) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "领取人才不存在");
|
}
|
if (request.cardId() != null && couponMapper.countTalentCard(request.cardId(), request.talentId()) == 0) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "人才卡不属于领取人");
|
}
|
Long duplicate = claimMapper.selectCount(Wrappers.<MerchantCouponClaim>lambdaQuery()
|
.eq(MerchantCouponClaim::getCouponId, couponId)
|
.eq(MerchantCouponClaim::getTalentId, request.talentId())
|
.eq(MerchantCouponClaim::getBatchNo, coupon.getBatchNo()));
|
if (duplicate != null && duplicate > 0) {
|
throw new BusinessException(ErrorCode.CONFLICT, "该用户已领取当前批次优惠券");
|
}
|
if (couponMapper.reserveOne(couponId) != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "优惠券已下架或发放数量不足");
|
}
|
MerchantCouponClaim claim = new MerchantCouponClaim();
|
claim.setClaimNo(BusinessIdGenerator.next("MCC"));
|
claim.setCouponId(couponId);
|
claim.setMerchantId(coupon.getMerchantId());
|
claim.setBatchNo(coupon.getBatchNo());
|
claim.setTalentId(request.talentId());
|
claim.setCardId(request.cardId());
|
claim.setClaimStatus(1);
|
claim.setUseStatus(0);
|
claim.setClaimedAt(LocalDateTime.now());
|
claim.setExpireTime(request.expireTime() != null ? request.expireTime()
|
: coupon.getValidEnd() == null ? null : LocalDateTime.of(coupon.getValidEnd(), LocalTime.MAX));
|
claim.setRemark(trimToNull(request.remark()));
|
claim.setDeleted(0);
|
if (claimMapper.insert(claim) != 1) {
|
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "优惠券发放记录保存失败");
|
}
|
return claim(claim.getId());
|
}
|
|
@Transactional(readOnly = true)
|
public PageResult<Map<String, Object>> claims(Long couponId, Long merchantId, String batchNo,
|
Integer claimStatus, Integer useStatus, String keyword,
|
LocalDate startDate, LocalDate endDate, int page, int size) {
|
validateClaimStatus(claimStatus);
|
validateUseStatus(useStatus);
|
if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "开始日期不能晚于结束日期");
|
}
|
int safePage = Math.max(page, 1);
|
int safeSize = Math.min(Math.max(size, 1), 100);
|
LocalDateTime start = startDate == null ? null : startDate.atStartOfDay();
|
LocalDateTime end = endDate == null ? null : endDate.plusDays(1).atStartOfDay();
|
List<Map<String, Object>> records = DatabaseRows.normalize(claimMapper.selectClaimPage(
|
couponId, merchantId, clean(batchNo), claimStatus, useStatus, clean(keyword),
|
start, end, safeSize, (safePage - 1) * safeSize)).stream().map(this::claimView).toList();
|
Long total = claimMapper.countClaimPage(couponId, merchantId, clean(batchNo), claimStatus,
|
useStatus, clean(keyword), start, end);
|
return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> claim(Long id) {
|
MerchantCouponClaim claim = requireClaim(id);
|
Map<String, Object> value = toMap(claim);
|
MerchantCouponManagement coupon = couponMapper.selectById(claim.getCouponId());
|
if (coupon != null) {
|
value.put("couponName", coupon.getCouponName());
|
value.put("couponType", coupon.getCouponType());
|
value.put("couponAmount", coupon.getCouponAmount());
|
}
|
return claimView(value);
|
}
|
|
@Transactional
|
public Map<String, Object> updateClaimStatus(Long id, CouponClaimStatusRequest request) {
|
if (request.claimStatus() == null && request.useStatus() == null) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "领取状态和使用状态至少填写一项");
|
}
|
validateClaimStatus(request.claimStatus());
|
validateUseStatus(request.useStatus());
|
MerchantCouponClaim claim = requireClaim(id);
|
Integer oldClaimStatus = claim.getClaimStatus();
|
Integer targetClaimStatus = request.claimStatus() == null ? oldClaimStatus : request.claimStatus();
|
Integer targetUseStatus = request.useStatus() == null ? claim.getUseStatus() : request.useStatus();
|
if (targetClaimStatus == 2 && targetUseStatus == 1) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "已取消领取的优惠券不能标记为已使用");
|
}
|
if (oldClaimStatus != 2 && targetClaimStatus == 2) couponMapper.releaseOne(claim.getCouponId());
|
if (oldClaimStatus == 2 && targetClaimStatus != 2 && couponMapper.reserveOne(claim.getCouponId()) != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "优惠券剩余发放数量不足,不能恢复领取");
|
}
|
claim.setClaimStatus(targetClaimStatus);
|
claim.setUseStatus(targetUseStatus);
|
claim.setOrderId(request.orderId());
|
claim.setRemark(trimToNull(request.remark()));
|
if (targetUseStatus == 1 && claim.getUsedAt() == null) claim.setUsedAt(LocalDateTime.now());
|
if (targetUseStatus != 1) claim.setUsedAt(null);
|
if (claimMapper.updateById(claim) != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "领取记录状态更新失败");
|
}
|
return claim(id);
|
}
|
|
private void apply(MerchantCouponManagement coupon, CouponManagementSaveRequest request) {
|
coupon.setMerchantId(request.merchantId());
|
coupon.setCouponName(request.couponName().trim());
|
coupon.setCouponType(normalizeType(request.couponType()));
|
coupon.setCouponAmount(request.couponAmount());
|
coupon.setConditionType(clean(request.conditionType()).isBlank() ? "NONE" : clean(request.conditionType()).toUpperCase(Locale.ROOT));
|
coupon.setConditionAmount(request.conditionAmount());
|
coupon.setTalentLevels(writeLevels(request.talentLevels()));
|
coupon.setValidStart(request.validStart());
|
coupon.setValidEnd(request.validEnd());
|
coupon.setTotalQuantity(request.totalQuantity());
|
coupon.setGovernmentSubsidy(Boolean.TRUE.equals(request.governmentSubsidy()) ? 1 : 0);
|
coupon.setStatus(request.status() == null ? 1 : request.status());
|
}
|
|
private void validateSave(CouponManagementSaveRequest request, MerchantCouponManagement current) {
|
normalizeType(request.couponType());
|
validateCouponStatus(request.status());
|
if (couponMapper.countMerchant(request.merchantId()) == 0) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "适用商户不存在");
|
}
|
if (request.validStart() != null && request.validEnd() != null && request.validStart().isAfter(request.validEnd())) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "有效开始日期不能晚于结束日期");
|
}
|
int claimed = current == null || current.getClaimedQuantity() == null ? 0 : current.getClaimedQuantity();
|
if (request.totalQuantity() != -1 && request.totalQuantity() < claimed) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "发放总量不能小于已领取数量");
|
}
|
}
|
|
private MerchantCouponManagement requireCoupon(Long id) {
|
if (id == null || id <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠券ID不正确");
|
MerchantCouponManagement coupon = couponMapper.selectById(id);
|
if (coupon == null) throw new BusinessException(ErrorCode.NOT_FOUND, "优惠券不存在");
|
return coupon;
|
}
|
|
private MerchantCouponClaim requireClaim(Long id) {
|
if (id == null || id <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "领取记录ID不正确");
|
MerchantCouponClaim claim = claimMapper.selectById(id);
|
if (claim == null) throw new BusinessException(ErrorCode.NOT_FOUND, "优惠券领取记录不存在");
|
return claim;
|
}
|
|
private void validateCouponStatus(Integer status) {
|
if (status != null && status != 0 && status != 1) throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠券状态只能为0下架或1上架");
|
}
|
|
private void validateClaimStatus(Integer status) {
|
if (status != null && (status < 0 || status > 2)) throw new BusinessException(ErrorCode.BAD_REQUEST, "领取状态不正确");
|
}
|
|
private void validateUseStatus(Integer status) {
|
if (status != null && (status < 0 || status > 3)) throw new BusinessException(ErrorCode.BAD_REQUEST, "使用状态不正确");
|
}
|
|
private String normalizeTypeFilter(String value) {
|
return clean(value).isBlank() ? "" : normalizeType(value);
|
}
|
|
private String normalizeType(String value) {
|
String type = clean(value).toUpperCase(Locale.ROOT).replace('-', '_');
|
if (!COUPON_TYPES.contains(type)) throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠券类型不正确");
|
return type;
|
}
|
|
private String writeLevels(List<String> levels) {
|
if (levels == null || levels.isEmpty()) return null;
|
try {
|
return objectMapper.writeValueAsString(levels.stream().filter(v -> v != null && !v.isBlank()).map(String::trim).distinct().toList());
|
} catch (Exception exception) {
|
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "适用人才卡等级保存失败");
|
}
|
}
|
|
private Object readLevels(Object value) {
|
if (value == null || String.valueOf(value).isBlank()) return List.of();
|
try {
|
return objectMapper.readValue(String.valueOf(value), new TypeReference<List<String>>() {});
|
} catch (Exception exception) {
|
return List.of(String.valueOf(value));
|
}
|
}
|
|
private Map<String, Object> couponView(Map<String, Object> source) {
|
Map<String, Object> result = new LinkedHashMap<>(source);
|
result.put("talentLevels", readLevels(source.get("talentLevels")));
|
Integer status = intValue(source.get("status"));
|
result.put("statusText", status != null && status == 1 ? "上架" : "下架");
|
String type = String.valueOf(source.getOrDefault("couponType", ""));
|
result.put("couponTypeText", couponTypeText(type));
|
int total = intValue(source.get("totalQuantity")) == null ? -1 : intValue(source.get("totalQuantity"));
|
int claimed = intValue(source.get("claimedQuantity")) == null ? 0 : intValue(source.get("claimedQuantity"));
|
result.put("remainingQuantity", total == -1 ? -1 : Math.max(total - claimed, 0));
|
return result;
|
}
|
|
private Map<String, Object> claimView(Map<String, Object> source) {
|
Map<String, Object> result = new LinkedHashMap<>(source);
|
result.put("claimStatusText", switch (intValue(source.get("claimStatus")) == null ? -1 : intValue(source.get("claimStatus"))) {
|
case 0 -> "待领取"; case 1 -> "已领取"; case 2 -> "已取消"; default -> "未知";
|
});
|
result.put("useStatusText", switch (intValue(source.get("useStatus")) == null ? -1 : intValue(source.get("useStatus"))) {
|
case 0 -> "未使用"; case 1 -> "已使用"; case 2 -> "已过期"; case 3 -> "已作废"; default -> "未知";
|
});
|
return result;
|
}
|
|
private String couponTypeText(String type) {
|
return switch (type) {
|
case "CASH" -> "代金券";
|
case "FULL_REDUCTION" -> "满减券";
|
case "DISCOUNT" -> "折扣券";
|
case "EXCHANGE" -> "兑换券";
|
default -> "未知";
|
};
|
}
|
|
private Map<String, Object> toMap(Object bean) {
|
return objectMapper.convertValue(bean, new TypeReference<LinkedHashMap<String, Object>>() {});
|
}
|
|
private String nextBatchNo() {
|
return "CPB" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
|
+ UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT);
|
}
|
|
private String clean(String value) { return value == null ? "" : value.trim(); }
|
private String trimToNull(String value) { return clean(value).isBlank() ? null : clean(value); }
|
private Integer intValue(Object value) { return value instanceof Number number ? number.intValue() : null; }
|
}
|