Compare commits

..

3 Commits

@ -36,6 +36,13 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version> <!-- 建议使用最新版本 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -104,6 +111,11 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.8.5</version>
</dependency>
</dependencies>
<build>

@ -0,0 +1,17 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "报文确认状态")
public class ConfirmationStatusResult {
@Schema(description = "报文确认状态", allowableValues = {"待确认", "已确认"})
private final String confirmationStatus;
public ConfirmationStatusResult(String confirmationStatus) {
this.confirmationStatus = confirmationStatus;
}
public String getConfirmationStatus() {
return confirmationStatus;
}
}

@ -115,6 +115,11 @@ public class ControlSystemSignatureService {
return new ControlSystemSignatureResult(requireLatest(subject));
}
@Transactional(readOnly = true)
public ControlSystemSignatureResult detail(InstitutionKeySubject subject) {
return new ControlSystemSignatureResult(requireLatest(subject));
}
@Transactional(readOnly = true)
public CommercialBankControlSystemSignatureResult commercialBankMessage(InstitutionKeySubject subject) {
return new CommercialBankControlSystemSignatureResult(latest(subject));

@ -43,12 +43,6 @@ public class CurrencyGenerationRequestService {
long identifierApplicationId = identifier.getApplicationId();
CurrencyGenerationRequest request = repository.findLatestBySubject(subject.getUserId(), subject.getSchoolId(),
subject.getClassId()).orElse(null);
if (request != null && request.getStatus() != CurrencyGenerationRequest.Status.CONCATENATED
&& request.getStatus() != CurrencyGenerationRequest.Status.DIGESTED) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"当前最新货币生成请求已进入" + request.getStatus().name()
+ ",不能重复创建;如需重新开始请先调用重新实现接口");
}
CurrencyGenerationRequest input = invokeDomainWithResult(() -> CurrencyGenerationRequest.prepare(
identifierApplicationId, institutionIdentifier,
identifier.getFullInstitutionIdentifier(), amount, deliveryNodeCode, requestTimestamp));

@ -0,0 +1,24 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
@Schema(description = "面额拆解结果")
public class DenominationDecompositionResult {
private final Integer totalCount;
private final List<DenominationBreakdownResult> breakdown;
public DenominationDecompositionResult(Integer totalCount, List<DenominationBreakdownResult> breakdown) {
this.totalCount = totalCount;
this.breakdown = breakdown;
}
public Integer getTotalCount() {
return totalCount;
}
public List<DenominationBreakdownResult> getBreakdown() {
return breakdown;
}
}

@ -64,13 +64,7 @@ public class InstitutionIdentifierStepService {
subject.getSchoolId(), subject.getClassId()).orElse(null);
if (application == null) {
prepare(normalizedOriginalText, subject, operator);
application = requireCurrent(subject, InstitutionIdentifierApplication.Status.PREPARED,
"当前没有待计算摘要的本轮机构标识数据");
} else if (application.getStatus() != InstitutionIdentifierApplication.Status.PREPARED
&& application.getStatus() != InstitutionIdentifierApplication.Status.DIGESTED) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"当前最新一轮已进入" + application.getStatus().name()
+ ",不能重复创建申请;如需重新开始请先调用重新实现接口");
application = requireCurrent(subject, "当前没有待计算摘要的本轮机构标识数据");
}
InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
String digest = cryptography.sm3(normalizedOriginalText);
@ -87,10 +81,11 @@ public class InstitutionIdentifierStepService {
@Transactional
public InstitutionSignatureResult sign(String privateKey, InstitutionKeySubject subject, String operator) {
InstitutionIdentifierApplication application = requireCurrent(subject,
InstitutionIdentifierApplication.Status.DIGESTED, "当前没有待签名的本轮机构标识数据");
"当前没有待签名的本轮机构标识数据");
if (application.getDigest() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM3摘要计算");
}
long applicationId = application.getId();
requireStatus(application, InstitutionIdentifierApplication.Status.DIGESTED,
"当前步骤不是商业银行签名,不能操作", operator);
InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
String signature = keyService.signCommercialBank(subject, privateKey, application.getDigest());
if (signature == null) {
@ -106,9 +101,11 @@ public class InstitutionIdentifierStepService {
@Transactional
public InstitutionApplicationPackageResult packageApplication(InstitutionKeySubject subject, String operator) {
InstitutionIdentifierApplication application = requireCurrent(subject,
InstitutionIdentifierApplication.Status.SIGNED, "当前没有待组装的本轮机构标识数据");
"当前没有待组装的本轮机构标识数据");
if (application.getBankSignature() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成商业银行签名");
}
long applicationId = application.getId();
requireStatus(application, InstitutionIdentifierApplication.Status.SIGNED, "当前步骤不是申请组装,不能操作", operator);
InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
invokeDomain(application::packageApplication);
repository.update(application, expectedStatus, "PACKAGE",
@ -121,7 +118,10 @@ public class InstitutionIdentifierStepService {
@Transactional
public InstitutionApplicationPackageResult sendApplication(InstitutionKeySubject subject, String operator) {
InstitutionIdentifierApplication application = requireCurrent(subject,
InstitutionIdentifierApplication.Status.PACKAGED, "当前没有待发送的已打包机构标识申请");
"当前没有待发送的已打包机构标识申请");
if (application.getBankSignature() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成申请报文组装");
}
InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
invokeDomain(application::submit);
repository.update(application, expectedStatus, "SEND",
@ -131,17 +131,9 @@ public class InstitutionIdentifierStepService {
application.getBankSignature());
}
private InstitutionIdentifierApplication requireCurrent(InstitutionKeySubject subject,
InstitutionIdentifierApplication.Status status,
String message) {
InstitutionIdentifierApplication application = repository.findLatestBySubject(subject.getUserId(),
subject.getSchoolId(), subject.getClassId()).orElseThrow(() ->
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, message));
if (application.getStatus() != status) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
message + ",当前最新一轮状态:" + application.getStatus().name());
}
return application;
private InstitutionIdentifierApplication requireCurrent(InstitutionKeySubject subject, String message) {
return repository.findLatestBySubject(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, message));
}
private void requireStatus(InstitutionIdentifierApplication application,

@ -0,0 +1,17 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "操作校验结果")
public class OperationBooleanResult {
@Schema(description = "本次校验是否通过")
private final boolean valid;
public OperationBooleanResult(boolean valid) {
this.valid = valid;
}
public boolean isValid() {
return valid;
}
}

@ -0,0 +1,24 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "创建或接收操作结果")
public class OperationIdStatusResult {
@Schema(description = "后续操作所需的业务记录编号")
private final Long verificationId;
@Schema(description = "本次操作完成后的状态")
private final String status;
public OperationIdStatusResult(Long id, String status) {
this.verificationId = id;
this.status = status;
}
public Long getVerificationId() {
return verificationId;
}
public String getStatus() {
return status;
}
}

@ -0,0 +1,17 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "操作生成的报文")
public class OperationMessageResult {
@Schema(description = "本次操作生成的报文")
private final String message;
public OperationMessageResult(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}

@ -0,0 +1,17 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "操作状态")
public class OperationStatusResult {
@Schema(description = "本次操作完成后的状态")
private final String status;
public OperationStatusResult(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
}

@ -0,0 +1,17 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "操作生成值")
public class OperationValueResult {
@Schema(description = "本次操作生成的值")
private final String value;
public OperationValueResult(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}

@ -149,10 +149,14 @@ public class QuotaControlBitService {
public QuotaControlBitResult sign(String privateKey, String completeControlBit, String algorithm,
InstitutionKeySubject subject, String operator) {
QuotaControlBitEntity value = requireLatest(subject);
requireResult(value.getCompleteControlBit(), "请先拼接完整额度控制位");
requireResult(value.getStaticSegment(), "请先生成静态段");
requireResult(value.getDynamicSegment(), "请先生成动态段");
requireResult(value.getCentralBankSignatureSegment(), "请先读取央行签名段");
String expectedControlBit = value.getStaticSegment() + "|" + value.getDynamicSegment() + "|"
+ value.getCentralBankSignatureSegment();
String submittedControlBit = completeControlBit == null ? null : completeControlBit.trim();
if (!value.getCompleteControlBit().equals(submittedControlBit)) {
throw keyError(subject, "完整额度控制位与步骤八拼接结果不一致");
if (!expectedControlBit.equals(submittedControlBit)) {
throw keyError(subject, "完整额度控制位与静态段、动态段及央行签名段拼接结果不一致");
}
if (!"SM2".equalsIgnoreCase(algorithm == null ? "" : algorithm.trim())) {
throw keyError(subject, "签名算法必须选择SM2");
@ -163,6 +167,7 @@ public class QuotaControlBitService {
}
update(value, value.getStatus(), "SIGNED", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getCompleteControlBit, submittedControlBit)
.set(QuotaControlBitEntity::getSigningKeyId, InstitutionKeyService.BANK_SECOND_KEY)
.set(QuotaControlBitEntity::getHashAlgorithm, "SM2")
.set(QuotaControlBitEntity::getBankSignature, signature)

@ -0,0 +1,23 @@
package com.yau.digitalrmb.institutionidentity.application;
import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyEntity;
import lombok.Getter;
import java.math.BigDecimal;
@Getter
public class StandardCurrencyListItemResult {
private final Integer sequenceNumber;
private final BigDecimal denomination;
private final String currencyId;
private final String completeCurrency;
private final Integer status;
public StandardCurrencyListItemResult(StandardCurrencyEntity value) {
this.sequenceNumber = value.getSequenceNumber();
this.denomination = value.getDenomination();
this.currencyId = value.getCurrencyId();
this.completeCurrency = value.getCompleteCurrency();
this.status = "可用".equals(value.getStatus()) || "已生效".equals(value.getStatus()) ? 2 : 1;
}
}

@ -0,0 +1,22 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "标准币串步骤操作结果")
public class StandardCurrencyOperationResult {
private final String status;
private final Integer totalCount;
public StandardCurrencyOperationResult(String status, Integer totalCount) {
this.status = status;
this.totalCount = totalCount;
}
public String getStatus() {
return status;
}
public Integer getTotalCount() {
return totalCount;
}
}

@ -43,11 +43,8 @@ public class StandardCurrencyService {
@Resource private InstitutionTrainingScoreService scoreService;
@Transactional
public StandardCurrencyBatchResult decompose(BigDecimal amount, InstitutionKeySubject subject, String operator) {
public StandardCurrencyBatchResult decompose(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) {
@ -210,6 +207,16 @@ public class StandardCurrencyService {
return result(requireLatest(subject));
}
@Transactional(readOnly = true)
public List<StandardCurrencyListItemResult> currencyList(InstitutionKeySubject subject) {
StandardCurrencyBatchEntity batch = requireLatest(subject);
return currencyMapper.selectList(new LambdaQueryWrapper<StandardCurrencyEntity>()
.eq(StandardCurrencyEntity::getBatchId, batch.getId())
.eq(StandardCurrencyEntity::getDeleted, false)
.orderByAsc(StandardCurrencyEntity::getSequenceNumber))
.stream().map(StandardCurrencyListItemResult::new).collect(Collectors.toList());
}
private List<DenominationBreakdownResult> decomposeAmount(BigDecimal amount) {
long remaining = amount.movePointRight(2).longValueExact();
List<DenominationBreakdownResult> result = new ArrayList<>();

@ -132,8 +132,10 @@ public class TransactionInformationIdentifierService {
@Transactional
public TransactionIdentifierAssemblyResult assemble(InstitutionKeySubject subject, String operator) {
TransactionInformationIdentifier value = requireStatus(subject,
TransactionInformationIdentifier.Status.DIGESTED, "请先使用SM3生成摘要");
TransactionInformationIdentifier value = requireLatest(subject);
if (value.getDigest() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先使用SM3生成摘要");
}
TransactionInformationIdentifier.Status expected = value.getStatus();
invoke(value::assemble);
repository.update(value, expected, "ASSEMBLE", "将固定前缀TXN_与SM3摘要组装为交易信息标识", operator);
@ -196,8 +198,10 @@ public class TransactionInformationIdentifierService {
@Transactional
public QuotaRequestMessageResult packageQuotaRequest(InstitutionKeySubject subject,
String operator) {
TransactionInformationIdentifier value = requireStatus(subject,
TransactionInformationIdentifier.Status.QUOTA_SIGNED, "请先完成步骤五额度请求签名");
TransactionInformationIdentifier value = requireLatest(subject);
if (value.getQuotaSignature() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成步骤五额度请求签名");
}
TransactionInformationIdentifier.Status expected = value.getStatus();
String message = buildQuotaRequestMessage(value);
invoke(() -> value.packageQuotaRequest(message));
@ -207,8 +211,10 @@ public class TransactionInformationIdentifierService {
@Transactional
public QuotaRequestMessageResult sendQuotaRequest(InstitutionKeySubject subject, String operator) {
TransactionInformationIdentifier value = requireStatus(subject,
TransactionInformationIdentifier.Status.QUOTA_PACKAGED, "请先打包步骤五额度请求报文");
TransactionInformationIdentifier value = requireLatest(subject);
if (value.getQuotaRequestMessage() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先打包步骤五额度请求报文");
}
TransactionInformationIdentifier.Status expected = value.getStatus();
invoke(value::sendQuotaRequest);
invoke(value::receiveQuotaRequest);
@ -302,8 +308,10 @@ public class TransactionInformationIdentifierService {
@Transactional(readOnly = true)
public QuotaVerificationResult quotaVerificationDetail(InstitutionKeySubject subject) {
TransactionInformationIdentifier value = requireStatus(subject,
TransactionInformationIdentifier.Status.QUOTA_VERIFIED, "步骤六尚未完成额度请求验签");
TransactionInformationIdentifier value = requireLatest(subject);
if (!Boolean.TRUE.equals(value.getQuotaDigestMatches())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤六尚未完成额度请求验签");
}
return new QuotaVerificationResult(value);
}

@ -91,9 +91,6 @@ public class CurrencyGenerationRequest {
public void replaceDigestInput(long identifierApplicationId, String institutionIdentifier,
String fullInstitutionIdentifier, BigDecimal amount,
String deliveryNodeCode, String requestTimestamp, String digest) {
if (status != Status.CONCATENATED && status != Status.DIGESTED) {
throw new IllegalStateException("当前货币生成请求已进入后续步骤,不能修改摘要输入");
}
CurrencyGenerationRequest replacement = prepare(identifierApplicationId, institutionIdentifier,
fullInstitutionIdentifier, amount, deliveryNodeCode, requestTimestamp);
if (digest == null || !digest.matches("[0-9A-F]{64}")) {
@ -107,6 +104,9 @@ public class CurrencyGenerationRequest {
this.requestTimestamp = replacement.requestTimestamp;
this.requestOriginalText = replacement.requestOriginalText;
this.requestDigest = digest;
this.bankSignature = null;
this.signingKeyId = null;
this.requestMessage = null;
this.status = Status.DIGESTED;
}
@ -120,17 +120,22 @@ public class CurrencyGenerationRequest {
}
public void recordSignature(String keyId, String signature) {
requireStatus(Status.DIGESTED, "当前步骤不是商业银行签名,不能操作");
if (requestDigest == null) {
throw new IllegalStateException("请先完成SM3摘要计算");
}
if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) {
throw new IllegalArgumentException("SM2签名信息不正确");
}
signingKeyId = keyId;
bankSignature = signature;
requestMessage = null;
status = Status.SIGNED;
}
public void packageMessage(String message) {
requireStatus(Status.SIGNED, "当前步骤不是请求报文组装,不能操作");
if (bankSignature == null) {
throw new IllegalStateException("请先完成商业银行签名");
}
if (message == null || message.trim().isEmpty()) {
throw new IllegalArgumentException("请求报文不能为空");
}
@ -139,12 +144,16 @@ public class CurrencyGenerationRequest {
}
public void send() {
requireStatus(Status.PACKAGED, "当前步骤不是请求发送,不能操作");
if (requestMessage == null) {
throw new IllegalStateException("请先完成请求报文组装");
}
status = Status.SENT;
}
public void receive() {
requireStatus(Status.SENT, "央行只能接收已发送的请求");
if (requestMessage == null) {
throw new IllegalStateException("央行只能接收已发送的请求");
}
status = Status.RECEIVED;
}

@ -133,7 +133,9 @@ public class CurrencyGenerationVerification {
}
public void verifyQuota(BigDecimal total, BigDecimal used, BigDecimal remaining) {
requireStatus(Status.DIGEST_MATCHED, "请先匹配摘要一和摘要二");
if (!Boolean.TRUE.equals(digestMatches)) {
throw new IllegalStateException("请先匹配摘要一和摘要二");
}
totalQuota = total;
usedQuota = used;
remainingQuota = remaining;
@ -144,7 +146,9 @@ public class CurrencyGenerationVerification {
}
public void confirm(String keyId, String signature, String time) {
requireStatus(Status.QUOTA_VERIFIED, "请先完成额度检查");
if (!Boolean.TRUE.equals(quotaSufficient)) {
throw new IllegalStateException("请先完成额度检查");
}
if (keyId == null || signature == null || time == null) {
throw new IllegalArgumentException("央行签名确权数据不完整");
}
@ -155,7 +159,9 @@ public class CurrencyGenerationVerification {
}
public void packageResponse(String message) {
requireStatus(Status.CONFIRMED, "请先完成央行签名确权");
if (centralBankSignature == null || confirmationTime == null) {
throw new IllegalStateException("请先完成央行签名确权");
}
if (message == null || message.trim().isEmpty()) {
throw new IllegalArgumentException("确认报文不能为空");
}
@ -164,7 +170,9 @@ public class CurrencyGenerationVerification {
}
public void returnResponse() {
requireStatus(Status.PACKAGED, "请先打包确认报文");
if (confirmationMessage == null) {
throw new IllegalStateException("请先打包确认报文");
}
status = Status.RESPONSE_RETURNED;
}

@ -67,9 +67,6 @@ public class InstitutionIdentifierApplication {
}
public void replaceDigestInput(String bankCode, String timestamp, String originalText, String digest) {
if (status != Status.PREPARED && status != Status.DIGESTED) {
throw new IllegalStateException("当前申请已进入后续步骤,不能修改摘要原文");
}
validateBase(bankCode, timestamp);
this.bankCode = bankCode;
this.timestamp = timestamp;
@ -78,6 +75,13 @@ public class InstitutionIdentifierApplication {
throw new IllegalArgumentException("SM3摘要格式不正确");
}
this.digest = digest;
this.bankSignature = null;
this.verificationKeyId = null;
this.signatureValid = null;
this.digestMatches = null;
this.signingKeyId = null;
this.institutionIdentifier = null;
this.centralBankSignature = null;
this.status = Status.DIGESTED;
}
@ -91,26 +95,40 @@ public class InstitutionIdentifierApplication {
}
public void recordBankSignature(String bankSignature) {
requireStatus(Status.DIGESTED, "当前步骤不是商业银行签名,不能操作");
if (digest == null) {
throw new IllegalStateException("请先完成SM3摘要计算");
}
if (bankSignature == null || !bankSignature.matches("[0-9A-F]+")) {
throw new IllegalArgumentException("银行SM2签名格式不正确");
}
this.bankSignature = bankSignature;
this.verificationKeyId = null;
this.signatureValid = null;
this.digestMatches = null;
this.signingKeyId = null;
this.institutionIdentifier = null;
this.centralBankSignature = null;
this.status = Status.SIGNED;
}
public void packageApplication() {
requireStatus(Status.SIGNED, "当前步骤不是申请组装,不能操作");
if (bankSignature == null) {
throw new IllegalStateException("请先完成商业银行签名");
}
this.status = Status.PACKAGED;
}
public void submit() {
requireStatus(Status.PACKAGED, "当前步骤不是发送申请,不能操作");
if (bankSignature == null) {
throw new IllegalStateException("请先完成申请报文组装");
}
this.status = Status.SUBMITTED;
}
public void recordVerification(String verificationKeyId, boolean signatureValid, boolean digestMatches) {
requireStatus(Status.SUBMITTED, "申请尚未发送至央行,不能进行解密验证");
if (bankSignature == null) {
throw new IllegalStateException("申请尚未发送至央行,不能进行解密验证");
}
this.verificationKeyId = verificationKeyId;
this.signatureValid = signatureValid;
this.digestMatches = digestMatches;
@ -118,7 +136,9 @@ public class InstitutionIdentifierApplication {
}
public void issue(String signingKeyId, String institutionIdentifier, String centralBankSignature) {
requireStatus(Status.VERIFIED, "申请尚未验证通过,不能进行央行签名确权");
if (!Boolean.TRUE.equals(signatureValid) || !Boolean.TRUE.equals(digestMatches)) {
throw new IllegalStateException("申请尚未验证通过,不能进行央行签名确权");
}
if (signingKeyId == null || institutionIdentifier == null || centralBankSignature == null) {
throw new IllegalArgumentException("签发信息不能为空");
}

@ -9,8 +9,8 @@ public class QuotaControlSignRequest {
@NotBlank(message = "商业银行第二私钥不能为空")
private String privateKey;
@NotBlank(message = "完整额度控制位不能为空")
@Schema(description = "前端拼接得到的完整额度控制位,格式为静态段|动态段|央行控制系统签名段",
example = "STATIC_7AD7D47A81F0230562F912AC5CFA6F44D5A851C978191F4DAB8241CF1BFD7971|DYN_YMUL9SQ0_20260807141518|CB_CTRL_30450220ABCDEF")
// @Schema(description = "前端拼接得到的完整额度控制位,格式为静态段|动态段|央行控制系统签名段",
// example = "STATIC_7AD7D47A81F0230562F912AC5CFA6F44D5A851C978191F4DAB8241CF1BFD7971|DYN_YMUL9SQ0_20260807141518|CB_CTRL_30450220ABCDEF")
private String completeControlBit;
@NotBlank(message = "签名算法不能为空")
@Schema(description = "必须为SM2", example = "SM2")

@ -11,6 +11,7 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResu
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.InstitutionVerificationProcessResult;
import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmInstitutionIdentifierRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.VerifyInstitutionApplicationRequest;
@ -102,10 +103,10 @@ public class CentralBankInstitutionIdentifierController {
@PostMapping("/applications/feedback")
@Operation(summary = "央行信息反馈",
description = "将已确权的最终机构标识同步到商业银行端商业银行状态由1变为2。")
public ApiResponse<InstitutionIdentifierFinalResult> feedback() {
public ApiResponse<OperationStatusResult> feedback() {
JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(feedbackService.feedback(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
feedbackService.feedback(InstitutionKeySubject.from(jwtUser), jwtUser.getUsername());
return ApiResponse.success(new OperationStatusResult("FEEDBACK_SENT"), traceId());
}
private String traceId() {

@ -4,6 +4,8 @@ import com.yau.digitalrmb.institutionidentity.application.ControlSystemDigestRes
import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.OperationMessageResult;
import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ControlSystemDigestRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ControlSystemSignRequest;
@ -49,25 +51,21 @@ public class ControlSystemSignatureController {
}
@PostMapping("/steps/package")
@Operation(summary = "打包控制系统签名反馈报文")
public ApiResponse<ControlSystemSignatureResult> packageResponse() {
return execute((subject, operator) -> service.packageResponse(subject, operator));
@Operation(summary = "打包控制系统签名反馈报文", description = "成功响应的data仅返回message。")
public ApiResponse<OperationMessageResult> packageResponse() {
JwtUser user = AuthContextHolder.get();
ControlSystemSignatureResult result = service.packageResponse(
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationMessageResult(result.getResponseMessage()), traceId());
}
@PostMapping("/steps/send")
@Operation(summary = "中央银行发送控制系统签名反馈")
public ApiResponse<ControlSystemSignatureResult> send() {
return execute((subject, operator) -> service.send(subject, operator));
}
private ApiResponse<ControlSystemSignatureResult> execute(OperationExecutor executor) {
@Operation(summary = "中央银行发送控制系统签名反馈", description = "成功响应的data仅返回status。")
public ApiResponse<OperationStatusResult> send() {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(executor.execute(InstitutionKeySubject.from(user), user.getUsername()), traceId());
ControlSystemSignatureResult result = service.send(InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationStatusResult(result.getStatus()), traceId());
}
private String traceId() { return MDC.get(TraceIdFilter.MDC_KEY); }
private interface OperationExecutor {
ControlSystemSignatureResult execute(InstitutionKeySubject subject, String operator);
}
}

@ -4,6 +4,8 @@ import com.yau.digitalrmb.institutionidentity.application.CurrencyDigestResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationRequestService;
import com.yau.digitalrmb.institutionidentity.application.CurrencyRequestResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.OperationMessageResult;
import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyRequestStepRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.GenerateCurrencyDigestRequest;
@ -52,19 +54,21 @@ public class CurrencyGenerationRequestController {
}
@PostMapping("/steps/package")
@Operation(summary = "打包货币生成请求报文")
public ApiResponse<CurrencyRequestResult> packageRequest() {
@Operation(summary = "打包货币生成请求报文", description = "成功响应的data仅返回message。")
public ApiResponse<OperationMessageResult> packageRequest() {
JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(currencyRequestService.packageRequest(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
CurrencyRequestResult result = currencyRequestService.packageRequest(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername());
return ApiResponse.success(new OperationMessageResult(result.getRequestMessage()), traceId());
}
@PostMapping("/steps/send")
@Operation(summary = "发送请求至央行", description = "只允许发送已经完成打包的货币生成请求报文。")
public ApiResponse<CurrencyRequestResult> send() {
@Operation(summary = "发送请求至央行", description = "成功响应的data仅返回status不重复返回请求报文和页面完整数据。")
public ApiResponse<OperationStatusResult> send() {
JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(currencyRequestService.send(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
CurrencyRequestResult result = currencyRequestService.send(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername());
return ApiResponse.success(new OperationStatusResult(result.getStatus()), traceId());
}
@GetMapping("/page")

@ -1,5 +1,6 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.ConfirmationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyConfirmationMessageResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyConfirmationSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyDigestResult;
@ -7,6 +8,10 @@ import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationVeri
import com.yau.digitalrmb.institutionidentity.application.CurrencyQuotaVerificationResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationVerificationService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.OperationBooleanResult;
import com.yau.digitalrmb.institutionidentity.application.OperationIdStatusResult;
import com.yau.digitalrmb.institutionidentity.application.OperationMessageResult;
import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CalculateCurrencyDigestRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmCurrencyRequest;
@ -33,20 +38,25 @@ public class CurrencyGenerationVerificationController {
@PostMapping("/steps/receive")
@Operation(summary = "接收步骤二请求报文",
description = "对应页面“接收步骤二请求报文”按钮。步骤二必须达到RECEIVED重复点击时复用verificationId并将步骤三恢复到RECEIVED清空后续按钮产生的数据。")
public ApiResponse<CurrencyGenerationVerificationResult> receive(
public ApiResponse<OperationIdStatusResult> receive(
@Valid @RequestBody ReceiveCurrencyVerificationRequest request) {
JwtUser user = AuthContextHolder.get();
return ok(service.receive(request.getRequestId(), InstitutionKeySubject.from(user), user.getUsername()));
CurrencyGenerationVerificationResult result = service.receive(request.getRequestId(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationIdStatusResult(result.getVerificationId(), result.getStatus()),
MDC.get(TraceIdFilter.MDC_KEY));
}
@PostMapping("/steps/verify-signature")
@Operation(summary = "第二公钥SM2验证",
description = "对应页面“第二公钥SM2验证”按钮。前端传当前实训主体的商业银行第二公钥完整值后端校验公钥归属并使用该第二公钥执行SM2_Verify(摘要值, 签名值)。")
public ApiResponse<CurrencyGenerationVerificationResult> verifySignature(
public ApiResponse<OperationBooleanResult> verifySignature(
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
return ok(service.verifySignature(request.getVerificationId(), request.getPublicKey(),
InstitutionKeySubject.from(user), user.getUsername()));
CurrencyGenerationVerificationResult result = service.verifySignature(request.getVerificationId(),
request.getPublicKey(), InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationBooleanResult(Boolean.TRUE.equals(result.getSignatureValid())),
MDC.get(TraceIdFilter.MDC_KEY));
}
@PostMapping("/steps/calculate-digest")
@ -62,11 +72,13 @@ public class CurrencyGenerationVerificationController {
@PostMapping("/steps/match-digest")
@Operation(summary = "匹配摘要一和摘要二", description = "对应页面“匹配”按钮,比较重算摘要一与请求报文摘要二。")
public ApiResponse<CurrencyGenerationVerificationResult> matchDigest(
public ApiResponse<OperationBooleanResult> matchDigest(
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
return ok(service.matchDigest(request.getVerificationId(),
InstitutionKeySubject.from(user), user.getUsername()));
CurrencyGenerationVerificationResult result = service.matchDigest(request.getVerificationId(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationBooleanResult(Boolean.TRUE.equals(result.getDigestMatches())),
MDC.get(TraceIdFilter.MDC_KEY));
}
@PostMapping("/steps/verify-quota")
@ -94,27 +106,33 @@ public class CurrencyGenerationVerificationController {
@PostMapping("/steps/package-response")
@Operation(summary = "打包确认报文", description = "对应页面“打包报文”按钮,只组装确认报文,不执行返回。")
public ApiResponse<CurrencyGenerationVerificationResult> packageResponse(
public ApiResponse<OperationMessageResult> packageResponse(
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
return ok(service.packageResponse(request.getVerificationId(),
InstitutionKeySubject.from(user), user.getUsername()));
CurrencyGenerationVerificationResult result = service.packageResponse(request.getVerificationId(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationMessageResult(result.getConfirmationMessage()),
MDC.get(TraceIdFilter.MDC_KEY));
}
@PostMapping("/steps/return-response")
@Operation(summary = "返回确认报文", description = "对应页面“返回确认报文”按钮,只返回已打包完成的确认报文。")
public ApiResponse<CurrencyGenerationVerificationResult> returnResponse(
public ApiResponse<OperationStatusResult> returnResponse(
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
return ok(service.returnResponse(request.getVerificationId(), InstitutionKeySubject.from(user), user.getUsername()));
CurrencyGenerationVerificationResult result = service.returnResponse(request.getVerificationId(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationStatusResult(result.getStatus()), MDC.get(TraceIdFilter.MDC_KEY));
}
@PostMapping("/steps/receive-confirmation")
@Operation(summary = "商业银行接收报文确认信息",
description = "对应商业银行端“接收报文确认信息”按钮。根据当前Token查询本次实训数据confirmationStatus在央行未返回时为“待确认”已返回时为“已确认”查询不改变流程状态。")
public ApiResponse<CurrencyConfirmationMessageResult> receiveConfirmation() {
return ApiResponse.success(service.receiveConfirmation(
InstitutionKeySubject.from(AuthContextHolder.get())), MDC.get(TraceIdFilter.MDC_KEY));
public ApiResponse<ConfirmationStatusResult> receiveConfirmation() {
CurrencyConfirmationMessageResult result = service.receiveConfirmation(
InstitutionKeySubject.from(AuthContextHolder.get()));
return ApiResponse.success(new ConfirmationStatusResult(result.getConfirmationStatus()),
MDC.get(TraceIdFilter.MDC_KEY));
}
@GetMapping("/current")

@ -11,6 +11,8 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResu
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.InstitutionSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.OperationMessageResult;
import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionTrainingResetResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionTrainingResetService;
@ -96,19 +98,21 @@ public class InstitutionIdentifierController {
@PostMapping("/steps/package")
@Operation(summary = "打包机构标识申请",
description = "无请求体仅SIGNED状态可操作。将机构代码、时间戳、SM3摘要和商业银行SM2签名组装成申请报文。")
public ApiResponse<InstitutionApplicationPackageResult> packageApplication() {
public ApiResponse<OperationMessageResult> packageApplication() {
JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(stepService.packageApplication(InstitutionKeySubject.from(jwtUser),
jwtUser.getUsername()), traceId());
InstitutionApplicationPackageResult result = stepService.packageApplication(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername());
return ApiResponse.success(new OperationMessageResult(result.getApplicationMessage()), traceId());
}
@PostMapping("/steps/send")
@Operation(summary = "发送机构标识申请",
description = "无请求体仅PACKAGED状态可操作。将已打包报文发送至央行返回SUBMITTED。")
public ApiResponse<InstitutionApplicationPackageResult> sendApplication() {
public ApiResponse<OperationStatusResult> sendApplication() {
JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(stepService.sendApplication(InstitutionKeySubject.from(jwtUser),
jwtUser.getUsername()), traceId());
InstitutionApplicationPackageResult result = stepService.sendApplication(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername());
return ApiResponse.success(new OperationStatusResult(result.getStatus()), traceId());
}
@GetMapping("/applications/commercial-bank")

@ -2,6 +2,9 @@ package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.CentralBankSignatureSegmentResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.OperationMessageResult;
import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.OperationValueResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitService;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlDynamicSegmentResult;
@ -70,12 +73,16 @@ public class QuotaControlBitController {
}
@PostMapping("/steps/assemble")
@Operation(summary = "拼接完整额度控制位")
public ApiResponse<QuotaControlBitResult> assemble() { return execute((s, o) -> service.assemble(s, o)); }
@Operation(summary = "拼接完整额度控制位", description = "成功响应的data仅返回value值为完整额度控制位。")
public ApiResponse<OperationValueResult> assemble() {
JwtUser user = AuthContextHolder.get();
QuotaControlBitResult result = service.assemble(InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationValueResult(result.getCompleteControlBit()), traceId());
}
@PostMapping("/steps/sign")
@Operation(summary = "商业银行第二私钥签名确认",
description = "前端提交完整额度控制位、商业银行第二SM2私钥和SM2算法。后端校验完整额度控制位与步骤八拼接结果一致后进行签名错误内容、私钥或算法会累计实训错误次数。")
description = "前端提交由静态段、动态段和央行签名段拼接的完整额度控制位以及商业银行第二SM2私钥和SM2算法无需提前调用后端拼接接口。后端校验三段拼接结果一致后落库并签名错误内容、私钥或算法会累计实训错误次数。")
public ApiResponse<SignatureResult> sign(@Valid @RequestBody QuotaControlSignRequest request) {
JwtUser user = AuthContextHolder.get();
QuotaControlBitResult result = service.sign(request.getPrivateKey(), request.getCompleteControlBit(),
@ -84,12 +91,20 @@ public class QuotaControlBitController {
}
@PostMapping("/steps/package")
@Operation(summary = "打包额度控制位数据包报文")
public ApiResponse<QuotaControlBitResult> packageRequest() { return execute((s, o) -> service.packageRequest(s, o)); }
@Operation(summary = "打包额度控制位数据包报文", description = "成功响应的data仅返回message值为本次打包的额度控制位请求报文。")
public ApiResponse<OperationMessageResult> packageRequest() {
JwtUser user = AuthContextHolder.get();
QuotaControlBitResult result = service.packageRequest(InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationMessageResult(result.getRequestMessage()), traceId());
}
@PostMapping("/steps/send")
@Operation(summary = "商业银行发送额度控制位数据包报文")
public ApiResponse<QuotaControlBitResult> send() { return execute((s, o) -> service.send(s, o)); }
@Operation(summary = "商业银行发送额度控制位数据包报文", description = "成功响应的data仅返回status不重复返回已打包报文和完整业务数据。")
public ApiResponse<OperationStatusResult> send() {
JwtUser user = AuthContextHolder.get();
QuotaControlBitResult result = service.send(InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationStatusResult(result.getStatus()), traceId());
}
@GetMapping
@Operation(summary = "查询步骤八商业银行端完整数据")
@ -97,10 +112,5 @@ public class QuotaControlBitController {
return ApiResponse.success(service.detail(InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private ApiResponse<QuotaControlBitResult> execute(Executor executor) {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(executor.execute(InstitutionKeySubject.from(user), user.getUsername()), traceId());
}
private String traceId() { return MDC.get(TraceIdFilter.MDC_KEY); }
private interface Executor { QuotaControlBitResult execute(InstitutionKeySubject subject, String operator); }
}

@ -1,6 +1,8 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.OperationMessageResult;
import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaRequestDigestResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaRequestMessageResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaRequestSignatureResult;
@ -62,20 +64,21 @@ public class QuotaRequestController {
}
@PostMapping("/steps/package")
@Operation(summary = "打包额度请求报文")
public ApiResponse<QuotaRequestMessageResult> packageRequest() {
@Operation(summary = "打包额度请求报文", description = "成功响应的data仅返回message。")
public ApiResponse<OperationMessageResult> packageRequest() {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.packageQuotaRequest(
InstitutionKeySubject.from(user), user.getUsername()), traceId());
QuotaRequestMessageResult result = service.packageQuotaRequest(
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationMessageResult(toMessage(result)), traceId());
}
@PostMapping("/steps/send")
@Operation(summary = "商业银行发送额度请求报文",
description = "发送成功即视为央行已收到央行查询状态由1未发送变为2已发送。")
public ApiResponse<QuotaRequestMessageResult> send() {
description = "发送成功即视为央行已收到成功响应的data仅返回status。")
public ApiResponse<OperationStatusResult> send() {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.sendQuotaRequest(
InstitutionKeySubject.from(user), user.getUsername()), traceId());
service.sendQuotaRequest(InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationStatusResult("SENT"), traceId());
}
@GetMapping
@ -85,6 +88,15 @@ public class QuotaRequestController {
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String toMessage(QuotaRequestMessageResult result) {
return "{\"quotaRequestId\":\"" + result.getQuotaRequestId()
+ "\",\"amount\":" + result.getAmount()
+ ",\"orgId\":\"" + result.getOrgId()
+ "\",\"txnId\":\"" + result.getTxnId()
+ "\",\"digest\":\"" + result.getDigest()
+ "\",\"signature\":\"" + result.getSignature() + "\"}";
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}

@ -1,9 +1,11 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.DenominationDecompositionResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyBatchResult;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyListItemResult;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyOperationResult;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyService;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyDecompositionRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyRuleRequest;
import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter;
@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
@Validated
@RestController
@ -30,27 +33,40 @@ public class StandardCurrencyController {
@Resource private StandardCurrencyService service;
@PostMapping("/steps/decompose")
@Operation(summary = "执行面额拆解", description = "按100、50、20、10、5、1、0.5、0.2、0.1、0.05、0.01从大到小拆解。")
public ApiResponse<StandardCurrencyBatchResult> decompose(
@Valid @RequestBody CurrencyDecompositionRequest request) {
@Operation(summary = "执行面额拆解",
description = "无需请求体。后端按当前JWT主体查询步骤八已确认额度控制位金额并按100、50、20、10、5、1、0.5、0.2、0.1、0.05、0.01从大到小拆解。")
public ApiResponse<DenominationDecompositionResult> decompose() {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.decompose(request.getAmount(), InstitutionKeySubject.from(user),
user.getUsername()), traceId());
StandardCurrencyBatchResult result = service.decompose(
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new DenominationDecompositionResult(
result.getTotalCount(), result.getBreakdown()), traceId());
}
@PostMapping("/steps/confirm-rule")
@Operation(summary = "确认币串生成规则", description = "校验DC前缀、币串ID规则和待生效初始状态。")
public ApiResponse<StandardCurrencyBatchResult> confirmRule(@Valid @RequestBody CurrencyRuleRequest request) {
public ApiResponse<StandardCurrencyOperationResult> confirmRule(@Valid @RequestBody CurrencyRuleRequest request) {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.confirmRule(request.getPrefix(), request.getIdRule(),
request.getCurrencyStatus(), InstitutionKeySubject.from(user), user.getUsername()), traceId());
StandardCurrencyBatchResult result = service.confirmRule(request.getPrefix(), request.getIdRule(),
request.getCurrencyStatus(), InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new StandardCurrencyOperationResult(
result.getStatus(), result.getTotalCount()), traceId());
}
@PostMapping("/steps/generate")
@Operation(summary = "逐枚生成标准面额币串", description = "根据拆解清单为每一枚标准面额生成唯一币串。")
public ApiResponse<StandardCurrencyBatchResult> generate() {
public ApiResponse<StandardCurrencyOperationResult> generate() {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.generate(InstitutionKeySubject.from(user), user.getUsername()), traceId());
StandardCurrencyBatchResult result = service.generate(
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new StandardCurrencyOperationResult(
result.getStatus(), result.getTotalCount()), traceId());
}
@GetMapping("/currencies")
@Operation(summary = "查询标准面额币串列表", description = "返回当前用户生成的全部币串status=1表示待生效status=2表示已生效。")
public ApiResponse<List<StandardCurrencyListItemResult>> currencies() {
return ApiResponse.success(service.currencyList(InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@GetMapping

@ -0,0 +1,89 @@
package com.yau.digitalrmb.shared.config;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
/**
*
*
* @author
*/
public interface IFileUtil {
/**
* http
*
* @param file formhttp
* @return
*/
String upload(MultipartFile file);
/**
*
*
* @param objectName
* @param fileIn
* @return
*/
String upload(String objectName, InputStream fileIn);
/**
*
*
* @param path
* @return true
*/
boolean remove(String path);
/**
*
*
* @param response
* @param fileName
* @param filePath
*/
void download(HttpServletResponse response, String fileName, String filePath);
/**
*
*
* @param response
* @param filePath
*/
void download(HttpServletResponse response, String filePath);
/**
*
*
* @param relativePath
* @return
*/
String getFullPath(String relativePath);
/**
*
*
* @param fileName
* @return suffix
*/
String getSuffix(String fileName);
/**
*
*
* @param fileName
* @param suffix
* @return
*/
String getDiskRelativePath(String fileName, String suffix);
}

@ -0,0 +1,178 @@
package com.yau.digitalrmb.shared.config;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@Component
public class LocalFileUtil implements IFileUtil{
private final static List<String> excludeSp = Arrays.asList("exe", "bin", "sh");
private final String localPath;
public LocalFileUtil(@Value("${file.path}")String localPath) {
this.localPath = localPath;
}
@Override
public String upload(MultipartFile file) {
return upload(null, file);
}
@Override
public String upload(String objectName, InputStream fileIn) {
Assert.notNull(fileIn, "文件不能为空");
Assert.hasText(objectName, "文件名为空");
Assert.isTrue(objectName.lastIndexOf(".") > 0, "文件名称需携带扩展后缀");
FileOutputStream out = null;
try {
String relativePath = getDiskRelativePath(objectName, null);
File file = new File(getFullPath(relativePath));
file.getParentFile().mkdirs();
out = new FileOutputStream(file);
byte[] buff = new byte[4096];
int len;
while ((len = fileIn.read(buff)) != -1) {
out.write(buff, 0, len);
}
out.flush();
return relativePath;
} catch (IOException e) {
e.printStackTrace();
throw new IllegalArgumentException("上传文件失败,IO错误");
} finally {
try {
fileIn.close();
if (out != null) out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public boolean remove(String path) {
if (!StringUtils.hasText(path)) return false;
File file = new File(getFullPath(path));
if (!file.exists()) return false;
return file.delete();
}
@Override
public void download(HttpServletResponse response, String fileName, String relativePath) {
Assert.hasText(relativePath, "路径为空");
System.out.println("------------------------------------" + getFullPath(relativePath));
File file = new File(getFullPath(relativePath));
Assert.isTrue(file.exists(), "附件不存在");
String sp = relativePath.substring(relativePath.lastIndexOf(".") + 1);
Assert.isTrue(!excludeSp.contains(sp.toLowerCase()), "文件类型错误");
if (!StringUtils.hasText(fileName)) {
fileName = file.getName().substring(0, file.getName().lastIndexOf("."));
}
InputStream fis = null;
try {
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode((fileName + "." + sp), CharsetUtil.UTF_8));
fis = new BufferedInputStream(Files.newInputStream(file.toPath()));
response.addHeader("Content-Length", String.valueOf(fis.available()));
byte[] buff = new byte[4096];
int len;
while ((len = fis.read(buff)) != -1) {
response.getOutputStream().write(buff, 0, len);
}
} catch (Exception e) {
throw new IllegalArgumentException("下载文件失败: " + e.getMessage());
} finally {
try {
if (fis != null) fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void download(HttpServletResponse response, String relativePath) {
download(response, null, relativePath);
}
@Override
public String getFullPath(String relativePath) {
if (!relativePath.startsWith("/")) {
relativePath = "/" + relativePath;
}
return localPath + relativePath;
}
@Override
public String getSuffix(String fileName) {
Assert.hasText(fileName, "文件名不存在!");
if (fileName.lastIndexOf(".") < 0) {
return null;
}
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
public String upload(String fileName, MultipartFile file) {
return upload(fileName, file, null);
}
@Override
public String getDiskRelativePath(String fileName, String suffix) {
Calendar c = Calendar.getInstance();
Date d = new Date();
c.setTime(d);
String year = Integer.toString(c.get(Calendar.YEAR));
String month = Integer.toString(c.get(Calendar.MONTH) + 1);
String day = Integer.toString(c.get(Calendar.DATE));
StringBuilder path = new StringBuilder();
path.append("/").append(year)
.append("/").append(month)
.append("/").append(day)
.append("/").append(fileName);
if (StringUtils.hasText(suffix)) path.append(".").append(suffix);
return path.toString();
}
private String upload(String fileName, MultipartFile file, String relativePath) {
Assert.isTrue(!file.isEmpty(), "文件不存在");
String originalFilename = file.getOriginalFilename();
if (fileName == null) fileName = IdUtil.simpleUUID();
String sp = getSuffix(originalFilename);
Assert.notNull(sp, "文件类型错误");
Assert.isTrue(!excludeSp.contains(sp.toLowerCase()), "文件类型错误");
try {
String filePath;
if (relativePath == null) {
filePath = getDiskRelativePath(fileName, sp);
} else {
relativePath = relativePath.endsWith("/") ? relativePath : relativePath + "/";
filePath = relativePath + fileName + "." + sp;
}
File destFile = new File(getFullPath(filePath));
destFile.getParentFile().mkdirs();
file.transferTo(destFile);
return filePath;
} catch (FileNotFoundException e) {
throw new IllegalArgumentException("上传文件失败,FileNotFound错误");
} catch (IOException e) {
throw new IllegalArgumentException("上传文件失败,IO错误");
}
}
}

@ -0,0 +1,65 @@
package com.yau.digitalrmb.shared.config;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import reactor.core.publisher.Flux;
/**
*
*/
@Getter
public class ResultDataEntity<T> {
/**
*
*/
private final Integer code;
/**
*
*/
// @JsonInclude(JsonInclude.Include.NON_NULL)
private String msg = null;
/**
*
*/
// @JsonInclude(JsonInclude.Include.NON_NULL)
private T data = null;
private Flux<String> flow;
public ResultDataEntity(T data) {
this.code = HttpStatus.OK.value();
this.data = data;
}
public ResultDataEntity(HttpStatus status, T data) {
this.code = status.value();
this.data = data;
}
public ResultDataEntity(HttpStatus status, Flux<String> data) {
this.code = status.value();
this.flow = data;
}
public ResultDataEntity(HttpStatus status, String msg, T data) {
this.code = status.value();
this.msg = msg;
this.data = data;
}
public ResultDataEntity(String msg) {
this.code = HttpStatus.OK.value();
this.msg = msg;
}
public ResultDataEntity(HttpStatus status, String msg) {
this.code = status.value();
this.msg = msg;
}
public ResultDataEntity(HttpStatus status) {
this.code = status.value();
}
}

@ -0,0 +1,44 @@
package com.yau.digitalrmb.shared.config;
import io.swagger.annotations.ApiModel;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import reactor.core.publisher.Flux;
/**
* ResponseEntity
*
*/
@ApiModel("API返回对象")
@Getter
public class ResultEntity<T> extends ResponseEntity<ResultDataEntity<T>> {
public ResultEntity(T data) {
super(new ResultDataEntity<>(HttpStatus.OK, data), HttpStatus.OK);
}
public ResultEntity(HttpStatus status, T data) {
super(new ResultDataEntity<>(status, data), status);
}
public ResultEntity(HttpStatus status, String msg, T data) {
super(new ResultDataEntity<>(status, msg, data), status);
}
public ResultEntity(String msg) {
super(new ResultDataEntity<>(HttpStatus.OK, msg), HttpStatus.OK);
}
public ResultEntity(HttpStatus status, String msg) {
super(new ResultDataEntity<>(status, msg), status);
}
public ResultEntity(HttpStatus status) {
super(new ResultDataEntity<>(status), status);
}
public ResultEntity(HttpStatus status, Flux<String> flow) {
super(new ResultDataEntity<>(status, flow), status);
}
}

@ -0,0 +1,225 @@
package com.yau.digitalrmb.shared.interfaces;
import com.yau.digitalrmb.shared.config.IFileUtil;
import com.yau.digitalrmb.shared.config.ResultEntity;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
*
*
*/
@Slf4j
@Tag(name = "文件管理")
@RestController
@Validated
public class FileManagementController {
@Autowired
private IFileUtil fileUtil;
/**
*
*
*/
@Operation(summary = "上传文件")
@PostMapping("api/file/upload")
public ResultEntity<Map<String, String>> uploadFile(
@ApiParam("文件对象") @RequestPart MultipartFile file) {
try {
if (file == null || file.isEmpty()) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件不能为空");
}
String originalFilename = file.getOriginalFilename();
if (!StringUtils.hasText(originalFilename)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件名不能为空");
}
String suffix = fileUtil.getSuffix(originalFilename);
if (!StringUtils.hasText(suffix)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件类型错误,文件必须包含扩展名");
}
String relativePath = fileUtil.upload(file);
Map<String, String> result = new HashMap<>();
result.put("relativePath", relativePath);
result.put("originalFilename", originalFilename);
result.put("fileSize", String.valueOf(file.getSize()));
result.put("suffix", suffix);
log.info("文件上传成功: 原始文件名={}, 保存路径={}, 文件大小={}",
originalFilename, relativePath, file.getSize());
return new ResultEntity<>(result);
} catch (IllegalArgumentException e) {
log.error("文件上传失败: {}", e.getMessage());
return new ResultEntity<>(HttpStatus.BAD_REQUEST, e.getMessage());
} catch (Exception e) {
log.error("文件上传异常", e);
return new ResultEntity<>(HttpStatus.INTERNAL_SERVER_ERROR, "文件上传失败: " + e.getMessage());
}
}
/**
*
* 使
*/
@Operation(summary = "下载文件(使用原始文件名)")
@GetMapping("api/file/download")
public void downloadFile(
@ApiParam("文件相对路径") @RequestParam String relativePath,
HttpServletResponse response) {
try {
if (!StringUtils.hasText(relativePath)) {
throw new IllegalArgumentException("文件路径不能为空");
}
log.info("文件下载请求: 路径={}", relativePath);
fileUtil.download(response, relativePath);
} catch (IllegalArgumentException e) {
log.error("文件下载失败: {}", e.getMessage());
try {
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\":\"" + e.getMessage() + "\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
} catch (Exception e) {
log.error("文件下载异常", e);
try {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\":\"文件下载失败: " + e.getMessage() + "\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
}
}
/**
*
*
*/
@Operation(summary = "下载文件(自定义文件名)")
@GetMapping("api/file/download/custom")
public void downloadFileWithCustomName(
@ApiParam("文件相对路径") @RequestParam String relativePath,
@ApiParam("自定义文件名(不含扩展名)") @RequestParam String fileName,
HttpServletResponse response) {
try {
if (!StringUtils.hasText(relativePath)) {
throw new IllegalArgumentException("文件路径不能为空");
}
if (!StringUtils.hasText(fileName)) {
throw new IllegalArgumentException("文件名不能为空");
}
log.info("文件下载请求: 路径={}, 自定义文件名={}", relativePath, fileName);
fileUtil.download(response, fileName, relativePath);
} catch (IllegalArgumentException e) {
log.error("文件下载失败: {}", e.getMessage());
try {
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\":\"" + e.getMessage() + "\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
} catch (Exception e) {
log.error("文件下载异常", e);
try {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\":\"文件下载失败: " + e.getMessage() + "\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
}
}
/**
*
*
*/
@Operation(summary = "删除文件")
@PostMapping("api/file/delete")
public ResultEntity<Map<String, Object>> deleteFile(
@ApiParam("文件相对路径") @RequestParam String relativePath) {
try {
if (!StringUtils.hasText(relativePath)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件路径不能为空");
}
boolean deleted = fileUtil.remove(relativePath);
Map<String, Object> result = new HashMap<>();
result.put("relativePath", relativePath);
result.put("deleted", deleted);
if (deleted) {
log.info("文件删除成功: 路径={}", relativePath);
result.put("message", "文件删除成功");
} else {
log.info("文件不存在或已被删除: 路径={}", relativePath);
result.put("message", "文件不存在或已被删除");
}
return new ResultEntity<>(result);
} catch (Exception e) {
log.error("文件删除异常: 路径={}", relativePath, e);
return new ResultEntity<>(HttpStatus.INTERNAL_SERVER_ERROR, "文件删除失败: " + e.getMessage());
}
}
/**
*
*
*/
@Operation(summary = "获取文件完整路径")
@GetMapping("api/file/fullPath")
public ResultEntity<Map<String, String>> getFullPath(
@ApiParam("文件相对路径") @RequestParam String relativePath) {
try {
if (!StringUtils.hasText(relativePath)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件路径不能为空");
}
String fullPath = fileUtil.getFullPath(relativePath);
Map<String, String> result = new HashMap<>();
result.put("relativePath", relativePath);
result.put("fullPath", fullPath);
return new ResultEntity<>(result);
} catch (Exception e) {
log.error("获取文件完整路径失败", e);
return new ResultEntity<>(HttpStatus.INTERNAL_SERVER_ERROR, "获取路径失败: " + e.getMessage());
}
}
}

@ -19,3 +19,7 @@ platform-integration:
security:
jwt:
secret: 0123456789012345678901234567890123456789012345678901234567890123
file:
type: local
path: D:\新建文件夹

@ -0,0 +1,9 @@
-- Execute the duplicate check first. It must return no rows before adding the constraint.
SELECT request_id, COUNT(*) AS duplicate_count
FROM currency_generation_verification
GROUP BY request_id
HAVING COUNT(*) > 1;
-- MySQL 8+: add the business uniqueness constraint to an existing table.
ALTER TABLE currency_generation_verification
ADD CONSTRAINT uk_currency_verification_request UNIQUE (request_id);

@ -187,6 +187,7 @@ CREATE TABLE IF NOT EXISTS currency_generation_verification (
created_by VARCHAR(64) NOT NULL,
updated_by VARCHAR(64) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT uk_currency_verification_request UNIQUE (request_id),
CONSTRAINT fk_currency_verification_request
FOREIGN KEY (request_id) REFERENCES currency_generation_request(id)
);

@ -273,30 +273,20 @@ class TransactionInformationIdentifierControllerTest {
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepFiveRecordId);
mvc.perform(post(QUOTA_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.quotaRequestMessage").doesNotExist());
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.message").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("\"quotaRequestId\""),
org.hamcrest.Matchers.containsString("\"amount\":1589.85"),
org.hamcrest.Matchers.containsString("\"orgId\":\"ORG_3A4B5C6D7E8F\""),
org.hamcrest.Matchers.containsString("\"txnId\":\"TXN_"))));
mvc.perform(get(CENTRAL_QUOTA_REQUEST_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.length()").value(7));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.centralBankReceiveStatus").doesNotExist());
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").value("SENT"));
mvc.perform(get(QUOTA_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6));
@ -469,8 +459,8 @@ class TransactionInformationIdentifierControllerTest {
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.responseMessage").value(org.hamcrest.Matchers.containsString(
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.message").value(org.hamcrest.Matchers.containsString(
"\"cbCtrlSignature\"")));
mvc.perform(get(COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
@ -584,12 +574,10 @@ class TransactionInformationIdentifierControllerTest {
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.completeControlBit").doesNotExist());
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.completeControlBit").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("|DYN_"),
org.hamcrest.Matchers.containsString("|CB_CTRL_"))));
String completeControlBit = quotaControlBitMapper.selectList(null).get(0).getCompleteControlBit();
QuotaControlBitEntity beforeSign = quotaControlBitMapper.selectList(null).get(0);
assertThat(beforeSign.getCompleteControlBit()).isNull();
String completeControlBit = beforeSign.getStaticSegment() + "|" + beforeSign.getDynamicSegment() + "|"
+ beforeSign.getCentralBankSignatureSegment();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
@ -603,7 +591,7 @@ class TransactionInformationIdentifierControllerTest {
+ completeControlBit + "_TAMPERED\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("完整额度控制位与步骤八拼接结果不一致"),
org.hamcrest.Matchers.containsString("完整额度控制位与静态段、动态段及央行签名段拼接结果不一致"),
org.hamcrest.Matchers.containsString("第5次错误"))));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
@ -618,9 +606,11 @@ class TransactionInformationIdentifierControllerTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(1));
assertThat(quotaControlBitMapper.selectList(null).get(0).getCompleteControlBit())
.isEqualTo(completeControlBit);
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.containsString("\"quotaId\"")));
.andExpect(status().isOk()).andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.message").value(org.hamcrest.Matchers.containsString("\"quotaId\"")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization))
.andExpect(status().isOk())
@ -649,8 +639,8 @@ class TransactionInformationIdentifierControllerTest {
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RECEIVED"))
.andExpect(jsonPath("$.data.receiveStatus").value(2));
.andExpect(status().isOk()).andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").value("RECEIVED"));
mvc.perform(get(CENTRAL_QUOTA_CONTROL_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(2))
@ -684,15 +674,9 @@ class TransactionInformationIdentifierControllerTest {
flowsUserInputThroughCompleteStepEightAndAccumulatesErrors();
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/decompose")
.header("Authorization", authorization).contentType("application/json")
.content("{\"amount\":1590.85}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第7次错误")));
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/decompose")
.header("Authorization", authorization).contentType("application/json")
.content("{\"amount\":1589.85}"))
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("DECOMPOSED"))
.andExpect(jsonPath("$.data.length()").value(2))
.andExpect(jsonPath("$.data.totalCount").value(27))
.andExpect(jsonPath("$.data.breakdown[0].denomination").value(100.0))
.andExpect(jsonPath("$.data.breakdown[0].count").value(15))
@ -703,7 +687,7 @@ class TransactionInformationIdentifierControllerTest {
.content("{\"prefix\":\"RMB\",\"idRule\":\"批次号+枚序号+随机校验码\"," +
"\"currencyStatus\":\"待生效\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第8次错误")));
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第7次错误")));
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/confirm-rule")
.header("Authorization", authorization).contentType("application/json")
.content("{\"prefix\":\"DC\",\"idRule\":\"批次号+枚序号+随机校验码\"," +
@ -712,16 +696,23 @@ class TransactionInformationIdentifierControllerTest {
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/generate")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(2))
.andExpect(jsonPath("$.data.status").value("GENERATED"))
.andExpect(jsonPath("$.data.currencies.length()").value(27))
.andExpect(jsonPath("$.data.currencies[0].sequenceNumber").value(1))
.andExpect(jsonPath("$.data.currencies[0].denomination").value(100.0))
.andExpect(jsonPath("$.data.currencies[0].currencyId").value(
.andExpect(jsonPath("$.data.totalCount").value(27))
.andExpect(jsonPath("$.data.currencies").doesNotExist());
mvc.perform(get(STANDARD_CURRENCY_BASE_PATH + "/currencies")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(27))
.andExpect(jsonPath("$.data[0].sequenceNumber").value(1))
.andExpect(jsonPath("$.data[0].denomination").value(100.0))
.andExpect(jsonPath("$.data[0].currencyId").value(
org.hamcrest.Matchers.matchesPattern("DC_[0-9]{8}_[0-9]{3}_001_[0-9A-Z]{6}")))
.andExpect(jsonPath("$.data.currencies[0].completeCurrency").value(
.andExpect(jsonPath("$.data[0].completeCurrency").value(
org.hamcrest.Matchers.containsString("_ORG_3A4B5C6D7E8F_100.00_")))
.andExpect(jsonPath("$.data.currencies[26].denomination").value(0.05))
.andExpect(jsonPath("$.data.currencies[26].status").value("待生效"));
.andExpect(jsonPath("$.data[0].status").value(1))
.andExpect(jsonPath("$.data[0].randomCheckCode").doesNotExist())
.andExpect(jsonPath("$.data[0].institutionIdentifier").doesNotExist());
List<StandardCurrencyEntity> currencies = standardCurrencyMapper.selectList(null);
assertThat(standardCurrencyBatchMapper.selectList(null)).hasSize(1);
@ -729,13 +720,25 @@ class TransactionInformationIdentifierControllerTest {
assertThat(currencies.stream().map(StandardCurrencyEntity::getDenomination)
.reduce(BigDecimal.ZERO, BigDecimal::add)).isEqualByComparingTo("1589.85");
assertThat(currencies.stream().map(StandardCurrencyEntity::getCurrencyId).distinct().count()).isEqualTo(27);
StandardCurrencyEntity activated = currencies.get(0);
activated.setStatus("可用");
standardCurrencyMapper.updateById(activated);
mvc.perform(get(STANDARD_CURRENCY_BASE_PATH + "/currencies")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(27))
.andExpect(jsonPath("$.data[0].status").value(2))
.andExpect(jsonPath("$.data[1].status").value(1));
activated.setStatus("待生效");
standardCurrencyMapper.updateById(activated);
List<Long> currencyIds = currencies.stream().map(StandardCurrencyEntity::getId)
.collect(java.util.stream.Collectors.toList());
long batchId = standardCurrencyBatchMapper.selectList(null).get(0).getId();
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/generate").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("GENERATED"))
.andExpect(jsonPath("$.data.currencies.length()").value(27));
.andExpect(jsonPath("$.data.totalCount").value(27))
.andExpect(jsonPath("$.data.currencies").doesNotExist());
assertThat(standardCurrencyBatchMapper.selectList(null)).hasSize(1);
assertThat(standardCurrencyBatchMapper.selectList(null).get(0).getId()).isEqualTo(batchId);
assertThat(standardCurrencyMapper.selectList(null)).hasSize(27);
@ -746,8 +749,7 @@ class TransactionInformationIdentifierControllerTest {
@Test
void rejectsStepNineUntilStepEightIsReceived() throws Exception {
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/decompose")
.header("Authorization", authorization).contentType("application/json")
.content("{\"amount\":1589.85}"))
.header("Authorization", authorization))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤八尚未完成央行接收")));

Loading…
Cancel
Save