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 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 DateTimeFormatter BATCH_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); @Resource private TExchangeCodePackageSpecMapper packageSpecMapper; @Resource private TExchangeCodeDetailMapper codeDetailMapper; @Resource private TExchangeCodeUseRecordMapper useRecordMapper; @Override public PageInfo pageList(ExchangeCodePackageQuery query) { PageInfo pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize()); List 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().eq("package_id", dto.getId())); saveSpecsAndCodes(dto.getId(), dto.getSpecList(), dto.getInventory(), old.getInventory()); refreshPackageStats(dto.getId()); } @Override @Transactional(rollbackFor = Exception.class) public void deletePackage(String ids) { if (!StringUtils.hasText(ids)) { throw new ServiceException("卡包ID不能为空"); } List 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() .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().in("package_id", idList)); codeDetailMapper.delete(new QueryWrapper().in("package_id", idList)); } @Override public ExchangeCodePackageDetailVO getDetail(Long id) { TExchangeCodePackage entity = this.getById(id); if (entity == null) { throw new ServiceException("卡包不存在"); } refreshPackageStats(id); entity = this.getById(id); ExchangeCodePackageDetailVO detailVO = new ExchangeCodePackageDetailVO(); BeanUtils.copyProperties(entity, detailVO); List specs = packageSpecMapper.selectList(new QueryWrapper() .eq("package_id", id) .orderByAsc("sort", "id")); List specVOList = new ArrayList<>(); for (TExchangeCodePackageSpec spec : specs) { ExchangeCodePackageSpecVO vo = new ExchangeCodePackageSpecVO(); BeanUtils.copyProperties(spec, vo); Long boundCount = codeDetailMapper.selectCount(new QueryWrapper() .eq("package_spec_id", spec.getId()) .eq("status", CODE_STATUS_BOUND) .eq("del_flag", 0)); Long unboundCount = codeDetailMapper.selectCount(new QueryWrapper() .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 codePage(ExchangeCodeDetailQuery query) { PageInfo pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize()); List records = codeDetailMapper.pageList(pageInfo, query); pageInfo.setRecords(records); return pageInfo; } @Override public List exportCodes(ExchangeCodeDetailQuery query) { 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 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 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() .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())) { 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(dto.getPhone()) ? dto.getPhone() : phone; if (!StringUtils.hasText(bindPhone)) { throw new ServiceException("绑定手机号不能为空"); } if (Objects.equals(exchangePackage.getUnlimitedReceiveLimit(), 1) && exchangePackage.getReceiveLimit() != null) { Long bindCount = codeDetailMapper.selectCount(new QueryWrapper() .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 useRecordPage(ExchangeCodeUseRecordQuery query) { Page page = useRecordMapper.selectPage(Page.of(query.getPageCurr(), query.getPageSize()), new QueryWrapper() .eq("user_id", query.getUserId()) .eq(query.getChangeType() != null, "change_type", query.getChangeType()) .orderByDesc("create_time")); fillUseRecordBalances(page.getRecords(), query.getUserId()); List 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() .eq("id", id) .eq("user_id", userId) .last("limit 1")); fillUseRecordBalance(record, userId); fillUseRecordView(record); return record; } @Override public Map getUserExchangeCodeBalanceMap(List userIds) { Map result = new HashMap<>(); if (userIds == null || userIds.isEmpty()) { return result; } List details = codeDetailMapper.selectList(new QueryWrapper() .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 existingRecords = useRecordMapper.selectList(new QueryWrapper() .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 details = codeDetailMapper.selectList(new QueryWrapper() .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 existingRecords = useRecordMapper.selectList(new QueryWrapper() .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 consumeRecords = useRecordMapper.selectList(new QueryWrapper() .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 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 specList, Integer inventory, Integer oldInventory) { List 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 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() .eq("package_id", packageId) .eq("status", CODE_STATUS_BOUND) .eq("del_flag", 0)); Long invalidCount = codeDetailMapper.selectCount(new QueryWrapper() .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 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 mergeUseRecords(List records) { if (records == null || records.isEmpty()) { return records; } Map 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; } List records = useRecordMapper.selectList(new QueryWrapper() .eq("user_id", userId) .le("id", record.getId()) .orderByAsc("id")); record.setCurrentBalance(calculateCurrentBalance(records)); } private void fillUseRecordBalances(List records, Long userId) { if (records == null || records.isEmpty() || userId == null) { return; } List allRecords = useRecordMapper.selectList(new QueryWrapper() .eq("user_id", userId) .orderByAsc("id")); Map balanceMap = new HashMap<>(); BigDecimal balance = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); for (TExchangeCodeUseRecord item : allRecords) { balance = applyUseRecordChange(balance, item); balanceMap.put(item.getId(), balance); } for (TExchangeCodeUseRecord item : records) { item.setCurrentBalance(balanceMap.getOrDefault(item.getId(), BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP))); } } private BigDecimal calculateCurrentBalance(List records) { BigDecimal balance = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); if (records == null) { return balance; } for (TExchangeCodeUseRecord item : records) { balance = applyUseRecordChange(balance, item); } return balance; } private BigDecimal applyUseRecordChange(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.subtract(amount).setScale(2, RoundingMode.HALF_UP); } return balance.add(amount).setScale(2, RoundingMode.HALF_UP); } 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); } }