package com.yau.digitalrmb.institutionidentity.application; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.yau.digitalrmb.institutionidentity.infrastructure.QuotaControlBitEntity; import com.yau.digitalrmb.institutionidentity.infrastructure.QuotaControlBitMapper; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyBatchEntity; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyBatchMapper; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyEntity; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyMapper; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; 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.security.SecureRandom; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @Service public class StandardCurrencyService { public static final String ID_RULE = "批次号+枚序号+随机校验码"; private static final List DENOMINATIONS = Arrays.asList( new BigDecimal("100.00"), new BigDecimal("50.00"), new BigDecimal("20.00"), new BigDecimal("10.00"), new BigDecimal("5.00"), new BigDecimal("1.00"), new BigDecimal("0.50"), new BigDecimal("0.20"), new BigDecimal("0.10"), new BigDecimal("0.05"), new BigDecimal("0.01")); private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); private static final char[] CHECK_CODE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); @Resource private QuotaControlBitMapper quotaControlBitMapper; @Resource private StandardCurrencyBatchMapper batchMapper; @Resource private StandardCurrencyMapper currencyMapper; @Resource private InstitutionTrainingErrorRecorder errorRecorder; @Resource private InstitutionTrainingScoreService scoreService; @Transactional public StandardCurrencyBatchResult decompose(BigDecimal amount, InstitutionKeySubject subject, String operator) { QuotaControlBitEntity source = requireStepEight(subject); if (amount == null || source.getAmount().compareTo(amount) != 0) { throw inputError(subject, "用户输入的货币总额与步骤八额度控制位金额不一致"); } StandardCurrencyBatchEntity current = latest(subject); List breakdown = decomposeAmount(source.getAmount()); if (current != null) { if (!current.getQuotaControlBitId().equals(source.getId())) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "当前实训已有步骤九记录,如需重新开始请先调用重新实训接口"); } currencyMapper.update(null, new LambdaUpdateWrapper() .eq(StandardCurrencyEntity::getBatchId, current.getId()) .eq(StandardCurrencyEntity::getDeleted, false) .set(StandardCurrencyEntity::getDeleted, true) .set(StandardCurrencyEntity::getUpdatedAt, LocalDateTime.now()) .set(StandardCurrencyEntity::getUpdatedBy, operator)); int updated = batchMapper.update(null, new LambdaUpdateWrapper() .eq(StandardCurrencyBatchEntity::getId, current.getId()) .eq(StandardCurrencyBatchEntity::getDeleted, false) .set(StandardCurrencyBatchEntity::getTotalAmount, source.getAmount().setScale(2, RoundingMode.UNNECESSARY)) .set(StandardCurrencyBatchEntity::getTotalCount, breakdown.stream().mapToInt(DenominationBreakdownResult::getCount).sum()) .set(StandardCurrencyBatchEntity::getDenominationSummary, encode(breakdown)) .set(StandardCurrencyBatchEntity::getPrefix, null) .set(StandardCurrencyBatchEntity::getIdRule, null) .set(StandardCurrencyBatchEntity::getCurrencyStatus, null) .set(StandardCurrencyBatchEntity::getStatus, "DECOMPOSED") .set(StandardCurrencyBatchEntity::getUpdatedAt, LocalDateTime.now()) .set(StandardCurrencyBatchEntity::getUpdatedBy, operator)); if (updated != 1) throw stateChanged(); return result(requireLatest(subject)); } StandardCurrencyBatchEntity batch = new StandardCurrencyBatchEntity(); batch.setQuotaControlBitId(source.getId()); batch.setBatchNumber(LocalDateTime.now().format(DATE) + "_" + String.format("%03d", source.getId() % 1000)); batch.setTotalAmount(source.getAmount().setScale(2, RoundingMode.UNNECESSARY)); batch.setTotalCount(breakdown.stream().mapToInt(DenominationBreakdownResult::getCount).sum()); batch.setDenominationSummary(encode(breakdown)); batch.setStatus("DECOMPOSED"); batch.setUserId(subject.getUserId()); batch.setSchoolId(subject.getSchoolId()); batch.setClassId(subject.getClassId()); batch.setCreatedAt(LocalDateTime.now()); batch.setUpdatedAt(batch.getCreatedAt()); batch.setCreatedBy(operator); batch.setUpdatedBy(operator); batch.setDeleted(false); batchMapper.insert(batch); return result(batch); } @Transactional public StandardCurrencyBatchResult confirmRule(String prefix, String idRule, String currencyStatus, InstitutionKeySubject subject, String operator) { StandardCurrencyBatchEntity batch = requireLatest(subject); if (batch.getDenominationSummary() == null || batch.getTotalAmount() == null) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先执行面额拆解"); } if (!"DC".equals(prefix == null ? null : prefix.trim().toUpperCase())) { throw inputError(subject, "币串前缀必须为DC"); } if (!ID_RULE.equals(idRule == null ? null : idRule.trim())) { throw inputError(subject, "币串ID规则必须为“批次号+枚序号+随机校验码”"); } if (!"待生效".equals(currencyStatus == null ? null : currencyStatus.trim())) { throw inputError(subject, "币串初始状态必须为“待生效”"); } int updated = batchMapper.update(null, new LambdaUpdateWrapper() .eq(StandardCurrencyBatchEntity::getId, batch.getId()) .eq(StandardCurrencyBatchEntity::getStatus, batch.getStatus()) .eq(StandardCurrencyBatchEntity::getDeleted, false) .set(StandardCurrencyBatchEntity::getPrefix, "DC") .set(StandardCurrencyBatchEntity::getIdRule, ID_RULE) .set(StandardCurrencyBatchEntity::getCurrencyStatus, "待生效") .set(StandardCurrencyBatchEntity::getStatus, "RULE_CONFIRMED") .set(StandardCurrencyBatchEntity::getUpdatedAt, LocalDateTime.now()) .set(StandardCurrencyBatchEntity::getUpdatedBy, operator)); if (updated != 1) throw stateChanged(); return result(requireLatest(subject)); } @Transactional public StandardCurrencyBatchResult generate(InstitutionKeySubject subject, String operator) { StandardCurrencyBatchEntity batch = requireLatest(subject); if (batch.getPrefix() == null || batch.getIdRule() == null || batch.getCurrencyStatus() == null) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先确认币串生成规则"); } QuotaControlBitEntity source = requireStepEight(subject); if (!source.getId().equals(batch.getQuotaControlBitId()) || source.getBankSignature() == null || source.getCentralBankSignatureSegment() == null) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤八额度控制位签名数据不完整"); } List values = expand(decode(batch.getDenominationSummary())); List existing = currencyMapper.selectList( new LambdaQueryWrapper() .eq(StandardCurrencyEntity::getBatchId, batch.getId()) .eq(StandardCurrencyEntity::getDeleted, false) .orderByAsc(StandardCurrencyEntity::getSequenceNumber)); SecureRandom random = new SecureRandom(); for (int i = 0; i < values.size(); i++) { int sequence = i + 1; String sequenceText = String.format("%03d", sequence); String checkCode = checkCode(random); String currencyId = "DC_" + batch.getBatchNumber() + "_" + sequenceText + "_" + checkCode; String denomination = values.get(i).setScale(2).toPlainString(); if (i < existing.size()) { StandardCurrencyEntity currency = existing.get(i); currencyMapper.update(null, new LambdaUpdateWrapper() .eq(StandardCurrencyEntity::getId, currency.getId()) .eq(StandardCurrencyEntity::getDeleted, false) .set(StandardCurrencyEntity::getSequenceNumber, sequence) .set(StandardCurrencyEntity::getDenomination, values.get(i)) .set(StandardCurrencyEntity::getRandomCheckCode, checkCode) .set(StandardCurrencyEntity::getCurrencyId, currencyId) .set(StandardCurrencyEntity::getInstitutionIdentifier, source.getInstitutionIdentifier()) .set(StandardCurrencyEntity::getBankSignatureSegment, source.getBankSignature()) .set(StandardCurrencyEntity::getCentralBankSignatureSegment, source.getCentralBankSignatureSegment()) .set(StandardCurrencyEntity::getCompleteCurrency, currencyId + "_" + source.getInstitutionIdentifier() + "_" + denomination + "_" + source.getBankSignature() + "_" + source.getCentralBankSignatureSegment()) .set(StandardCurrencyEntity::getStatus, "待生效") .set(StandardCurrencyEntity::getUpdatedAt, LocalDateTime.now()) .set(StandardCurrencyEntity::getUpdatedBy, operator)); } else { StandardCurrencyEntity currency = new StandardCurrencyEntity(); currency.setBatchId(batch.getId()); currency.setSequenceNumber(sequence); currency.setDenomination(values.get(i)); currency.setRandomCheckCode(checkCode); currency.setCurrencyId(currencyId); currency.setInstitutionIdentifier(source.getInstitutionIdentifier()); currency.setBankSignatureSegment(source.getBankSignature()); currency.setCentralBankSignatureSegment(source.getCentralBankSignatureSegment()); currency.setCompleteCurrency(currencyId + "_" + source.getInstitutionIdentifier() + "_" + denomination + "_" + source.getBankSignature() + "_" + source.getCentralBankSignatureSegment()); currency.setStatus("待生效"); currency.setCreatedAt(LocalDateTime.now()); currency.setUpdatedAt(currency.getCreatedAt()); currency.setCreatedBy(operator); currency.setUpdatedBy(operator); currency.setDeleted(false); currencyMapper.insert(currency); } } int updated = batchMapper.update(null, new LambdaUpdateWrapper() .eq(StandardCurrencyBatchEntity::getId, batch.getId()) .eq(StandardCurrencyBatchEntity::getStatus, batch.getStatus()) .eq(StandardCurrencyBatchEntity::getDeleted, false) .set(StandardCurrencyBatchEntity::getStatus, "GENERATED") .set(StandardCurrencyBatchEntity::getUpdatedAt, LocalDateTime.now()) .set(StandardCurrencyBatchEntity::getUpdatedBy, operator)); if (updated != 1) throw stateChanged(); scoreService.markStepCompleted(subject.getUserId(), 9); return result(requireLatest(subject)); } @Transactional(readOnly = true) public StandardCurrencyBatchResult detail(InstitutionKeySubject subject) { return result(requireLatest(subject)); } private List decomposeAmount(BigDecimal amount) { long remaining = amount.movePointRight(2).longValueExact(); List result = new ArrayList<>(); for (BigDecimal denomination : DENOMINATIONS) { long cents = denomination.movePointRight(2).longValueExact(); int count = Math.toIntExact(remaining / cents); remaining %= cents; result.add(new DenominationBreakdownResult(denomination, count)); } if (remaining != 0) throw new BusinessException(ErrorCode.VALIDATION_ERROR, "货币金额无法按标准面额完整拆解"); return result; } private String encode(List values) { return values.stream().map(v -> v.getDenomination().setScale(2).toPlainString() + ":" + v.getCount()) .collect(Collectors.joining(",")); } private List decode(String summary) { List result = new ArrayList<>(); for (String item : summary.split(",")) { String[] parts = item.split(":", 2); result.add(new DenominationBreakdownResult(new BigDecimal(parts[0]), Integer.parseInt(parts[1]))); } return result; } private List expand(List breakdown) { List result = new ArrayList<>(); for (DenominationBreakdownResult item : breakdown) { result.addAll(Collections.nCopies(item.getCount(), item.getDenomination())); } return result; } private String checkCode(SecureRandom random) { StringBuilder value = new StringBuilder(6); for (int i = 0; i < 6; i++) value.append(CHECK_CODE[random.nextInt(CHECK_CODE.length)]); return value.toString(); } private QuotaControlBitEntity requireStepEight(InstitutionKeySubject subject) { QuotaControlBitEntity value = quotaControlBitMapper.selectOne(new LambdaQueryWrapper() .eq(QuotaControlBitEntity::getUserId, subject.getUserId()) .eq(QuotaControlBitEntity::getSchoolId, subject.getSchoolId()) .eq(QuotaControlBitEntity::getClassId, subject.getClassId()) .eq(QuotaControlBitEntity::getStatus, "RECEIVED") .eq(QuotaControlBitEntity::getDeleted, false) .orderByDesc(QuotaControlBitEntity::getCreatedAt).last("LIMIT 1")); if (value == null) throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤八尚未完成央行接收,不能进入步骤九"); return value; } private StandardCurrencyBatchEntity requireStatus(InstitutionKeySubject subject, String status, String message) { StandardCurrencyBatchEntity value = requireLatest(subject); if (!status.equals(value.getStatus())) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, message + ",当前状态:" + value.getStatus()); } return value; } private StandardCurrencyBatchEntity requireLatest(InstitutionKeySubject subject) { StandardCurrencyBatchEntity value = latest(subject); if (value == null) throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前没有步骤九标准币串记录"); return value; } private StandardCurrencyBatchEntity latest(InstitutionKeySubject subject) { return batchMapper.selectOne(new LambdaQueryWrapper() .eq(StandardCurrencyBatchEntity::getUserId, subject.getUserId()) .eq(StandardCurrencyBatchEntity::getSchoolId, subject.getSchoolId()) .eq(StandardCurrencyBatchEntity::getClassId, subject.getClassId()) .eq(StandardCurrencyBatchEntity::getDeleted, false) .orderByDesc(StandardCurrencyBatchEntity::getCreatedAt).last("LIMIT 1")); } private StandardCurrencyBatchResult result(StandardCurrencyBatchEntity batch) { List currencies = currencyMapper.selectList( new LambdaQueryWrapper() .eq(StandardCurrencyEntity::getBatchId, batch.getId()) .eq(StandardCurrencyEntity::getDeleted, false) .orderByAsc(StandardCurrencyEntity::getSequenceNumber)); return new StandardCurrencyBatchResult(batch, decode(batch.getDenominationSummary()), currencies); } private BusinessException inputError(InstitutionKeySubject subject, String message) { int sequence = errorRecorder.recordScoreError(subject.getUserId()); return new BusinessException(ErrorCode.VALIDATION_ERROR, message + ",本次实训第" + sequence + "次错误"); } private BusinessException stateChanged() { return new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤九状态已变化,不能重复或越级操作"); } }