You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
digital-rmb-backend/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyService.java

309 lines
18 KiB
Java

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<BigDecimal> 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<DenominationBreakdownResult> breakdown = decomposeAmount(source.getAmount());
if (current != null) {
if (!current.getQuotaControlBitId().equals(source.getId())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"当前实训已有步骤九记录,如需重新开始请先调用重新实训接口");
}
currencyMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyEntity>()
.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<StandardCurrencyBatchEntity>()
.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<StandardCurrencyBatchEntity>()
.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<BigDecimal> values = expand(decode(batch.getDenominationSummary()));
List<StandardCurrencyEntity> existing = currencyMapper.selectList(
new LambdaQueryWrapper<StandardCurrencyEntity>()
.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<StandardCurrencyEntity>()
.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<StandardCurrencyBatchEntity>()
.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<DenominationBreakdownResult> decomposeAmount(BigDecimal amount) {
long remaining = amount.movePointRight(2).longValueExact();
List<DenominationBreakdownResult> 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<DenominationBreakdownResult> values) {
return values.stream().map(v -> v.getDenomination().setScale(2).toPlainString() + ":" + v.getCount())
.collect(Collectors.joining(","));
}
private List<DenominationBreakdownResult> decode(String summary) {
List<DenominationBreakdownResult> 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<BigDecimal> expand(List<DenominationBreakdownResult> breakdown) {
List<BigDecimal> 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<QuotaControlBitEntity>()
.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<StandardCurrencyBatchEntity>()
.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<StandardCurrencyEntity> currencies = currencyMapper.selectList(
new LambdaQueryWrapper<StandardCurrencyEntity>()
.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, "步骤九状态已变化,不能重复或越级操作");
}
}