package com.ruoyi.other.service.impl;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.ruoyi.common.core.exception.ServiceException;
|
import com.ruoyi.common.core.utils.StringUtils;
|
import com.ruoyi.common.core.web.page.PageInfo;
|
import com.ruoyi.other.api.domain.TExchangeCodeDetail;
|
import com.ruoyi.other.api.domain.TExchangeCodePackage;
|
import com.ruoyi.other.api.domain.TExchangeCodePackageSpec;
|
import com.ruoyi.other.api.domain.TExchangeCodeUseRecord;
|
import com.ruoyi.other.api.dto.ExchangeCodeBindDTO;
|
import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO;
|
import com.ruoyi.other.api.dto.ExchangeCodePackageSaveDTO;
|
import com.ruoyi.other.api.dto.ExchangeCodePackageSpecDTO;
|
import com.ruoyi.other.api.dto.ExchangeCodeUseRecordQuery;
|
import com.ruoyi.other.api.vo.ExchangeCodeBindResultVO;
|
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
|
import com.ruoyi.other.api.vo.ExchangeCodePackageDetailVO;
|
import com.ruoyi.other.api.vo.ExchangeCodePackageSpecVO;
|
import com.ruoyi.other.mapper.TExchangeCodeDetailMapper;
|
import com.ruoyi.other.mapper.TExchangeCodePackageMapper;
|
import com.ruoyi.other.mapper.TExchangeCodePackageSpecMapper;
|
import com.ruoyi.other.mapper.TExchangeCodeUseRecordMapper;
|
import com.ruoyi.other.query.ExchangeCodeBatchInvalidDTO;
|
import com.ruoyi.other.query.ExchangeCodeDetailQuery;
|
import com.ruoyi.other.query.ExchangeCodePackageQuery;
|
import com.ruoyi.other.service.TExchangeCodePackageService;
|
import com.ruoyi.other.vo.ExchangeCodeBatchInvalidResultVO;
|
import com.ruoyi.other.vo.ExchangeCodeDetailVO;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import javax.annotation.Resource;
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.HashMap;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.UUID;
|
import java.util.stream.Collectors;
|
|
@Service
|
public class TExchangeCodePackageServiceImpl extends ServiceImpl<TExchangeCodePackageMapper, TExchangeCodePackage>
|
implements TExchangeCodePackageService {
|
|
private static final Integer STATUS_ENABLE = 1;
|
private static final Integer STATUS_DISABLE = 2;
|
private static final Integer CODE_STATUS_UNBOUND = 1;
|
private static final Integer CODE_STATUS_BOUND = 2;
|
private static final Integer CODE_STATUS_INVALID = 3;
|
private static final Integer USE_RECORD_TYPE_EXCHANGE = 1;
|
private static final Integer USE_RECORD_TYPE_CONSUME = 2;
|
private static final Integer USE_RECORD_TYPE_REFUND = 3;
|
private static final String EXPIRE_INVALID_REASON = "领取截止日期已过期";
|
private static final DateTimeFormatter BATCH_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
|
@Resource
|
private TExchangeCodePackageSpecMapper packageSpecMapper;
|
|
@Resource
|
private TExchangeCodeDetailMapper codeDetailMapper;
|
|
@Resource
|
private TExchangeCodeUseRecordMapper useRecordMapper;
|
|
@Override
|
public PageInfo<TExchangeCodePackage> pageList(ExchangeCodePackageQuery query) {
|
PageInfo<TExchangeCodePackage> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
|
List<TExchangeCodePackage> records = this.baseMapper.pageList(pageInfo, query);
|
pageInfo.setRecords(records);
|
return pageInfo;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void savePackage(ExchangeCodePackageSaveDTO dto) {
|
validateSaveDTO(dto, false);
|
TExchangeCodePackage entity = buildPackageEntity(dto, new TExchangeCodePackage());
|
this.save(entity);
|
saveSpecsAndCodes(entity.getId(), dto.getSpecList(), dto.getInventory(), null);
|
refreshPackageStats(entity.getId());
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updatePackage(ExchangeCodePackageSaveDTO dto) {
|
if (dto.getId() == null) {
|
throw new ServiceException("卡包ID不能为空");
|
}
|
validateSaveDTO(dto, true);
|
TExchangeCodePackage old = this.getById(dto.getId());
|
if (old == null) {
|
throw new ServiceException("卡包不存在");
|
}
|
if (dto.getInventory() < old.getInventory()) {
|
throw new ServiceException("编辑时库存只能增加");
|
}
|
TExchangeCodePackage entity = buildPackageEntity(dto, old);
|
this.updateById(entity);
|
packageSpecMapper.delete(new QueryWrapper<TExchangeCodePackageSpec>().eq("package_id", dto.getId()));
|
saveSpecsAndCodes(dto.getId(), dto.getSpecList(), dto.getInventory(), old.getInventory());
|
refreshPackageStats(dto.getId());
|
}
|
|
@Override
|
public void updatePackageStatus(Long id, Integer status) {
|
if (id == null) {
|
throw new ServiceException("卡包ID不能为空");
|
}
|
if (!STATUS_ENABLE.equals(status) && !STATUS_DISABLE.equals(status)) {
|
throw new ServiceException("启用状态不正确");
|
}
|
TExchangeCodePackage old = this.getById(id);
|
if (old == null) {
|
throw new ServiceException("卡包不存在");
|
}
|
TExchangeCodePackage entity = new TExchangeCodePackage();
|
entity.setId(id);
|
entity.setStatus(status);
|
this.updateById(entity);
|
}
|
|
@Override
|
public void refreshExpiredPackageCodes() {
|
List<TExchangeCodePackage> expiredPackages = this.list(new QueryWrapper<TExchangeCodePackage>()
|
.isNotNull("expire_time")
|
.lt("expire_time", LocalDateTime.now())
|
.eq("del_flag", 0));
|
for (TExchangeCodePackage exchangePackage : expiredPackages) {
|
refreshExpiredPackageCodes(exchangePackage.getId());
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deletePackage(String ids) {
|
if (!StringUtils.hasText(ids)) {
|
throw new ServiceException("卡包ID不能为空");
|
}
|
List<Long> idList = Arrays.stream(ids.split(","))
|
.filter(StringUtils::hasText)
|
.map(Long::valueOf)
|
.collect(Collectors.toList());
|
if (idList.isEmpty()) {
|
throw new ServiceException("卡包ID不能为空");
|
}
|
Long boundCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
|
.in("package_id", idList)
|
.eq("status", CODE_STATUS_BOUND)
|
.eq("del_flag", 0));
|
if (boundCount != null && boundCount > 0) {
|
throw new ServiceException("存在已绑定兑换码,不允许删除");
|
}
|
this.removeByIds(idList);
|
packageSpecMapper.delete(new QueryWrapper<TExchangeCodePackageSpec>().in("package_id", idList));
|
codeDetailMapper.delete(new QueryWrapper<TExchangeCodeDetail>().in("package_id", idList));
|
}
|
|
@Override
|
public ExchangeCodePackageDetailVO getDetail(Long id) {
|
TExchangeCodePackage entity = this.getById(id);
|
if (entity == null) {
|
throw new ServiceException("卡包不存在");
|
}
|
refreshExpiredPackageCodes(id);
|
refreshPackageStats(id);
|
entity = this.getById(id);
|
ExchangeCodePackageDetailVO detailVO = new ExchangeCodePackageDetailVO();
|
BeanUtils.copyProperties(entity, detailVO);
|
List<TExchangeCodePackageSpec> specs = packageSpecMapper.selectList(new QueryWrapper<TExchangeCodePackageSpec>()
|
.eq("package_id", id)
|
.orderByAsc("sort", "id"));
|
List<ExchangeCodePackageSpecVO> specVOList = new ArrayList<>();
|
for (TExchangeCodePackageSpec spec : specs) {
|
ExchangeCodePackageSpecVO vo = new ExchangeCodePackageSpecVO();
|
BeanUtils.copyProperties(spec, vo);
|
Long boundCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
|
.eq("package_spec_id", spec.getId())
|
.eq("status", CODE_STATUS_BOUND)
|
.eq("del_flag", 0));
|
Long unboundCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
|
.eq("package_spec_id", spec.getId())
|
.eq("status", CODE_STATUS_UNBOUND)
|
.eq("del_flag", 0));
|
vo.setBoundCount(boundCount == null ? 0 : boundCount.intValue());
|
vo.setUnboundCount(unboundCount == null ? 0 : unboundCount.intValue());
|
specVOList.add(vo);
|
}
|
detailVO.setSpecList(specVOList);
|
return detailVO;
|
}
|
|
@Override
|
public PageInfo<ExchangeCodeDetailVO> codePage(ExchangeCodeDetailQuery query) {
|
refreshExpiredPackageCodes(query.getPackageId());
|
PageInfo<ExchangeCodeDetailVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
|
List<ExchangeCodeDetailVO> records = codeDetailMapper.pageList(pageInfo, query);
|
pageInfo.setRecords(records);
|
return pageInfo;
|
}
|
|
@Override
|
public List<ExchangeCodeDetailVO> exportCodes(ExchangeCodeDetailQuery query) {
|
refreshExpiredPackageCodes(query.getPackageId());
|
return codeDetailMapper.exportList(query);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public ExchangeCodeBatchInvalidResultVO batchInvalidate(ExchangeCodeBatchInvalidDTO dto) {
|
if (dto.getPackageId() == null || dto.getDetailIds() == null || dto.getDetailIds().isEmpty()) {
|
throw new ServiceException("请选择需要失效的兑换码");
|
}
|
int success = 0;
|
int skip = 0;
|
List<TExchangeCodeDetail> details = codeDetailMapper.selectBatchIds(dto.getDetailIds());
|
for (TExchangeCodeDetail detail : details) {
|
if (!Objects.equals(detail.getPackageId(), dto.getPackageId())) {
|
continue;
|
}
|
if (Objects.equals(detail.getStatus(), CODE_STATUS_UNBOUND)) {
|
detail.setStatus(CODE_STATUS_INVALID);
|
detail.setInvalidTime(LocalDateTime.now());
|
detail.setInvalidReason("后台批量失效");
|
codeDetailMapper.updateById(detail);
|
success++;
|
} else {
|
skip++;
|
}
|
}
|
refreshPackageStats(dto.getPackageId());
|
return new ExchangeCodeBatchInvalidResultVO(success, skip);
|
}
|
|
@Override
|
public List<ExchangeCodeDetailVO> listDownloadableCodes(Long packageId, boolean onlyUnbound) {
|
ExchangeCodeDetailQuery query = new ExchangeCodeDetailQuery();
|
query.setPackageId(packageId);
|
if (onlyUnbound) {
|
query.setStatus(CODE_STATUS_UNBOUND);
|
}
|
return exportCodes(query);
|
}
|
|
@Override
|
public ExchangeCodeDetailVO getCodeDetail(Long id) {
|
TExchangeCodeDetail detail = codeDetailMapper.selectById(id);
|
if (detail == null || Boolean.TRUE.equals(detail.getDelFlag())) {
|
throw new ServiceException("兑换码不存在");
|
}
|
ExchangeCodeDetailVO vo = new ExchangeCodeDetailVO();
|
BeanUtils.copyProperties(detail, vo);
|
if (detail.getPackageSpecId() != null) {
|
TExchangeCodePackageSpec spec = packageSpecMapper.selectById(detail.getPackageSpecId());
|
if (spec != null) {
|
vo.setAmount(spec.getAmount());
|
vo.setQuantity(spec.getQuantity());
|
}
|
}
|
return vo;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public ExchangeCodeBindResultVO bindExchangeCode(ExchangeCodeBindDTO dto, Long userId, String phone) {
|
if (!StringUtils.hasText(dto.getCode()) || !StringUtils.hasText(dto.getPassword())) {
|
throw new ServiceException("兑换码或密码不能为空");
|
}
|
TExchangeCodeDetail detail = codeDetailMapper.selectOne(new QueryWrapper<TExchangeCodeDetail>()
|
.eq("code", dto.getCode())
|
.eq("password", dto.getPassword())
|
.last("limit 1"));
|
if (detail == null) {
|
throw new ServiceException("兑换码或密码错误");
|
}
|
TExchangeCodePackage exchangePackage = this.getById(detail.getPackageId());
|
if (exchangePackage == null || !Objects.equals(exchangePackage.getStatus(), STATUS_ENABLE)) {
|
throw new ServiceException("卡包已下架");
|
}
|
if (exchangePackage.getExpireTime() != null && exchangePackage.getExpireTime().isBefore(LocalDateTime.now())) {
|
refreshExpiredPackageCodes(exchangePackage.getId());
|
throw new ServiceException("已失效");
|
}
|
if (Objects.equals(detail.getStatus(), CODE_STATUS_INVALID)) {
|
throw new ServiceException("卡号失效");
|
}
|
if (Objects.equals(detail.getStatus(), CODE_STATUS_BOUND)) {
|
throw new ServiceException("兑换码已绑定");
|
}
|
String bindPhone = StringUtils.hasText(phone) ? phone : dto.getPhone();
|
if (!StringUtils.hasText(bindPhone)) {
|
throw new ServiceException("绑定手机号不能为空");
|
}
|
if (exchangePackage.getReceiveLimit() != null && exchangePackage.getReceiveLimit() > 0) {
|
Long bindCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
|
.eq("package_id", exchangePackage.getId())
|
.eq("bind_phone", bindPhone)
|
.eq("status", CODE_STATUS_BOUND)
|
.eq("del_flag", 0));
|
if (bindCount >= exchangePackage.getReceiveLimit()) {
|
throw new ServiceException("该手机号领取次数已达上限");
|
}
|
}
|
detail.setStatus(CODE_STATUS_BOUND);
|
detail.setBindUserId(userId);
|
detail.setBindPhone(bindPhone);
|
detail.setBindTime(LocalDateTime.now());
|
codeDetailMapper.updateById(detail);
|
BigDecimal amount = getDetailRemainAmount(detail);
|
TExchangeCodeUseRecord record = new TExchangeCodeUseRecord();
|
record.setDetailId(detail.getId());
|
record.setOrderId(0L);
|
record.setUserId(userId);
|
record.setChangeType(USE_RECORD_TYPE_EXCHANGE);
|
record.setUsedAmount(amount);
|
record.setGiftUsedAmount(defaultMoney(detail.getGiftAmount()));
|
useRecordMapper.insert(record);
|
refreshPackageStats(exchangePackage.getId());
|
ExchangeCodeBindResultVO resultVO = new ExchangeCodeBindResultVO();
|
resultVO.setDetailId(detail.getId());
|
resultVO.setPackageId(exchangePackage.getId());
|
resultVO.setPackageName(exchangePackage.getPackageName());
|
resultVO.setBindPhone(bindPhone);
|
return resultVO;
|
}
|
|
@Override
|
public Page<TExchangeCodeUseRecord> useRecordPage(ExchangeCodeUseRecordQuery query) {
|
Page<TExchangeCodeUseRecord> page = useRecordMapper.selectPage(Page.of(query.getPageCurr(), query.getPageSize()),
|
new QueryWrapper<TExchangeCodeUseRecord>()
|
.eq("user_id", query.getUserId())
|
.eq(query.getChangeType() != null, "change_type", query.getChangeType())
|
.orderByDesc("create_time"));
|
fillUseRecordBalances(page.getRecords(), query.getUserId());
|
List<TExchangeCodeUseRecord> mergedRecords = mergeUseRecords(page.getRecords());
|
mergedRecords.forEach(this::fillUseRecordView);
|
page.setRecords(mergedRecords);
|
return page;
|
}
|
|
@Override
|
public TExchangeCodeUseRecord useRecordDetail(Long id, Long userId) {
|
TExchangeCodeUseRecord record = useRecordMapper.selectOne(new QueryWrapper<TExchangeCodeUseRecord>()
|
.eq("id", id)
|
.eq("user_id", userId)
|
.last("limit 1"));
|
fillUseRecordBalance(record, userId);
|
fillUseRecordView(record);
|
return record;
|
}
|
|
@Override
|
public Map<Long, BigDecimal> getUserExchangeCodeBalanceMap(List<Long> userIds) {
|
Map<Long, BigDecimal> result = new HashMap<>();
|
if (userIds == null || userIds.isEmpty()) {
|
return result;
|
}
|
List<TExchangeCodeDetail> details = codeDetailMapper.selectList(new QueryWrapper<TExchangeCodeDetail>()
|
.in("bind_user_id", userIds)
|
.eq("status", CODE_STATUS_BOUND)
|
.eq("del_flag", 0));
|
for (TExchangeCodeDetail detail : details) {
|
BigDecimal remainAmount = getDetailRemainAmount(detail);
|
if (remainAmount.compareTo(BigDecimal.ZERO) > 0) {
|
result.merge(detail.getBindUserId(), remainAmount, BigDecimal::add);
|
}
|
}
|
return result;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public ExchangeCodeConsumeResultVO consumeBalance(ExchangeCodeConsumeDTO dto) {
|
if (dto == null || dto.getUserId() == null || dto.getOrderId() == null || dto.getAmount() == null) {
|
throw new ServiceException("扣减参数不能为空");
|
}
|
ExchangeCodeConsumeResultVO resultVO = new ExchangeCodeConsumeResultVO();
|
BigDecimal requestedAmount = dto.getAmount().setScale(2, RoundingMode.HALF_UP);
|
if (requestedAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
resultVO.setDeductedAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
|
return resultVO;
|
}
|
List<TExchangeCodeUseRecord> existingRecords = useRecordMapper.selectList(new QueryWrapper<TExchangeCodeUseRecord>()
|
.eq("order_id", dto.getOrderId())
|
.gt("order_id", 0)
|
.eq("change_type", USE_RECORD_TYPE_CONSUME)
|
.eq("del_flag", 0));
|
if (!existingRecords.isEmpty()) {
|
BigDecimal deducted = existingRecords.stream()
|
.map(TExchangeCodeUseRecord::getUsedAmount)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
.setScale(2, RoundingMode.HALF_UP);
|
resultVO.setDeductedAmount(deducted);
|
return resultVO;
|
}
|
BigDecimal remainingNeed = requestedAmount;
|
BigDecimal deductedAmount = BigDecimal.ZERO;
|
List<TExchangeCodeDetail> details = codeDetailMapper.selectList(new QueryWrapper<TExchangeCodeDetail>()
|
.eq("bind_user_id", dto.getUserId())
|
.eq("status", CODE_STATUS_BOUND)
|
.eq("del_flag", 0)
|
.orderByAsc("bind_time", "id"));
|
for (TExchangeCodeDetail detail : details) {
|
if (remainingNeed.compareTo(BigDecimal.ZERO) <= 0) {
|
break;
|
}
|
BigDecimal remainAmount = getDetailRemainAmount(detail);
|
if (remainAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
continue;
|
}
|
BigDecimal useAmount = remainAmount.min(remainingNeed).setScale(2, RoundingMode.HALF_UP);
|
detail.setUsedAmount(defaultMoney(detail.getUsedAmount()).add(useAmount).setScale(2, RoundingMode.HALF_UP));
|
codeDetailMapper.updateById(detail);
|
TExchangeCodeUseRecord record = new TExchangeCodeUseRecord();
|
record.setDetailId(detail.getId());
|
record.setOrderId(dto.getOrderId());
|
record.setUserId(dto.getUserId());
|
record.setChangeType(USE_RECORD_TYPE_CONSUME);
|
record.setUsedAmount(useAmount);
|
record.setGiftUsedAmount(calculateGiftUsedAmount(useAmount, detail));
|
useRecordMapper.insert(record);
|
remainingNeed = remainingNeed.subtract(useAmount);
|
deductedAmount = deductedAmount.add(useAmount);
|
}
|
resultVO.setDeductedAmount(deductedAmount.setScale(2, RoundingMode.HALF_UP));
|
return resultVO;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public ExchangeCodeConsumeResultVO refundBalance(ExchangeCodeConsumeDTO dto) {
|
if (dto == null || dto.getUserId() == null || dto.getOrderId() == null || dto.getAmount() == null) {
|
throw new ServiceException("参数异常");
|
}
|
ExchangeCodeConsumeResultVO resultVO = new ExchangeCodeConsumeResultVO();
|
BigDecimal remainingRefund = dto.getAmount().setScale(2, RoundingMode.HALF_UP);
|
if (remainingRefund.compareTo(BigDecimal.ZERO) <= 0) {
|
resultVO.setDeductedAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
|
return resultVO;
|
}
|
List<TExchangeCodeUseRecord> existingRecords = useRecordMapper.selectList(new QueryWrapper<TExchangeCodeUseRecord>()
|
.eq("order_id", dto.getOrderId())
|
.eq("change_type", USE_RECORD_TYPE_REFUND)
|
.eq(dto.getRefundNo() != null && !dto.getRefundNo().isEmpty(), "refund_no", dto.getRefundNo())
|
.eq("del_flag", 0));
|
if (!existingRecords.isEmpty()) {
|
BigDecimal refunded = existingRecords.stream()
|
.map(TExchangeCodeUseRecord::getUsedAmount)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
.setScale(2, RoundingMode.HALF_UP);
|
resultVO.setDeductedAmount(refunded);
|
return resultVO;
|
}
|
List<TExchangeCodeUseRecord> consumeRecords = useRecordMapper.selectList(new QueryWrapper<TExchangeCodeUseRecord>()
|
.eq("order_id", dto.getOrderId())
|
.eq("user_id", dto.getUserId())
|
.eq("change_type", USE_RECORD_TYPE_CONSUME)
|
.eq("del_flag", 0)
|
.orderByDesc("id"));
|
BigDecimal refundedAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
for (TExchangeCodeUseRecord consumeRecord : consumeRecords) {
|
if (remainingRefund.compareTo(BigDecimal.ZERO) <= 0) {
|
break;
|
}
|
if (consumeRecord.getDetailId() == null) {
|
continue;
|
}
|
TExchangeCodeDetail detail = codeDetailMapper.selectById(consumeRecord.getDetailId());
|
if (detail == null) {
|
continue;
|
}
|
BigDecimal useAmount = defaultMoney(consumeRecord.getUsedAmount());
|
BigDecimal refundAmount = remainingRefund.min(useAmount).setScale(2, RoundingMode.HALF_UP);
|
detail.setUsedAmount(defaultMoney(detail.getUsedAmount()).subtract(refundAmount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP));
|
codeDetailMapper.updateById(detail);
|
TExchangeCodeUseRecord record = new TExchangeCodeUseRecord();
|
record.setDetailId(detail.getId());
|
record.setOrderId(dto.getOrderId());
|
record.setRefundNo(dto.getRefundNo());
|
record.setUserId(dto.getUserId());
|
record.setChangeType(USE_RECORD_TYPE_REFUND);
|
record.setUsedAmount(refundAmount);
|
record.setGiftUsedAmount(calculateGiftUsedAmount(refundAmount, detail));
|
useRecordMapper.insert(record);
|
remainingRefund = remainingRefund.subtract(refundAmount);
|
refundedAmount = refundedAmount.add(refundAmount);
|
}
|
resultVO.setDeductedAmount(refundedAmount.setScale(2, RoundingMode.HALF_UP));
|
return resultVO;
|
}
|
|
private BigDecimal calculateGiftUsedAmount(BigDecimal useAmount, TExchangeCodeDetail detail) {
|
if (useAmount == null || detail == null || detail.getGiftRatio() == null) {
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
}
|
return useAmount.multiply(detail.getGiftRatio()).setScale(2, RoundingMode.HALF_UP);
|
}
|
|
private BigDecimal getDetailRemainAmount(TExchangeCodeDetail detail) {
|
if (detail == null || detail.getPackageSpecId() == null) {
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
}
|
TExchangeCodePackageSpec spec = packageSpecMapper.selectById(detail.getPackageSpecId());
|
if (spec == null || spec.getAmount() == null) {
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
}
|
BigDecimal remainAmount = spec.getAmount().subtract(defaultMoney(detail.getUsedAmount()));
|
return remainAmount.compareTo(BigDecimal.ZERO) < 0
|
? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
|
: remainAmount.setScale(2, RoundingMode.HALF_UP);
|
}
|
|
private TExchangeCodePackage buildPackageEntity(ExchangeCodePackageSaveDTO dto, TExchangeCodePackage entity) {
|
entity.setPackageName(dto.getPackageName());
|
entity.setCoverPicture(dto.getCoverPicture());
|
entity.setDetailsPicture(dto.getDetailsPicture());
|
entity.setExchangeDescription(dto.getExchangeDescription());
|
entity.setRemark(dto.getRemark());
|
entity.setInventory(dto.getInventory());
|
entity.setUnlimitedReceiveLimit(dto.getUnlimitedReceiveLimit());
|
entity.setReceiveLimit(Objects.equals(dto.getUnlimitedReceiveLimit(), 1) ? null : dto.getReceiveLimit());
|
entity.setFinanceReceivedAmount(defaultMoney(dto.getFinanceReceivedAmount()));
|
entity.setGiftAmount(defaultMoney(dto.getGiftAmount()));
|
entity.setTotalAmount(defaultMoney(dto.getTotalAmount()));
|
entity.setExpireTime(dto.getExpireTime());
|
entity.setStatus(dto.getStatus());
|
return entity;
|
}
|
|
private void validateSaveDTO(ExchangeCodePackageSaveDTO dto, boolean editing) {
|
if (!StringUtils.hasText(dto.getPackageName())) {
|
throw new ServiceException("请输入卡包名称");
|
}
|
if (dto.getInventory() == null || dto.getInventory() <= 0) {
|
throw new ServiceException("库存必须大于0");
|
}
|
if (dto.getExpireTime() != null && dto.getExpireTime().isBefore(LocalDateTime.now())) {
|
throw new ServiceException("截止领取时间不能早于当前时间");
|
}
|
if (!Objects.equals(dto.getUnlimitedReceiveLimit(), 1)
|
&& (dto.getReceiveLimit() == null || dto.getReceiveLimit() <= 0)) {
|
throw new ServiceException("请输入领取限制次数");
|
}
|
if (dto.getSpecList() == null || dto.getSpecList().isEmpty()) {
|
throw new ServiceException("请至少添加一个规格");
|
}
|
if (dto.getStatus() == null) {
|
dto.setStatus(STATUS_DISABLE);
|
}
|
BigDecimal finance = defaultMoney(dto.getFinanceReceivedAmount());
|
BigDecimal gift = defaultMoney(dto.getGiftAmount());
|
BigDecimal total = defaultMoney(dto.getTotalAmount());
|
if (finance.add(gift).compareTo(total) != 0) {
|
throw new ServiceException("财务实收 + 赠送金额 必须等于规格总金额");
|
}
|
BigDecimal specTotal = BigDecimal.ZERO;
|
int quantityTotal = 0;
|
for (ExchangeCodePackageSpecDTO spec : dto.getSpecList()) {
|
if (spec.getAmount() == null || spec.getAmount().compareTo(BigDecimal.ZERO) < 0) {
|
throw new ServiceException("规格金额不正确");
|
}
|
if (spec.getQuantity() == null || spec.getQuantity() <= 0) {
|
throw new ServiceException("规格数量必须大于0");
|
}
|
BigDecimal subtotal = spec.getAmount().multiply(BigDecimal.valueOf(spec.getQuantity()))
|
.setScale(2, RoundingMode.HALF_UP);
|
spec.setSubtotalAmount(subtotal);
|
specTotal = specTotal.add(subtotal);
|
quantityTotal += spec.getQuantity();
|
}
|
if (specTotal.compareTo(total) != 0) {
|
throw new ServiceException("总额必须等于所有规格金额乘数量之和");
|
}
|
if (quantityTotal != dto.getInventory()) {
|
throw new ServiceException(editing ? "编辑后的库存必须等于规格数量总和" : "库存必须等于规格数量总和");
|
}
|
allocateDiscount(dto.getSpecList(), gift, total);
|
}
|
|
private void allocateDiscount(List<ExchangeCodePackageSpecDTO> specList, BigDecimal giftAmount, BigDecimal totalAmount) {
|
if (giftAmount.compareTo(BigDecimal.ZERO) == 0 || totalAmount.compareTo(BigDecimal.ZERO) == 0) {
|
for (ExchangeCodePackageSpecDTO spec : specList) {
|
spec.setDiscountAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
|
}
|
return;
|
}
|
BigDecimal allocated = BigDecimal.ZERO;
|
for (int i = 0; i < specList.size(); i++) {
|
ExchangeCodePackageSpecDTO spec = specList.get(i);
|
BigDecimal discount;
|
if (i == specList.size() - 1) {
|
discount = giftAmount.subtract(allocated).setScale(2, RoundingMode.HALF_UP);
|
} else {
|
discount = spec.getSubtotalAmount().multiply(giftAmount)
|
.divide(totalAmount, 2, RoundingMode.HALF_UP);
|
allocated = allocated.add(discount);
|
}
|
spec.setDiscountAmount(discount);
|
}
|
}
|
|
private void saveSpecsAndCodes(Long packageId, List<ExchangeCodePackageSpecDTO> specList, Integer inventory, Integer oldInventory) {
|
List<TExchangeCodePackageSpec> specEntities = new ArrayList<>();
|
for (int i = 0; i < specList.size(); i++) {
|
ExchangeCodePackageSpecDTO spec = specList.get(i);
|
TExchangeCodePackageSpec entity = new TExchangeCodePackageSpec();
|
entity.setPackageId(packageId);
|
entity.setAmount(spec.getAmount());
|
entity.setQuantity(spec.getQuantity());
|
entity.setSubtotalAmount(spec.getSubtotalAmount());
|
entity.setDiscountAmount(spec.getDiscountAmount());
|
entity.setSort(spec.getSort() == null ? i + 1 : spec.getSort());
|
packageSpecMapper.insert(entity);
|
specEntities.add(entity);
|
}
|
int needCreate = oldInventory == null ? inventory : inventory - oldInventory;
|
if (needCreate <= 0) {
|
return;
|
}
|
String batchNo = "PKG" + LocalDateTime.now().format(BATCH_FORMATTER);
|
TExchangeCodePackage exchangePackage = this.getById(packageId);
|
BigDecimal giftRatio = calculateGiftRatio(exchangePackage);
|
List<TExchangeCodePackageSpec> expandList = new ArrayList<>();
|
for (TExchangeCodePackageSpec spec : specEntities) {
|
for (int i = 0; i < spec.getQuantity(); i++) {
|
expandList.add(spec);
|
}
|
}
|
if (expandList.isEmpty()) {
|
expandList = Collections.nCopies(needCreate, null);
|
}
|
for (int i = 0; i < needCreate; i++) {
|
TExchangeCodePackageSpec spec = expandList.get(i % expandList.size());
|
TExchangeCodeDetail detail = new TExchangeCodeDetail();
|
detail.setPackageId(packageId);
|
detail.setPackageSpecId(spec == null ? null : spec.getId());
|
detail.setCode(generateCode(packageId, i));
|
detail.setPassword(generatePassword());
|
detail.setStatus(CODE_STATUS_UNBOUND);
|
detail.setGiftRatio(giftRatio);
|
detail.setGiftAmount(calculateDetailGiftAmount(spec, giftRatio));
|
detail.setBatchNo(batchNo);
|
codeDetailMapper.insert(detail);
|
}
|
}
|
|
private BigDecimal calculateGiftRatio(TExchangeCodePackage exchangePackage) {
|
if (exchangePackage == null || exchangePackage.getTotalAmount() == null
|
|| exchangePackage.getTotalAmount().compareTo(BigDecimal.ZERO) == 0) {
|
return BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP);
|
}
|
return defaultMoney(exchangePackage.getGiftAmount())
|
.divide(exchangePackage.getTotalAmount(), 6, RoundingMode.HALF_UP);
|
}
|
|
private BigDecimal calculateDetailGiftAmount(TExchangeCodePackageSpec spec, BigDecimal giftRatio) {
|
if (spec == null || spec.getAmount() == null || giftRatio == null) {
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
}
|
return spec.getAmount().multiply(giftRatio).setScale(2, RoundingMode.HALF_UP);
|
}
|
|
private void refreshPackageStats(Long packageId) {
|
TExchangeCodePackage entity = this.getById(packageId);
|
if (entity == null) {
|
return;
|
}
|
Long boundCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
|
.eq("package_id", packageId)
|
.eq("status", CODE_STATUS_BOUND)
|
.eq("del_flag", 0));
|
Long invalidCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
|
.eq("package_id", packageId)
|
.eq("status", CODE_STATUS_INVALID)
|
.eq("del_flag", 0));
|
entity.setBoundCount(boundCount.intValue());
|
entity.setInvalidCount(invalidCount.intValue());
|
entity.setUnboundCount(entity.getInventory() - entity.getBoundCount() - entity.getInvalidCount());
|
this.baseMapper.updateById(entity);
|
}
|
|
private void refreshExpiredPackageCodes(Long packageId) {
|
if (packageId == null) {
|
return;
|
}
|
TExchangeCodePackage exchangePackage = this.getById(packageId);
|
if (exchangePackage == null || exchangePackage.getExpireTime() == null
|
|| !exchangePackage.getExpireTime().isBefore(LocalDateTime.now())) {
|
return;
|
}
|
TExchangeCodeDetail detail = new TExchangeCodeDetail();
|
detail.setStatus(CODE_STATUS_INVALID);
|
detail.setInvalidTime(LocalDateTime.now());
|
detail.setInvalidReason(EXPIRE_INVALID_REASON);
|
codeDetailMapper.update(detail, new QueryWrapper<TExchangeCodeDetail>()
|
.eq("package_id", packageId)
|
.eq("status", CODE_STATUS_UNBOUND)
|
.eq("del_flag", 0));
|
refreshPackageStats(packageId);
|
}
|
|
private String generateCode(Long packageId, int seq) {
|
return "CD" + packageId + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmss")) + String.format("%03d", seq);
|
}
|
|
private String generatePassword() {
|
return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
|
}
|
|
private List<TExchangeCodeUseRecord> mergeUseRecords(List<TExchangeCodeUseRecord> records) {
|
if (records == null || records.isEmpty()) {
|
return records;
|
}
|
Map<String, TExchangeCodeUseRecord> recordMap = new LinkedHashMap<>();
|
for (TExchangeCodeUseRecord record : records) {
|
String key = buildUseRecordMergeKey(record);
|
TExchangeCodeUseRecord merged = recordMap.get(key);
|
if (merged == null) {
|
recordMap.put(key, record);
|
continue;
|
}
|
merged.setUsedAmount(defaultMoney(merged.getUsedAmount()).add(defaultMoney(record.getUsedAmount())).setScale(2, RoundingMode.HALF_UP));
|
merged.setGiftUsedAmount(defaultMoney(merged.getGiftUsedAmount()).add(defaultMoney(record.getGiftUsedAmount())).setScale(2, RoundingMode.HALF_UP));
|
if (record.getId() != null && merged.getId() != null && record.getId() > merged.getId()) {
|
merged.setId(record.getId());
|
merged.setDetailId(record.getDetailId());
|
merged.setCreateTime(record.getCreateTime());
|
merged.setDateTime(record.getDateTime());
|
merged.setCurrentBalance(record.getCurrentBalance());
|
}
|
}
|
return new ArrayList<>(recordMap.values());
|
}
|
|
private String buildUseRecordMergeKey(TExchangeCodeUseRecord record) {
|
Long orderId = record.getOrderId() == null ? 0L : record.getOrderId();
|
Long detailId = record.getDetailId() == null ? 0L : record.getDetailId();
|
Integer changeType = record.getChangeType() == null ? 0 : record.getChangeType();
|
String refundNo = record.getRefundNo() == null ? "" : record.getRefundNo();
|
if (orderId > 0) {
|
return orderId + "_" + changeType + "_" + refundNo;
|
}
|
return detailId + "_" + changeType + "_" + refundNo;
|
}
|
|
private void fillUseRecordBalance(TExchangeCodeUseRecord record, Long userId) {
|
if (record == null || userId == null || record.getId() == null) {
|
return;
|
}
|
record.setCurrentBalance(calculateCurrentBalanceAfterRecord(userId, record.getId()));
|
}
|
|
private void fillUseRecordBalances(List<TExchangeCodeUseRecord> records, Long userId) {
|
if (records == null || records.isEmpty() || userId == null) {
|
return;
|
}
|
List<TExchangeCodeUseRecord> allRecords = useRecordMapper.selectList(new QueryWrapper<TExchangeCodeUseRecord>()
|
.eq("user_id", userId)
|
.orderByDesc("id"));
|
Map<Long, BigDecimal> balanceMap = new HashMap<>();
|
BigDecimal balance = getCurrentExchangeCodeBalance(userId);
|
for (TExchangeCodeUseRecord item : allRecords) {
|
balanceMap.put(item.getId(), balance);
|
balance = rollbackUseRecordChange(balance, item);
|
}
|
for (TExchangeCodeUseRecord item : records) {
|
item.setCurrentBalance(balanceMap.getOrDefault(item.getId(), BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)));
|
}
|
}
|
|
private BigDecimal calculateCurrentBalanceAfterRecord(Long userId, Long recordId) {
|
BigDecimal balance = getCurrentExchangeCodeBalance(userId);
|
List<TExchangeCodeUseRecord> records = useRecordMapper.selectList(new QueryWrapper<TExchangeCodeUseRecord>()
|
.eq("user_id", userId)
|
.gt("id", recordId)
|
.orderByDesc("id"));
|
for (TExchangeCodeUseRecord item : records) {
|
balance = rollbackUseRecordChange(balance, item);
|
}
|
return balance;
|
}
|
|
private BigDecimal rollbackUseRecordChange(BigDecimal balance, TExchangeCodeUseRecord record) {
|
BigDecimal amount = defaultMoney(record == null ? null : record.getUsedAmount());
|
Integer changeType = record == null ? null : record.getChangeType();
|
if (USE_RECORD_TYPE_CONSUME.equals(changeType)) {
|
return balance.add(amount).setScale(2, RoundingMode.HALF_UP);
|
}
|
return balance.subtract(amount).setScale(2, RoundingMode.HALF_UP);
|
}
|
|
private BigDecimal getCurrentExchangeCodeBalance(Long userId) {
|
Map<Long, BigDecimal> balanceMap = getUserExchangeCodeBalanceMap(Collections.singletonList(userId));
|
return defaultMoney(balanceMap.get(userId));
|
}
|
|
private void fillUseRecordView(TExchangeCodeUseRecord record) {
|
if (record == null) {
|
return;
|
}
|
record.setUid(String.valueOf(record.getId()));
|
record.setDateTime(record.getCreateTime());
|
record.setChangeAmount(defaultMoney(record.getUsedAmount()));
|
if (record.getCurrentBalance() == null) {
|
record.setCurrentBalance(defaultMoney(record.getUsedAmount()));
|
}
|
if (record.getChangeType() == null) {
|
record.setChangeType(record.getOrderId() == null || record.getOrderId() == 0
|
? USE_RECORD_TYPE_EXCHANGE : USE_RECORD_TYPE_CONSUME);
|
}
|
if (record.getDetailId() != null) {
|
TExchangeCodeDetail detail = codeDetailMapper.selectById(record.getDetailId());
|
if (detail != null) {
|
record.setCode(detail.getCode());
|
}
|
}
|
if (record.getOrderId() != null && record.getOrderId() > 0) {
|
record.setOrderCode(String.valueOf(record.getOrderId()));
|
}
|
}
|
|
private void fillUseRecordAmount(TExchangeCodeUseRecord record, BigDecimal amount, BigDecimal giftRatio) {
|
BigDecimal usedAmount = defaultMoney(amount);
|
BigDecimal ratio = giftRatio == null ? BigDecimal.ZERO : giftRatio;
|
if (ratio.compareTo(BigDecimal.ZERO) < 0) {
|
ratio = BigDecimal.ZERO;
|
}
|
BigDecimal giftUsedAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
if (ratio.compareTo(BigDecimal.ZERO) > 0) {
|
BigDecimal divisor = BigDecimal.ONE.add(ratio);
|
BigDecimal principalAmount = usedAmount.divide(divisor, 2, RoundingMode.HALF_UP);
|
giftUsedAmount = usedAmount.subtract(principalAmount).setScale(2, RoundingMode.HALF_UP);
|
}
|
record.setUsedAmount(usedAmount);
|
record.setGiftUsedAmount(giftUsedAmount);
|
}
|
|
private BigDecimal defaultMoney(BigDecimal amount) {
|
return amount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
|
: amount.setScale(2, RoundingMode.HALF_UP);
|
}
|
}
|