Compare commits

..

No commits in common. 'f2314bd8214002e85c0778a1c06138c598420f9f' and 'df8381b26ef1549463b170ba05a28f74e10ab7c1' have entirely different histories.

@ -1,37 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import com.yau.digitalrmb.institutionidentity.infrastructure.QuotaControlBitEntity;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "中央银行接收额度控制位报文查询结果")
public class CentralBankQuotaControlBitResult {
@Schema(description = "确认状态:1-待确认,2-已确认", example = "2")
private final int status;
@Schema(description = "额度控制位编号", example = "QC_20260730_001")
private final String quotaId;
@Schema(description = "静态段")
private final String staticSegment;
@Schema(description = "动态段", example = "DYN_7G8H9I0J_20260730153600")
private final String dynamicSegment;
@Schema(description = "中央银行控制系统签名段")
private final String cbSignatureSegment;
@Schema(description = "商业银行第二私钥签名")
private final String bankSignature;
public CentralBankQuotaControlBitResult(QuotaControlBitEntity value) {
boolean confirmed = value != null && "RECEIVED".equals(value.getStatus());
status = confirmed ? 2 : 1;
quotaId = value == null ? null : value.getQuotaId();
staticSegment = value == null ? null : value.getStaticSegment();
dynamicSegment = value == null ? null : value.getDynamicSegment();
cbSignatureSegment = value == null ? null : value.getCentralBankSignatureSegment();
bankSignature = value == null ? null : value.getBankSignature();
}
public int getStatus() { return status; }
public String getQuotaId() { return quotaId; }
public String getStaticSegment() { return staticSegment; }
public String getDynamicSegment() { return dynamicSegment; }
public String getCbSignatureSegment() { return cbSignatureSegment; }
public String getBankSignature() { return bankSignature; }
}

@ -1,45 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
@ApiModel(description = "中央银行额度请求接收结果")
@Schema(description = "中央银行额度请求接收结果")
public class CentralBankQuotaRequestResult {
@Schema(description = "确认状态:1-待确认,2-已确认", example = "2")
private final int status;
@Schema(description = "额度请求编号", example = "QR_20260730_001")
private final String quotaRequestId;
@Schema(description = "申请金额", example = "1589.85")
private final BigDecimal amount;
@Schema(description = "机构标识", example = "ORG_3A4B5C6D7E8F")
private final String orgId;
@Schema(description = "交易信息标识")
private final String txnId;
@Schema(description = "额度请求SM3摘要")
private final String digest;
@Schema(description = "商业银行第二私钥生成的SM2签名")
private final String signature;
public CentralBankQuotaRequestResult(TransactionInformationIdentifier value) {
this.status = value.getStatus().ordinal()
>= TransactionInformationIdentifier.Status.QUOTA_RECEIVED.ordinal() ? 2 : 1;
this.quotaRequestId = value.getQuotaRequestId();
this.amount = value.getAmount();
this.orgId = value.getInstitutionIdentifier();
this.txnId = value.getTransactionIdentifier();
this.digest = value.getQuotaDigest();
this.signature = value.getQuotaSignature();
}
public int getStatus() { return status; }
public String getQuotaRequestId() { return quotaRequestId; }
public BigDecimal getAmount() { return amount; }
public String getOrgId() { return orgId; }
public String getTxnId() { return txnId; }
public String getDigest() { return digest; }
public String getSignature() { return signature; }
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
@ApiModel(description = "央行控制系统签名段读取结果")
@Schema(description = "央行控制系统签名段读取结果")
public class CentralBankSignatureSegmentResult {
@Schema(description = "步骤七央行控制系统签名段", example = "CB_CTRL_30450220...")
private final String centralBankSignatureSegment;
public CentralBankSignatureSegmentResult(String centralBankSignatureSegment) {
this.centralBankSignatureSegment = centralBankSignatureSegment;
}
public String getCentralBankSignatureSegment() { return centralBankSignatureSegment; }
}

@ -1,38 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import com.yau.digitalrmb.institutionidentity.infrastructure.ControlSystemSignatureEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
@Schema(description = "商业银行接收控制系统签名报文结果")
public class CommercialBankControlSystemSignatureResult {
@Schema(description = "接收状态:1-未收到,2-已收到", example = "2")
private final int status;
@Schema(description = "控制系统签名编号", example = "CTRL_20260730_001")
private final String ctrlId;
@Schema(description = "交易金额", example = "1589.85")
private final BigDecimal amount;
@Schema(description = "机构标识", example = "ORG_3A4B5C6D7E8F")
private final String orgId;
@Schema(description = "交易信息标识")
private final String txnId;
@Schema(description = "中央银行控制系统SM2签名")
private final String cbCtrlSignature;
public CommercialBankControlSystemSignatureResult(ControlSystemSignatureEntity value) {
this.status = value != null && "SENT".equals(value.getStatus()) ? 2 : 1;
this.ctrlId = value == null ? null : value.getControlId();
this.amount = value == null ? null : value.getAmount();
this.orgId = value == null ? null : value.getInstitutionIdentifier();
this.txnId = value == null ? null : value.getTransactionIdentifier();
this.cbCtrlSignature = value == null ? null : value.getControlSignature();
}
public int getStatus() { return status; }
public String getCtrlId() { return ctrlId; }
public BigDecimal getAmount() { return amount; }
public String getOrgId() { return orgId; }
public String getTxnId() { return txnId; }
public String getCbCtrlSignature() { return cbCtrlSignature; }
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤七控制系统签名SM3摘要结果")
public class ControlSystemDigestResult {
@Schema(description = "拼接请求原文的SM3摘要")
private final String digest;
public ControlSystemDigestResult(String digest) {
this.digest = digest;
}
public String getDigest() {
return digest;
}
}

@ -13,7 +13,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@ -29,37 +28,25 @@ public class ControlSystemSignatureService {
@Resource private InstitutionTrainingErrorRecorder errorRecorder; @Resource private InstitutionTrainingErrorRecorder errorRecorder;
@Transactional @Transactional
public ControlSystemDigestResult digest(String originalText, InstitutionKeySubject subject, String operator) { public ControlSystemSignatureResult prepare(InstitutionKeySubject subject, String operator) {
TransactionInformationIdentifierEntity transaction = requireQuotaVerified(subject); TransactionInformationIdentifierEntity transaction = requireQuotaVerified(subject);
String normalizedOriginalText = validateOriginalText(originalText, transaction, subject);
ControlSystemSignatureEntity current = latest(subject); ControlSystemSignatureEntity current = latest(subject);
if (current != null) { if (current != null) {
if (!current.getTransactionIdentifierId().equals(transaction.getId())) { if (current.getTransactionIdentifierId().equals(transaction.getId())) {
return new ControlSystemSignatureResult(current);
}
throw new BusinessException(ErrorCode.VALIDATION_ERROR, throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"当前实训已有步骤七控制系统签名记录,如需重新开始请先调用重新实训接口"); "当前实训已有步骤七控制系统签名记录,如需重新开始请先调用重新实训接口");
} }
String digest = cryptography.sm3(normalizedOriginalText);
update(current, current.getStatus(), "DIGESTED", operator,
new LambdaUpdateWrapper<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getOriginalText, normalizedOriginalText)
.set(ControlSystemSignatureEntity::getDigest, digest)
.set(ControlSystemSignatureEntity::getSigningKeyId, null)
.set(ControlSystemSignatureEntity::getSignature, null)
.set(ControlSystemSignatureEntity::getControlSignature, null)
.set(ControlSystemSignatureEntity::getResponseMessage, null));
return new ControlSystemDigestResult(digest);
}
ControlSystemSignatureEntity value = new ControlSystemSignatureEntity(); ControlSystemSignatureEntity value = new ControlSystemSignatureEntity();
value.setTransactionIdentifierId(transaction.getId()); value.setTransactionIdentifierId(transaction.getId());
value.setControlId("CTRL_" + LocalDateTime.now().format(DATE) + "_" value.setControlId("CTRL_" + LocalDateTime.now().format(DATE) + "_" + String.format("%03d", transaction.getId() % 1000));
+ String.format("%03d", transaction.getId() % 1000));
value.setAmount(transaction.getAmount()); value.setAmount(transaction.getAmount());
value.setInstitutionIdentifier(transaction.getInstitutionIdentifier()); value.setInstitutionIdentifier(transaction.getInstitutionIdentifier());
value.setTransactionIdentifier(transaction.getTransactionIdentifier()); value.setTransactionIdentifier(transaction.getTransactionIdentifier());
value.setOriginalText(normalizedOriginalText); value.setOriginalText(value.getControlId() + "|" + transaction.getAmount().setScale(2).toPlainString() + "|"
value.setDigest(cryptography.sm3(normalizedOriginalText)); + transaction.getInstitutionIdentifier() + "|" + transaction.getTransactionIdentifier());
value.setStatus("DIGESTED"); value.setStatus("PREPARED");
value.setUserId(subject.getUserId()); value.setUserId(subject.getUserId());
value.setSchoolId(subject.getSchoolId()); value.setSchoolId(subject.getSchoolId());
value.setClassId(subject.getClassId()); value.setClassId(subject.getClassId());
@ -69,38 +56,44 @@ public class ControlSystemSignatureService {
value.setUpdatedBy(operator); value.setUpdatedBy(operator);
value.setDeleted(false); value.setDeleted(false);
mapper.insert(value); mapper.insert(value);
return new ControlSystemDigestResult(value.getDigest()); return new ControlSystemSignatureResult(value);
}
@Transactional
public ControlSystemSignatureResult digest(InstitutionKeySubject subject, String operator) {
ControlSystemSignatureEntity value = requireStatus(subject, "PREPARED", "请先生成控制系统签名基础数据");
update(value, "PREPARED", "DIGESTED", operator,
new LambdaUpdateWrapper<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getDigest, cryptography.sm3(value.getOriginalText())));
return new ControlSystemSignatureResult(requireLatest(subject));
} }
@Transactional @Transactional
public ControlSystemSignatureResult sign(String privateKey, InstitutionKeySubject subject, String operator) { public ControlSystemSignatureResult sign(String privateKey, InstitutionKeySubject subject, String operator) {
ControlSystemSignatureEntity value = requireLatest(subject); ControlSystemSignatureEntity value = requireStatus(subject, "DIGESTED", "请先计算控制系统签名摘要");
requireResult(value.getDigest(), "请先计算控制系统签名摘要");
String signature = keyService.signCentralBank(subject, privateKey, value.getDigest()); String signature = keyService.signCentralBank(subject, privateKey, value.getDigest());
if (signature == null) { if (signature == null) {
int errorSequence = errorRecorder.recordScoreError(subject.getUserId()); int errorSequence = errorRecorder.recordScoreError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"中央银行第一私钥不正确,本次实训第" + errorSequence + "次错误"); "中央银行第一私钥不正确,本次实训第" + errorSequence + "次错误");
} }
update(value, value.getStatus(), "SIGNED", operator, update(value, "DIGESTED", "SIGNED", operator,
new LambdaUpdateWrapper<ControlSystemSignatureEntity>() new LambdaUpdateWrapper<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getSigningKeyId, InstitutionKeyService.CENTRAL_FIRST_KEY) .set(ControlSystemSignatureEntity::getSigningKeyId, InstitutionKeyService.CENTRAL_FIRST_KEY)
.set(ControlSystemSignatureEntity::getSignature, signature) .set(ControlSystemSignatureEntity::getSignature, signature)
.set(ControlSystemSignatureEntity::getControlSignature, signature) .set(ControlSystemSignatureEntity::getControlSignature, signature));
.set(ControlSystemSignatureEntity::getResponseMessage, null));
return new ControlSystemSignatureResult(requireLatest(subject)); return new ControlSystemSignatureResult(requireLatest(subject));
} }
@Transactional @Transactional
public ControlSystemSignatureResult packageResponse(InstitutionKeySubject subject, String operator) { public ControlSystemSignatureResult packageResponse(InstitutionKeySubject subject, String operator) {
ControlSystemSignatureEntity value = requireLatest(subject); ControlSystemSignatureEntity value = requireStatus(subject, "SIGNED", "请先完成中央银行控制系统签名");
requireResult(value.getControlSignature(), "请先完成中央银行控制系统签名");
String message = "{\"ctrlId\":\"" + value.getControlId() + "\",\"amount\":\"" String message = "{\"ctrlId\":\"" + value.getControlId() + "\",\"amount\":\""
+ value.getAmount().setScale(2).toPlainString() + "\",\"orgId\":\"" + value.getAmount().setScale(2).toPlainString() + "\",\"orgId\":\""
+ value.getInstitutionIdentifier() + "\",\"txnId\":\"" + value.getInstitutionIdentifier() + "\",\"txnId\":\""
+ value.getTransactionIdentifier() + "\",\"cbCtrlSignature\":\"" + value.getTransactionIdentifier() + "\",\"cbCtrlSignature\":\""
+ value.getControlSignature() + "\"}"; + value.getControlSignature() + "\"}";
update(value, value.getStatus(), "PACKAGED", operator, update(value, "SIGNED", "PACKAGED", operator,
new LambdaUpdateWrapper<ControlSystemSignatureEntity>() new LambdaUpdateWrapper<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getResponseMessage, message)); .set(ControlSystemSignatureEntity::getResponseMessage, message));
return new ControlSystemSignatureResult(requireLatest(subject)); return new ControlSystemSignatureResult(requireLatest(subject));
@ -108,47 +101,22 @@ public class ControlSystemSignatureService {
@Transactional @Transactional
public ControlSystemSignatureResult send(InstitutionKeySubject subject, String operator) { public ControlSystemSignatureResult send(InstitutionKeySubject subject, String operator) {
ControlSystemSignatureEntity value = requireLatest(subject); ControlSystemSignatureEntity value = requireStatus(subject, "PACKAGED", "请先打包控制系统签名反馈报文");
requireResult(value.getResponseMessage(), "请先打包控制系统签名反馈报文"); update(value, "PACKAGED", "SENT", operator, new LambdaUpdateWrapper<ControlSystemSignatureEntity>());
update(value, value.getStatus(), "SENT", operator, new LambdaUpdateWrapper<ControlSystemSignatureEntity>());
scoreService.markStepCompleted(subject.getUserId(), 7);
return new ControlSystemSignatureResult(requireLatest(subject)); return new ControlSystemSignatureResult(requireLatest(subject));
} }
@Transactional(readOnly = true) @Transactional
public CommercialBankControlSystemSignatureResult commercialBankMessage(InstitutionKeySubject subject) { public ControlSystemSignatureResult receive(InstitutionKeySubject subject, String operator) {
return new CommercialBankControlSystemSignatureResult(latest(subject)); ControlSystemSignatureEntity value = requireStatus(subject, "SENT", "中央银行尚未发送控制系统签名报文");
} update(value, "SENT", "RECEIVED", operator, new LambdaUpdateWrapper<ControlSystemSignatureEntity>());
scoreService.markStepCompleted(subject.getUserId(), 7);
private String validateOriginalText(String originalText, TransactionInformationIdentifierEntity transaction, return new ControlSystemSignatureResult(requireLatest(subject));
InstitutionKeySubject subject) {
String value = originalText == null ? null : originalText.trim();
if (value == null || value.isEmpty()) {
throw dataError(subject, "拼接请求原文不能为空");
}
String[] parts = value.split("\\|", -1);
if (parts.length != 3) {
throw dataError(subject, "拼接请求原文格式必须为:金额|机构标识|交易信息标识");
}
BigDecimal amount;
try {
amount = new BigDecimal(parts[0]);
} catch (NumberFormatException exception) {
throw dataError(subject, "拼接请求原文中的金额格式不正确");
}
if (amount.compareTo(transaction.getAmount()) != 0
|| !transaction.getInstitutionIdentifier().equals(parts[1])
|| !transaction.getTransactionIdentifier().equals(parts[2])) {
throw dataError(subject,
"拼接请求原文中的金额、机构标识或交易信息标识与当前用户前序数据不一致");
}
return value;
} }
private BusinessException dataError(InstitutionKeySubject subject, String message) { @Transactional(readOnly = true)
int errorSequence = errorRecorder.recordScoreError(subject.getUserId()); public ControlSystemSignatureResult detail(InstitutionKeySubject subject) {
return new BusinessException(ErrorCode.VALIDATION_ERROR, return new ControlSystemSignatureResult(requireLatest(subject));
message + ",本次实训第" + errorSequence + "次错误");
} }
private TransactionInformationIdentifierEntity requireQuotaVerified(InstitutionKeySubject subject) { private TransactionInformationIdentifierEntity requireQuotaVerified(InstitutionKeySubject subject) {
@ -164,12 +132,6 @@ public class ControlSystemSignatureService {
return value; return value;
} }
private void requireResult(String value, String message) {
if (value == null || value.trim().isEmpty()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, message);
}
}
private ControlSystemSignatureEntity requireStatus(InstitutionKeySubject subject, String status, String message) { private ControlSystemSignatureEntity requireStatus(InstitutionKeySubject subject, String status, String message) {
ControlSystemSignatureEntity value = requireLatest(subject); ControlSystemSignatureEntity value = requireLatest(subject);
if (!status.equals(value.getStatus())) { if (!status.equals(value.getStatus())) {

@ -1,32 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "商业银行接收的央行确认报文信息")
public class CurrencyConfirmationMessageResult {
@Schema(description = "报文确认状态", example = "已确认", allowableValues = {"待确认", "已确认"})
private final String confirmationStatus;
@Schema(description = "步骤二货币生成请求展示编号", example = "REQ_20260730_001")
private final String requestId;
@Schema(description = "确认报文业务状态;待确认时为空", example = "CONFIRMED")
private final String status;
@Schema(description = "央行确认签名")
private final String cbSignature;
@Schema(description = "央行确权时间,格式yyyyMMddHHmmss", example = "20260730153300")
private final String confirmTime;
public CurrencyConfirmationMessageResult(String confirmationStatus, String requestId, String status,
String cbSignature, String confirmTime) {
this.confirmationStatus = confirmationStatus;
this.requestId = requestId;
this.status = status;
this.cbSignature = cbSignature;
this.confirmTime = confirmTime;
}
public String getConfirmationStatus() { return confirmationStatus; }
public String getRequestId() { return requestId; }
public String getStatus() { return status; }
public String getCbSignature() { return cbSignature; }
public String getConfirmTime() { return confirmTime; }
}

@ -1,19 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
@ApiModel(description = "央行确权签名结果")
@Schema(description = "央行确权签名结果")
public class CurrencyConfirmationSignatureResult {
@Schema(description = "生成的央行确权签名", example = "CB_SIG_6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B2C3D")
private final String centralBankSignature;
public CurrencyConfirmationSignatureResult(String centralBankSignature) {
this.centralBankSignature = centralBankSignature;
}
public String getCentralBankSignature() {
return centralBankSignature;
}
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "用户输入请求原文生成的SM3摘要")
public class CurrencyDigestResult {
@Schema(description = "SM3摘要一", example = "A1B2C3D4E5F60718293A4B5C6D7E8F90123456789ABCDEF00112233445566778")
private final String digest;
public CurrencyDigestResult(String digest) {
this.digest = digest;
}
public String getDigest() {
return digest;
}
}

@ -32,7 +32,7 @@ public class CurrencyGenerationRequestService {
private InstitutionTrainingErrorRecorder errorRecorder; private InstitutionTrainingErrorRecorder errorRecorder;
@Transactional @Transactional
public CurrencyDigestResult generateDigest(BigDecimal amount, String deliveryNodeCode, public CurrencyRequestResult generateDigest(BigDecimal amount, String deliveryNodeCode,
String institutionIdentifier, String requestTimestamp, String institutionIdentifier, String requestTimestamp,
InstitutionKeySubject subject, String operator) { InstitutionKeySubject subject, String operator) {
IssuedInstitutionIdentifierResult identifier = identifierQueryService.requireLatestFeedbacked(subject); IssuedInstitutionIdentifierResult identifier = identifierQueryService.requireLatestFeedbacked(subject);
@ -69,7 +69,7 @@ public class CurrencyGenerationRequestService {
repository.update(request, expected, "DIGEST", expected == CurrencyGenerationRequest.Status.DIGESTED repository.update(request, expected, "DIGEST", expected == CurrencyGenerationRequest.Status.DIGESTED
? "修改当前货币生成请求并重新计算SM3摘要" ? "修改当前货币生成请求并重新计算SM3摘要"
: "初始化请求后使用SM3计算并保存请求摘要", operator); : "初始化请求后使用SM3计算并保存请求摘要", operator);
return new CurrencyDigestResult(digest); return new CurrencyRequestResult(request);
} }
@Transactional @Transactional
@ -117,18 +117,10 @@ public class CurrencyGenerationRequestService {
} }
private static String buildRequestMessage(CurrencyGenerationRequest request) { private static String buildRequestMessage(CurrencyGenerationRequest request) {
return "{\"requestId\":\"" + displayRequestId(request) return "{\"originalText\":\"" + request.getRequestOriginalText()
+ "\",\"orgId\":\"" + request.getInstitutionIdentifier()
+ "\",\"amount\":\"" + request.getAmount().setScale(2).toPlainString()
+ "\",\"systemId\":\"" + request.getDeliveryNodeCode()
+ "\",\"timestamp\":\"" + request.getRequestTimestamp()
+ "\",\"digest\":\"" + request.getRequestDigest() + "\",\"digest\":\"" + request.getRequestDigest()
+ "\",\"signature\":\"" + request.getBankSignature() + "\"}"; + "\",\"signature\":\"" + request.getBankSignature()
} + "\",\"keyId\":\"" + request.getSigningKeyId() + "\"}";
public static String displayRequestId(CurrencyGenerationRequest request) {
String date = request.getRequestTimestamp().substring(0, 8);
return "REQ_" + date + "_" + String.format("%03d", request.getId() % 1000);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)

@ -73,14 +73,14 @@ public class CurrencyGenerationVerificationService {
} }
@Transactional @Transactional
public CurrencyDigestResult calculateDigest(long id, String requestOriginalText, public CurrencyGenerationVerificationResult calculateDigest(long id, String requestOriginalText,
InstitutionKeySubject subject, String operator) { InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = require(id, subject); CurrencyGenerationVerification value = require(id, subject);
CurrencyGenerationVerification.Status expected = value.getStatus(); CurrencyGenerationVerification.Status expected = value.getStatus();
String digest = cryptography.sm3(requestOriginalText); String digest = cryptography.sm3(requestOriginalText);
invoke(() -> value.calculateDigest(digest)); invoke(() -> value.calculateDigest(digest));
repository.update(value, expected, "CALCULATE_DIGEST", "对用户输入的请求原文计算摘要一", operator); repository.update(value, expected, "CALCULATE_DIGEST", "对用户输入的请求原文计算摘要一", operator);
return new CurrencyDigestResult(digest); return new CurrencyGenerationVerificationResult(value);
} }
@Transactional @Transactional
@ -101,37 +101,31 @@ public class CurrencyGenerationVerificationService {
@Transactional @Transactional
public CurrencyQuotaVerificationResult verifyQuota(long id, InstitutionKeySubject subject, String operator) { public CurrencyQuotaVerificationResult verifyQuota(long id, InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = require(id, subject); CurrencyGenerationVerification value = require(id, subject);
if (value.getStatus().ordinal() >= CurrencyGenerationVerification.Status.QUOTA_VERIFIED.ordinal()) { if (value.getStatus() == CurrencyGenerationVerification.Status.QUOTA_VERIFIED) {
return new CurrencyQuotaVerificationResult(value); return new CurrencyQuotaVerificationResult(value);
} }
CurrencyGenerationVerification.Status expected = value.getStatus(); CurrencyGenerationVerification.Status expected = value.getStatus();
BigDecimal usedQuota = requestMapper.sumActiveAmount(); BigDecimal usedQuota = requestMapper.sumActiveAmountExcluding(value.getRequestId());
BigDecimal remainingQuota = TOTAL_QUOTA.subtract(usedQuota).max(BigDecimal.ZERO); BigDecimal remainingQuota = TOTAL_QUOTA.subtract(usedQuota).max(BigDecimal.ZERO);
invoke(() -> value.verifyQuota(TOTAL_QUOTA, usedQuota, remainingQuota)); invoke(() -> value.verifyQuota(TOTAL_QUOTA, usedQuota, remainingQuota));
repository.update(value, expected, "VERIFY_QUOTA", repository.update(value, expected, "VERIFY_QUOTA",
"验证全系统50000元实训额度,累计已用额度包含本次申请金额", operator); "验证全系统50000元实训额度,统计未逻辑删除申请累计金额", operator);
return new CurrencyQuotaVerificationResult(value); return new CurrencyQuotaVerificationResult(value);
} }
@Transactional @Transactional
public CurrencyConfirmationSignatureResult confirm(long id, String requestDigest, String privateKey, public CurrencyGenerationVerificationResult confirm(long id, String privateKey,
InstitutionKeySubject subject, String operator) { InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = require(id, subject); CurrencyGenerationVerification value = require(id, subject);
if (!value.getRequestDigest().equalsIgnoreCase(requestDigest)) {
throw scoreError(subject, "输入请求摘要与步骤二请求报文摘要不一致");
}
CurrencyGenerationVerification.Status expected = value.getStatus(); CurrencyGenerationVerification.Status expected = value.getStatus();
String signature = keyService.signCentralBank(subject, privateKey, requestDigest); String signature = keyService.signCentralBank(subject, privateKey, value.getRequestDigest());
if (signature == null) { if (signature == null) {
throw keyError(subject, "确认签名必须使用当前实训主体的中央银行第一私钥"); throw keyError(subject, "确认签名必须使用当前实训主体的中央银行第一私钥");
} }
if (value.getStatus().ordinal() >= CurrencyGenerationVerification.Status.CONFIRMED.ordinal()) {
return new CurrencyConfirmationSignatureResult(value.getCentralBankSignature());
}
String time = LocalDateTime.now().format(TIME); String time = LocalDateTime.now().format(TIME);
invoke(() -> value.confirm(InstitutionKeyService.CENTRAL_FIRST_KEY, signature, time)); invoke(() -> value.confirm(InstitutionKeyService.CENTRAL_FIRST_KEY, signature, time));
repository.update(value, expected, "CONFIRM", "校验用户输入请求摘要并使用央行第一私钥完成签名确权", operator); repository.update(value, expected, "CONFIRM", "使用央行第一私钥完成签名确权", operator);
return new CurrencyConfirmationSignatureResult(value.getCentralBankSignature()); return new CurrencyGenerationVerificationResult(value);
} }
@Transactional @Transactional
@ -139,10 +133,8 @@ public class CurrencyGenerationVerificationService {
String operator) { String operator) {
CurrencyGenerationVerification value = require(id, subject); CurrencyGenerationVerification value = require(id, subject);
CurrencyGenerationVerification.Status expected = value.getStatus(); CurrencyGenerationVerification.Status expected = value.getStatus();
CurrencyGenerationRequest request = requestService.requireReceived(value.getRequestId(), subject); String message = "{\"requestId\":\"REQ_" + value.getRequestId() + "\",\"status\":\"CONFIRMED\","
String message = "{\"requestId\":\"" + CurrencyGenerationRequestService.displayRequestId(request) + "\"cbSignature\":\"CB_SIG_" + value.getCentralBankSignature() + "\","
+ "\",\"status\":\"CONFIRMED\","
+ "\"cbSignature\":\"" + displayCentralBankSignature(value.getCentralBankSignature()) + "\","
+ "\"confirmTime\":\"" + value.getConfirmationTime() + "\"}"; + "\"confirmTime\":\"" + value.getConfirmationTime() + "\"}";
invoke(() -> value.packageResponse(message)); invoke(() -> value.packageResponse(message));
repository.update(value, expected, "PACKAGE_RESPONSE", "打包央行确认报文", operator); repository.update(value, expected, "PACKAGE_RESPONSE", "打包央行确认报文", operator);
@ -159,29 +151,6 @@ public class CurrencyGenerationVerificationService {
return new CurrencyGenerationVerificationResult(value); return new CurrencyGenerationVerificationResult(value);
} }
@Transactional(readOnly = true)
public CurrencyConfirmationMessageResult receiveConfirmation(InstitutionKeySubject subject) {
CurrencyGenerationVerification value = repository.findLatestBySubject(subject.getUserId(),
subject.getSchoolId(), subject.getClassId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"当前实训主体没有未逻辑删除的步骤三验证记录"));
CurrencyGenerationRequest request = requestService.requireReceived(value.getRequestId(), subject);
String requestId = CurrencyGenerationRequestService.displayRequestId(request);
boolean confirmed = value.getStatus() == CurrencyGenerationVerification.Status.RESPONSE_RETURNED
&& value.getConfirmationMessage() != null
&& value.getCentralBankSignature() != null
&& value.getConfirmationTime() != null;
if (!confirmed) {
return new CurrencyConfirmationMessageResult("待确认", requestId, null, null, null);
}
return new CurrencyConfirmationMessageResult("已确认", requestId, "CONFIRMED",
displayCentralBankSignature(value.getCentralBankSignature()), value.getConfirmationTime());
}
private static String displayCentralBankSignature(String signature) {
return signature.startsWith("CB_SIG_") ? signature : "CB_SIG_" + signature;
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public CurrencyGenerationVerificationResult current(InstitutionKeySubject subject) { public CurrencyGenerationVerificationResult current(InstitutionKeySubject subject) {
CurrencyGenerationVerification value = repository.findLatestBySubject(subject.getUserId(), CurrencyGenerationVerification value = repository.findLatestBySubject(subject.getUserId(),

@ -11,9 +11,9 @@ import java.math.BigDecimal;
public class CurrencyQuotaVerificationResult { public class CurrencyQuotaVerificationResult {
@Schema(description = "整个系统一次实训可申请的总额度", example = "50000.00") @Schema(description = "整个系统一次实训可申请的总额度", example = "50000.00")
private final BigDecimal totalQuota; private final BigDecimal totalQuota;
@Schema(description = "整个系统全部未逻辑删除申请累计占用额度,包含本次申请", example = "43589.85") @Schema(description = "整个系统其他未逻辑删除申请累计占用额度", example = "42000.00")
private final BigDecimal usedQuota; private final BigDecimal usedQuota;
@Schema(description = "本次申请计入后整个系统剩余额度,即总额度减已用额度", example = "6410.15") @Schema(description = "本次申请前整个系统剩余额度", example = "8000.00")
private final BigDecimal remainingQuota; private final BigDecimal remainingQuota;
@Schema(description = "当前步骤二请求的本次申请金额", example = "1589.85") @Schema(description = "当前步骤二请求的本次申请金额", example = "1589.85")
private final BigDecimal currentAmount; private final BigDecimal currentAmount;

@ -5,16 +5,45 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
@ApiModel(description = "机构标识申请SM3摘要结果") @ApiModel(description = "接口返回数据")
@Schema(description = "机构标识申请SM3摘要结果") @Schema(description = "接口返回数据")
public class InstitutionDigestResult { public class InstitutionDigestResult {
@Schema(description = "机构标识申请编号")
private final long applicationId;
@Schema(description = "当前业务状态")
private final String status;
@Schema(description = "密码算法名称")
private final String algorithm;
@Schema(description = "参与摘要计算的完整拼接原文")
private final String concatenatedText;
@Schema(description = "SM3摘要值") @Schema(description = "SM3摘要值")
private final String digest; private final String digest;
public InstitutionDigestResult(String digest) { public InstitutionDigestResult(long applicationId, String status, String algorithm, String concatenatedText,
String digest) {
this.applicationId = applicationId;
this.status = status;
this.algorithm = algorithm;
this.concatenatedText = concatenatedText;
this.digest = digest; this.digest = digest;
} }
public long getApplicationId() {
return applicationId;
}
public String getStatus() {
return status;
}
public String getAlgorithm() {
return algorithm;
}
public String getConcatenatedText() {
return concatenatedText;
}
public String getDigest() { public String getDigest() {
return digest; return digest;
} }

@ -81,7 +81,8 @@ public class InstitutionIdentifierStepService {
? "修改当前最新一轮申请原文并重新计算SM3摘要" ? "修改当前最新一轮申请原文并重新计算SM3摘要"
: "创建本轮记录并使用SM3计算业务摘要", : "创建本轮记录并使用SM3计算业务摘要",
operator); operator);
return new InstitutionDigestResult(digest); return new InstitutionDigestResult(application.getId(), application.getStatus().name(), "SM3",
normalizedOriginalText, digest);
} }
@Transactional @Transactional

@ -13,16 +13,13 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.security.SecureRandom; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@Service @Service
public class QuotaControlBitService { public class QuotaControlBitService {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final char[] RANDOM_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
private static final SecureRandom RANDOM = new SecureRandom();
@Resource private ControlSystemSignatureMapper controlSignatureMapper; @Resource private ControlSystemSignatureMapper controlSignatureMapper;
@Resource private QuotaControlBitMapper mapper; @Resource private QuotaControlBitMapper mapper;
@ -32,35 +29,21 @@ public class QuotaControlBitService {
@Resource private InstitutionTrainingScoreService scoreService; @Resource private InstitutionTrainingScoreService scoreService;
@Transactional @Transactional
public QuotaControlStaticSegmentResult staticSegment(InstitutionKeySubject subject, String operator) { public QuotaControlBitResult staticSegment(BigDecimal amount, String institutionIdentifier,
String transactionIdentifier, InstitutionKeySubject subject,
String operator) {
ControlSystemSignatureEntity source = requireStepSeven(subject); ControlSystemSignatureEntity source = requireStepSeven(subject);
validateSourceInput(source, amount, institutionIdentifier, transactionIdentifier, subject);
QuotaControlBitEntity current = latest(subject); QuotaControlBitEntity current = latest(subject);
if (current != null) { if (current != null) {
if (!current.getControlSignatureId().equals(source.getId())) { if (!current.getControlSignatureId().equals(source.getId())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"当前实训已有步骤八记录,如需重新开始请先调用重新实训接口"); "当前实训已有步骤八记录,如需重新开始请先调用重新实训接口");
} }
String originalText = source.getAmount().setScale(2).toPlainString() + "|" if (!"STATIC_READY".equals(current.getStatus())) {
+ source.getInstitutionIdentifier() + "|" + source.getTransactionIdentifier(); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "静态段已提交,不能重复修改后续流程数据");
String segment = "STATIC_" + cryptography.sm3(originalText); }
update(current, current.getStatus(), "STATIC_READY", operator, return new QuotaControlBitResult(current);
new LambdaUpdateWrapper<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getAmount, source.getAmount().setScale(2))
.set(QuotaControlBitEntity::getInstitutionIdentifier, source.getInstitutionIdentifier())
.set(QuotaControlBitEntity::getTransactionIdentifier, source.getTransactionIdentifier())
.set(QuotaControlBitEntity::getStaticOriginalText, originalText)
.set(QuotaControlBitEntity::getStaticSegment, segment)
.set(QuotaControlBitEntity::getRandomValue, null)
.set(QuotaControlBitEntity::getSegmentTimestamp, null)
.set(QuotaControlBitEntity::getDynamicSegment, null)
.set(QuotaControlBitEntity::getCentralBankSignatureSegment, null)
.set(QuotaControlBitEntity::getCompleteControlBit, null)
.set(QuotaControlBitEntity::getSigningKeyId, null)
.set(QuotaControlBitEntity::getHashAlgorithm, null)
.set(QuotaControlBitEntity::getBankSignature, null)
.set(QuotaControlBitEntity::getRequestMessage, null)
.set(QuotaControlBitEntity::getReceiveStatus, 1));
return new QuotaControlStaticSegmentResult(segment);
} }
QuotaControlBitEntity value = new QuotaControlBitEntity(); QuotaControlBitEntity value = new QuotaControlBitEntity();
value.setControlSignatureId(source.getId()); value.setControlSignatureId(source.getId());
@ -77,146 +60,92 @@ public class QuotaControlBitService {
value.setCreatedAt(LocalDateTime.now()); value.setUpdatedAt(value.getCreatedAt()); value.setCreatedAt(LocalDateTime.now()); value.setUpdatedAt(value.getCreatedAt());
value.setCreatedBy(operator); value.setUpdatedBy(operator); value.setDeleted(false); value.setCreatedBy(operator); value.setUpdatedBy(operator); value.setDeleted(false);
mapper.insert(value); mapper.insert(value);
return new QuotaControlStaticSegmentResult(value.getStaticSegment()); return new QuotaControlBitResult(value);
} }
@Transactional @Transactional
public QuotaControlDynamicSegmentResult dynamicSegment(InstitutionKeySubject subject, String operator) { public QuotaControlBitResult dynamicSegment(String randomValue, String timestamp,
QuotaControlBitEntity value = requireLatest(subject); InstitutionKeySubject subject, String operator) {
if (value.getStaticSegment() == null) { QuotaControlBitEntity value = requireStatus(subject, "STATIC_READY", "请先生成静态段");
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成静态段"); update(value, "STATIC_READY", "DYNAMIC_READY", operator,
}
String randomValue = randomValue();
String timestamp = LocalDateTime.now().format(TIMESTAMP);
String segment = "DYN_" + randomValue + "_" + timestamp;
update(value, value.getStatus(), "DYNAMIC_READY", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getRandomValue, randomValue) .set(QuotaControlBitEntity::getRandomValue, randomValue.toUpperCase())
.set(QuotaControlBitEntity::getSegmentTimestamp, timestamp) .set(QuotaControlBitEntity::getSegmentTimestamp, timestamp)
.set(QuotaControlBitEntity::getDynamicSegment, segment) .set(QuotaControlBitEntity::getDynamicSegment,
.set(QuotaControlBitEntity::getCentralBankSignatureSegment, null) "DYN_" + randomValue.toUpperCase() + "_" + timestamp));
.set(QuotaControlBitEntity::getCompleteControlBit, null) return new QuotaControlBitResult(requireLatest(subject));
.set(QuotaControlBitEntity::getSigningKeyId, null)
.set(QuotaControlBitEntity::getHashAlgorithm, null)
.set(QuotaControlBitEntity::getBankSignature, null)
.set(QuotaControlBitEntity::getRequestMessage, null)
.set(QuotaControlBitEntity::getReceiveStatus, 1));
return new QuotaControlDynamicSegmentResult(segment);
} }
@Transactional @Transactional
public CentralBankSignatureSegmentResult readCentralBankSignature(InstitutionKeySubject subject, String operator) { public QuotaControlBitResult readCentralBankSignature(InstitutionKeySubject subject, String operator) {
QuotaControlBitEntity value = requireLatest(subject); QuotaControlBitEntity value = requireStatus(subject, "DYNAMIC_READY", "请先生成动态段");
requireResult(value.getDynamicSegment(), "请先生成动态段");
ControlSystemSignatureEntity source = requireStepSeven(subject); ControlSystemSignatureEntity source = requireStepSeven(subject);
if (!source.getId().equals(value.getControlSignatureId()) || source.getControlSignature() == null) { if (!source.getId().equals(value.getControlSignatureId()) || source.getControlSignature() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤七央行控制系统签名数据不完整"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤七央行控制系统签名数据不完整");
} }
update(value, value.getStatus(), "CB_SIGNATURE_READY", operator, update(value, "DYNAMIC_READY", "CB_SIGNATURE_READY", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>().set(
.set(QuotaControlBitEntity::getCentralBankSignatureSegment, QuotaControlBitEntity::getCentralBankSignatureSegment,
"CB_CTRL_" + source.getControlSignature()) "CB_CTRL_" + source.getControlSignature()));
.set(QuotaControlBitEntity::getCompleteControlBit, null) return new QuotaControlBitResult(requireLatest(subject));
.set(QuotaControlBitEntity::getSigningKeyId, null)
.set(QuotaControlBitEntity::getHashAlgorithm, null)
.set(QuotaControlBitEntity::getBankSignature, null)
.set(QuotaControlBitEntity::getRequestMessage, null)
.set(QuotaControlBitEntity::getReceiveStatus, 1));
return new CentralBankSignatureSegmentResult(
requireLatest(subject).getCentralBankSignatureSegment());
} }
@Transactional @Transactional
public QuotaControlBitResult assemble(InstitutionKeySubject subject, String operator) { public QuotaControlBitResult assemble(InstitutionKeySubject subject, String operator) {
QuotaControlBitEntity value = requireLatest(subject); QuotaControlBitEntity value = requireStatus(subject, "CB_SIGNATURE_READY", "请先读取央行签名段");
requireResult(value.getStaticSegment(), "请先生成静态段");
requireResult(value.getDynamicSegment(), "请先生成动态段");
requireResult(value.getCentralBankSignatureSegment(), "请先读取央行签名段");
String complete = value.getStaticSegment() + "|" + value.getDynamicSegment() + "|" String complete = value.getStaticSegment() + "|" + value.getDynamicSegment() + "|"
+ value.getCentralBankSignatureSegment(); + value.getCentralBankSignatureSegment();
update(value, value.getStatus(), "ASSEMBLED", operator, update(value, "CB_SIGNATURE_READY", "ASSEMBLED", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>().set(
.set(QuotaControlBitEntity::getCompleteControlBit, complete) QuotaControlBitEntity::getCompleteControlBit, complete));
.set(QuotaControlBitEntity::getSigningKeyId, null)
.set(QuotaControlBitEntity::getHashAlgorithm, null)
.set(QuotaControlBitEntity::getBankSignature, null)
.set(QuotaControlBitEntity::getRequestMessage, null)
.set(QuotaControlBitEntity::getReceiveStatus, 1));
return new QuotaControlBitResult(requireLatest(subject)); return new QuotaControlBitResult(requireLatest(subject));
} }
@Transactional @Transactional
public QuotaControlBitResult sign(String privateKey, String completeControlBit, String algorithm, public QuotaControlBitResult sign(String privateKey, String algorithm, InstitutionKeySubject subject,
InstitutionKeySubject subject, String operator) { String operator) {
QuotaControlBitEntity value = requireLatest(subject); QuotaControlBitEntity value = requireStatus(subject, "ASSEMBLED", "请先拼接完整额度控制位");
requireResult(value.getCompleteControlBit(), "请先拼接完整额度控制位");
String submittedControlBit = completeControlBit == null ? null : completeControlBit.trim();
if (!value.getCompleteControlBit().equals(submittedControlBit)) {
throw keyError(subject, "完整额度控制位与步骤八拼接结果不一致");
}
if (!"SM2".equalsIgnoreCase(algorithm == null ? "" : algorithm.trim())) { if (!"SM2".equalsIgnoreCase(algorithm == null ? "" : algorithm.trim())) {
throw keyError(subject, "签名算法必须选择SM2"); throw keyError(subject, "签名算法必须选择SM2");
} }
String signature = keyService.signCommercialBank(subject, privateKey, submittedControlBit); String signature = keyService.signCommercialBank(subject, privateKey, value.getCompleteControlBit());
if (signature == null) { if (signature == null) {
throw keyError(subject, "签名必须使用当前实训主体的商业银行第二私钥"); throw keyError(subject, "签名必须使用当前实训主体的商业银行第二私钥");
} }
update(value, value.getStatus(), "SIGNED", operator, update(value, "ASSEMBLED", "SIGNED", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getSigningKeyId, InstitutionKeyService.BANK_SECOND_KEY) .set(QuotaControlBitEntity::getSigningKeyId, InstitutionKeyService.BANK_SECOND_KEY)
.set(QuotaControlBitEntity::getHashAlgorithm, "SM2") .set(QuotaControlBitEntity::getHashAlgorithm, "SM2")
.set(QuotaControlBitEntity::getBankSignature, signature) .set(QuotaControlBitEntity::getBankSignature, signature));
.set(QuotaControlBitEntity::getRequestMessage, null)
.set(QuotaControlBitEntity::getReceiveStatus, 1));
return new QuotaControlBitResult(requireLatest(subject)); return new QuotaControlBitResult(requireLatest(subject));
} }
@Transactional @Transactional
public QuotaControlBitResult packageRequest(InstitutionKeySubject subject, String operator) { public QuotaControlBitResult packageRequest(InstitutionKeySubject subject, String operator) {
QuotaControlBitEntity value = requireLatest(subject); QuotaControlBitEntity value = requireStatus(subject, "SIGNED", "请先完成商业银行签名确认");
requireResult(value.getBankSignature(), "请先完成商业银行签名确认");
String message = "{\"quotaId\":\"" + value.getQuotaId() + "\",\"staticSegment\":\"" String message = "{\"quotaId\":\"" + value.getQuotaId() + "\",\"staticSegment\":\""
+ value.getStaticSegment() + "\",\"dynamicSegment\":\"" + value.getDynamicSegment() + value.getStaticSegment() + "\",\"dynamicSegment\":\"" + value.getDynamicSegment()
+ "\",\"cbSignatureSegment\":\"" + value.getCentralBankSignatureSegment() + "\",\"cbSignatureSegment\":\"" + value.getCentralBankSignatureSegment()
+ "\",\"bankSignature\":\"" + value.getBankSignature() + "\"}"; + "\",\"bankSignature\":\"" + value.getBankSignature() + "\"}";
update(value, value.getStatus(), "PACKAGED", operator, update(value, "SIGNED", "PACKAGED", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>().set(QuotaControlBitEntity::getRequestMessage, message));
.set(QuotaControlBitEntity::getRequestMessage, message)
.set(QuotaControlBitEntity::getReceiveStatus, 1));
return new QuotaControlBitResult(requireLatest(subject)); return new QuotaControlBitResult(requireLatest(subject));
} }
@Transactional @Transactional
public QuotaControlBitResult send(InstitutionKeySubject subject, String operator) { public QuotaControlBitResult send(InstitutionKeySubject subject, String operator) {
QuotaControlBitEntity value = requireLatest(subject); QuotaControlBitEntity value = requireStatus(subject, "PACKAGED", "请先打包额度控制位数据包报文");
requireResult(value.getRequestMessage(), "请先打包额度控制位数据包报文"); update(value, "PACKAGED", "SENT", operator, new LambdaUpdateWrapper<QuotaControlBitEntity>());
update(value, value.getStatus(), "RECEIVED", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>().set(QuotaControlBitEntity::getReceiveStatus, 2));
scoreService.markStepCompleted(subject.getUserId(), 8);
return new QuotaControlBitResult(requireLatest(subject)); return new QuotaControlBitResult(requireLatest(subject));
} }
@Transactional(readOnly = true) @Transactional
public CentralBankQuotaControlBitResult centralBankMessage(InstitutionKeySubject subject) { public QuotaControlBitResult receive(InstitutionKeySubject subject, String operator) {
return new CentralBankQuotaControlBitResult(latest(subject)); QuotaControlBitEntity value = requireStatus(subject, "SENT", "商业银行尚未发送额度控制位数据包报文");
} update(value, "SENT", "RECEIVED", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>().set(QuotaControlBitEntity::getReceiveStatus, 2));
@Transactional(readOnly = true) scoreService.markStepCompleted(subject.getUserId(), 8);
public QuotaControlStaticSegmentResult queryStaticSegment(InstitutionKeySubject subject) { return new QuotaControlBitResult(requireLatest(subject));
String staticSegment = requireLatest(subject).getStaticSegment();
if (staticSegment == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前尚未生成步骤八静态段");
}
return new QuotaControlStaticSegmentResult(staticSegment);
}
@Transactional(readOnly = true)
public QuotaControlDynamicSegmentResult queryDynamicSegment(InstitutionKeySubject subject) {
String dynamicSegment = requireLatest(subject).getDynamicSegment();
if (dynamicSegment == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前尚未生成步骤八动态段");
}
return new QuotaControlDynamicSegmentResult(dynamicSegment);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -224,17 +153,17 @@ public class QuotaControlBitService {
return new QuotaControlBitResult(requireLatest(subject)); return new QuotaControlBitResult(requireLatest(subject));
} }
private String randomValue() { private void validateSourceInput(ControlSystemSignatureEntity source, BigDecimal amount,
StringBuilder value = new StringBuilder(8); String institutionIdentifier, String transactionIdentifier,
for (int index = 0; index < 8; index++) { InstitutionKeySubject subject) {
value.append(RANDOM_CHARS[RANDOM.nextInt(RANDOM_CHARS.length)]); if (amount == null || source.getAmount().compareTo(amount) != 0) {
throw keyError(subject, "用户输入的货币金额与前序步骤数据不一致");
} }
return value.toString(); if (!source.getInstitutionIdentifier().equals(institutionIdentifier)) {
throw keyError(subject, "用户输入的机构标识与前序步骤数据不一致");
} }
if (!source.getTransactionIdentifier().equals(transactionIdentifier)) {
private void requireResult(String value, String message) { throw keyError(subject, "用户输入的交易信息标识与前序步骤数据不一致");
if (value == null || value.trim().isEmpty()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, message);
} }
} }
@ -244,11 +173,11 @@ public class QuotaControlBitService {
.eq(ControlSystemSignatureEntity::getUserId, subject.getUserId()) .eq(ControlSystemSignatureEntity::getUserId, subject.getUserId())
.eq(ControlSystemSignatureEntity::getSchoolId, subject.getSchoolId()) .eq(ControlSystemSignatureEntity::getSchoolId, subject.getSchoolId())
.eq(ControlSystemSignatureEntity::getClassId, subject.getClassId()) .eq(ControlSystemSignatureEntity::getClassId, subject.getClassId())
.eq(ControlSystemSignatureEntity::getStatus, "SENT") .eq(ControlSystemSignatureEntity::getStatus, "RECEIVED")
.eq(ControlSystemSignatureEntity::getDeleted, false) .eq(ControlSystemSignatureEntity::getDeleted, false)
.orderByDesc(ControlSystemSignatureEntity::getCreatedAt).last("LIMIT 1")); .orderByDesc(ControlSystemSignatureEntity::getCreatedAt).last("LIMIT 1"));
if (value == null) throw new BusinessException(ErrorCode.VALIDATION_ERROR, if (value == null) throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"步骤七尚未发送控制系统签名反馈,不能进入步骤八"); "步骤七尚未完成商业银行接收,不能进入步骤八");
return value; return value;
} }

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤八动态段生成结果")
public class QuotaControlDynamicSegmentResult {
@Schema(description = "由后端随机数和当前时间戳生成的动态段", example = "DYN_7G8H9I0J_20260730153600")
private final String dynamicSegment;
public QuotaControlDynamicSegmentResult(String dynamicSegment) {
this.dynamicSegment = dynamicSegment;
}
public String getDynamicSegment() {
return dynamicSegment;
}
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤八静态段生成结果")
public class QuotaControlStaticSegmentResult {
@Schema(description = "由后端前序金额、机构标识和交易信息标识计算的SM3静态段")
private final String staticSegment;
public QuotaControlStaticSegmentResult(String staticSegment) {
this.staticSegment = staticSegment;
}
public String getStaticSegment() {
return staticSegment;
}
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤五额度请求摘要结果")
public class QuotaRequestDigestResult {
@Schema(description = "额度请求原文的SM3摘要")
private final String digest;
public QuotaRequestDigestResult(String digest) {
this.digest = digest;
}
public String getDigest() {
return digest;
}
}

@ -1,38 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
@Schema(description = "商业银行额度请求报文")
public class QuotaRequestMessageResult {
@Schema(description = "额度请求编号", example = "QR_20260730_001")
private final String quotaRequestId;
@Schema(description = "申请金额", example = "1589.85")
private final BigDecimal amount;
@Schema(description = "机构标识", example = "ORG_3A4B5C6D7E8F")
private final String orgId;
@Schema(description = "交易信息标识")
private final String txnId;
@Schema(description = "额度请求SM3摘要")
private final String digest;
@Schema(description = "商业银行第二私钥生成的SM2签名")
private final String signature;
public QuotaRequestMessageResult(TransactionInformationIdentifier value) {
this.quotaRequestId = value.getQuotaRequestId();
this.amount = value.getAmount();
this.orgId = value.getInstitutionIdentifier();
this.txnId = value.getTransactionIdentifier();
this.digest = value.getQuotaDigest();
this.signature = value.getQuotaSignature();
}
public String getQuotaRequestId() { return quotaRequestId; }
public BigDecimal getAmount() { return amount; }
public String getOrgId() { return orgId; }
public String getTxnId() { return txnId; }
public String getDigest() { return digest; }
public String getSignature() { return signature; }
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤五额度请求签名结果")
public class QuotaRequestSignatureResult {
@Schema(description = "商业银行第二私钥生成的SM2签名值")
private final String signature;
public QuotaRequestSignatureResult(String signature) {
this.signature = signature;
}
public String getSignature() {
return signature;
}
}

@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal; import java.math.BigDecimal;
@Schema(description = "步骤五交易信息标识存在性判断结果") @Schema(description = "步骤五从前序步骤读取的额度请求基础数据")
public class QuotaRequestSourceDataResult { public class QuotaRequestSourceDataResult {
@Schema(description = "步骤二生成并经步骤三确认的货币金额", example = "1589.85") @Schema(description = "步骤二生成并经步骤三确认的货币金额", example = "1589.85")
private final BigDecimal amount; private final BigDecimal amount;
@ -13,6 +13,7 @@ public class QuotaRequestSourceDataResult {
private final String institutionIdentifier; private final String institutionIdentifier;
@Schema(description = "步骤四生成的交易信息标识", example = "TXN_8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7") @Schema(description = "步骤四生成的交易信息标识", example = "TXN_8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7")
private final String transactionIdentifier; private final String transactionIdentifier;
public QuotaRequestSourceDataResult(TransactionInformationIdentifier value) { public QuotaRequestSourceDataResult(TransactionInformationIdentifier value) {
this.amount = value.getAmount(); this.amount = value.getAmount();
this.institutionIdentifier = value.getInstitutionIdentifier(); this.institutionIdentifier = value.getInstitutionIdentifier();

@ -1,24 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤六H1与H2比对结果")
public class QuotaVerificationComparisonResult {
@Schema(description = "H1与H2是否一致")
private final boolean h1EqualsH2;
@Schema(description = "验签结论")
private final String conclusion;
public QuotaVerificationComparisonResult(boolean h1EqualsH2) {
this.h1EqualsH2 = h1EqualsH2;
this.conclusion = h1EqualsH2 ? "一致,验签通过" : "不一致,验签失败";
}
public boolean isH1EqualsH2() {
return h1EqualsH2;
}
public String getConclusion() {
return conclusion;
}
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤六SM3哈希运算结果")
public class QuotaVerificationHashResult {
@Schema(description = "对额度请求拼接原文进行SM3运算得到的H1")
private final String h1;
public QuotaVerificationHashResult(String h1) {
this.h1 = h1;
}
public String getH1() {
return h1;
}
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤六额度请求拼接原文")
public class QuotaVerificationOriginalTextResult {
@Schema(description = "待哈希原文,格式:金额|机构标识|交易信息标识")
private final String originalText;
public QuotaVerificationOriginalTextResult(String originalText) {
this.originalText = originalText;
}
public String getOriginalText() {
return originalText;
}
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "步骤六第二公钥验签获取摘要结果")
public class QuotaVerificationSignatureDigestResult {
@Schema(description = "商业银行第二公钥验签成功后由签名确认的摘要H2")
private final String h2;
public QuotaVerificationSignatureDigestResult(String h2) {
this.h2 = h2;
}
public String getH2() {
return h2;
}
}

@ -1,17 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "签名结果")
public class SignatureResult {
@Schema(description = "生成的签名值")
private final String signature;
public SignatureResult(String signature) {
this.signature = signature;
}
public String getSignature() {
return signature;
}
}

@ -49,35 +49,14 @@ public class StandardCurrencyService {
throw inputError(subject, "用户输入的货币总额与步骤八额度控制位金额不一致"); throw inputError(subject, "用户输入的货币总额与步骤八额度控制位金额不一致");
} }
StandardCurrencyBatchEntity current = latest(subject); StandardCurrencyBatchEntity current = latest(subject);
List<DenominationBreakdownResult> breakdown = decomposeAmount(source.getAmount());
if (current != null) { if (current != null) {
if (!current.getQuotaControlBitId().equals(source.getId())) { if (!current.getQuotaControlBitId().equals(source.getId())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"当前实训已有步骤九记录,如需重新开始请先调用重新实训接口"); "当前实训已有步骤九记录,如需重新开始请先调用重新实训接口");
} }
currencyMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyEntity>() return result(current);
.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));
} }
List<DenominationBreakdownResult> breakdown = decomposeAmount(source.getAmount());
StandardCurrencyBatchEntity batch = new StandardCurrencyBatchEntity(); StandardCurrencyBatchEntity batch = new StandardCurrencyBatchEntity();
batch.setQuotaControlBitId(source.getId()); batch.setQuotaControlBitId(source.getId());
batch.setBatchNumber(LocalDateTime.now().format(DATE) + "_" batch.setBatchNumber(LocalDateTime.now().format(DATE) + "_"
@ -101,10 +80,7 @@ public class StandardCurrencyService {
@Transactional @Transactional
public StandardCurrencyBatchResult confirmRule(String prefix, String idRule, String currencyStatus, public StandardCurrencyBatchResult confirmRule(String prefix, String idRule, String currencyStatus,
InstitutionKeySubject subject, String operator) { InstitutionKeySubject subject, String operator) {
StandardCurrencyBatchEntity batch = requireLatest(subject); StandardCurrencyBatchEntity batch = requireStatus(subject, "DECOMPOSED", "请先执行面额拆解");
if (batch.getDenominationSummary() == null || batch.getTotalAmount() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先执行面额拆解");
}
if (!"DC".equals(prefix == null ? null : prefix.trim().toUpperCase())) { if (!"DC".equals(prefix == null ? null : prefix.trim().toUpperCase())) {
throw inputError(subject, "币串前缀必须为DC"); throw inputError(subject, "币串前缀必须为DC");
} }
@ -116,7 +92,7 @@ public class StandardCurrencyService {
} }
int updated = batchMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyBatchEntity>() int updated = batchMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyBatchEntity>()
.eq(StandardCurrencyBatchEntity::getId, batch.getId()) .eq(StandardCurrencyBatchEntity::getId, batch.getId())
.eq(StandardCurrencyBatchEntity::getStatus, batch.getStatus()) .eq(StandardCurrencyBatchEntity::getStatus, "DECOMPOSED")
.eq(StandardCurrencyBatchEntity::getDeleted, false) .eq(StandardCurrencyBatchEntity::getDeleted, false)
.set(StandardCurrencyBatchEntity::getPrefix, "DC") .set(StandardCurrencyBatchEntity::getPrefix, "DC")
.set(StandardCurrencyBatchEntity::getIdRule, ID_RULE) .set(StandardCurrencyBatchEntity::getIdRule, ID_RULE)
@ -130,21 +106,13 @@ public class StandardCurrencyService {
@Transactional @Transactional
public StandardCurrencyBatchResult generate(InstitutionKeySubject subject, String operator) { public StandardCurrencyBatchResult generate(InstitutionKeySubject subject, String operator) {
StandardCurrencyBatchEntity batch = requireLatest(subject); StandardCurrencyBatchEntity batch = requireStatus(subject, "RULE_CONFIRMED", "请先确认币串生成规则");
if (batch.getPrefix() == null || batch.getIdRule() == null || batch.getCurrencyStatus() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先确认币串生成规则");
}
QuotaControlBitEntity source = requireStepEight(subject); QuotaControlBitEntity source = requireStepEight(subject);
if (!source.getId().equals(batch.getQuotaControlBitId()) || source.getBankSignature() == null if (!source.getId().equals(batch.getQuotaControlBitId()) || source.getBankSignature() == null
|| source.getCentralBankSignatureSegment() == null) { || source.getCentralBankSignatureSegment() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤八额度控制位签名数据不完整"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤八额度控制位签名数据不完整");
} }
List<BigDecimal> values = expand(decode(batch.getDenominationSummary())); 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(); SecureRandom random = new SecureRandom();
for (int i = 0; i < values.size(); i++) { for (int i = 0; i < values.size(); i++) {
int sequence = i + 1; int sequence = i + 1;
@ -152,27 +120,6 @@ public class StandardCurrencyService {
String checkCode = checkCode(random); String checkCode = checkCode(random);
String currencyId = "DC_" + batch.getBatchNumber() + "_" + sequenceText + "_" + checkCode; String currencyId = "DC_" + batch.getBatchNumber() + "_" + sequenceText + "_" + checkCode;
String denomination = values.get(i).setScale(2).toPlainString(); 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(); StandardCurrencyEntity currency = new StandardCurrencyEntity();
currency.setBatchId(batch.getId()); currency.setBatchId(batch.getId());
currency.setSequenceNumber(sequence); currency.setSequenceNumber(sequence);
@ -192,10 +139,9 @@ public class StandardCurrencyService {
currency.setDeleted(false); currency.setDeleted(false);
currencyMapper.insert(currency); currencyMapper.insert(currency);
} }
}
int updated = batchMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyBatchEntity>() int updated = batchMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyBatchEntity>()
.eq(StandardCurrencyBatchEntity::getId, batch.getId()) .eq(StandardCurrencyBatchEntity::getId, batch.getId())
.eq(StandardCurrencyBatchEntity::getStatus, batch.getStatus()) .eq(StandardCurrencyBatchEntity::getStatus, "RULE_CONFIRMED")
.eq(StandardCurrencyBatchEntity::getDeleted, false) .eq(StandardCurrencyBatchEntity::getDeleted, false)
.set(StandardCurrencyBatchEntity::getStatus, "GENERATED") .set(StandardCurrencyBatchEntity::getStatus, "GENERATED")
.set(StandardCurrencyBatchEntity::getUpdatedAt, LocalDateTime.now()) .set(StandardCurrencyBatchEntity::getUpdatedAt, LocalDateTime.now())

@ -1,18 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
@ApiModel(description = "交易信息摘要结果")
@Schema(description = "交易信息摘要结果")
public class TransactionDigestResult {
@Schema(description = "交易原文的SM3摘要", example = "50EC4F489F198B527B22054625B0B0627E9162D3A83567C34E94AABB3738651B")
private final String digest;
public TransactionDigestResult(TransactionInformationIdentifier value) {
this.digest = value.getDigest();
}
public String getDigest() { return digest; }
}

@ -1,18 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
@ApiModel(description = "交易信息标识组装结果")
@Schema(description = "交易信息标识组装结果")
public class TransactionIdentifierAssemblyResult {
@Schema(description = "最终交易信息标识", example = "TXN_50EC4F489F198B527B22054625B0B0627E9162D3A83567C34E94AABB3738651B")
private final String transactionIdentifier;
public TransactionIdentifierAssemblyResult(TransactionInformationIdentifier value) {
this.transactionIdentifier = value.getTransactionIdentifier();
}
public String getTransactionIdentifier() { return transactionIdentifier; }
}

@ -68,8 +68,7 @@ public class TransactionInformationIdentifierResult {
this.quotaSignature = value.getQuotaSignature(); this.quotaSignature = value.getQuotaSignature();
this.quotaSigningKeyId = value.getQuotaSigningKeyId(); this.quotaSigningKeyId = value.getQuotaSigningKeyId();
this.quotaRequestMessage = value.getQuotaRequestMessage(); this.quotaRequestMessage = value.getQuotaRequestMessage();
this.centralBankReceiveStatus = value.getStatus().ordinal() this.centralBankReceiveStatus = value.getStatus() == TransactionInformationIdentifier.Status.QUOTA_RECEIVED
>= TransactionInformationIdentifier.Status.QUOTA_RECEIVED.ordinal()
? "RECEIVED" : "NOT_RECEIVED"; ? "RECEIVED" : "NOT_RECEIVED";
this.status = value.getStatus().name(); this.status = value.getStatus().name();
} }

@ -74,115 +74,70 @@ public class TransactionInformationIdentifierService {
} }
@Transactional @Transactional
public TransactionDigestResult digest(String originalText, InstitutionKeySubject subject, public TransactionInformationIdentifierResult digest(InstitutionKeySubject subject, String operator) {
String operator) { TransactionInformationIdentifier value = requireStatus(subject,
CurrencyGenerationVerificationEntity verification = requireCompletedVerification(subject); TransactionInformationIdentifier.Status.PREPARED, "请先获取前序数据并准备交易信息标识");
CurrencyGenerationRequestEntity request = requireCompletedRequest(verification.getRequestId(), subject); TransactionInformationIdentifier.Status expected = value.getStatus();
InstitutionIdentifierApplicationEntity application = requireCompletedIdentifier( String digest = cryptography.sm3(value.getOriginalText());
request.getIdentifierApplicationId(), subject); invoke(() -> value.recordDigest(digest));
TransactionInformationIdentifier existing = repository.findLatestByVerificationAndSubject( repository.update(value, expected, "DIGEST", "使用SM3计算交易信息标识摘要", operator);
verification.getId(), subject.getUserId(), subject.getSchoolId(), subject.getClassId()).orElse(null); return new TransactionInformationIdentifierResult(value);
try {
String normalizedOriginalText = originalText == null ? null : originalText.trim();
String timestamp = extractTimestamp(normalizedOriginalText, request.getAmount(),
application.getInstitutionIdentifier());
if (existing == null) {
TransactionInformationIdentifier created = TransactionInformationIdentifier.prepare(
verification.getId(), request.getId(), application.getId(), request.getAmount(),
application.getInstitutionIdentifier(), timestamp);
created.recordDigest(normalizedOriginalText, cryptography.sm3(normalizedOriginalText));
long id = repository.save(created, subject.getUserId(), subject.getSchoolId(),
subject.getClassId(), operator);
return new TransactionDigestResult(TransactionInformationIdentifier.restore(id,
created.getVerificationId(), created.getRequestId(), created.getIdentifierApplicationId(),
created.getAmount(), created.getInstitutionIdentifier(), created.getTransactionTimestamp(),
created.getOriginalText(), created.getDigest(), null, null, null, null, null, null,
null, null, null, null, null, null, TransactionInformationIdentifier.Status.DIGESTED));
}
TransactionInformationIdentifier.Status expected = existing.getStatus();
existing.recordDigest(normalizedOriginalText, cryptography.sm3(normalizedOriginalText));
repository.update(existing, expected, "DIGEST", "重新提交原文并更新SM3摘要", operator);
return new TransactionDigestResult(existing);
} catch (IllegalArgumentException | IllegalStateException exception) {
errorRecorder.recordScoreError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数据拼接错误");
}
}
private String extractTimestamp(String originalText, java.math.BigDecimal amount,
String institutionIdentifier) {
if (originalText == null || originalText.isEmpty()) {
throw new IllegalArgumentException("拼接请求原文不能为空");
}
String[] parts = originalText.split("\\|", -1);
if (parts.length != 3 || !parts[2].matches("\\d{14}")) {
throw new IllegalArgumentException("拼接请求原文格式错误");
}
java.math.BigDecimal inputAmount;
try {
inputAmount = new java.math.BigDecimal(parts[0]);
} catch (NumberFormatException exception) {
throw new IllegalArgumentException("拼接金额格式错误");
}
if (inputAmount.compareTo(amount) != 0 || !institutionIdentifier.equals(parts[1])) {
throw new IllegalArgumentException("拼接数据与前序数据不一致");
}
return parts[2];
} }
@Transactional @Transactional
public TransactionIdentifierAssemblyResult assemble(InstitutionKeySubject subject, String operator) { public TransactionInformationIdentifierResult assemble(InstitutionKeySubject subject, String operator) {
TransactionInformationIdentifier value = requireStatus(subject, TransactionInformationIdentifier value = requireStatus(subject,
TransactionInformationIdentifier.Status.DIGESTED, "请先使用SM3生成摘要"); TransactionInformationIdentifier.Status.DIGESTED, "请先使用SM3生成摘要");
TransactionInformationIdentifier.Status expected = value.getStatus(); TransactionInformationIdentifier.Status expected = value.getStatus();
invoke(value::assemble); invoke(value::assemble);
repository.update(value, expected, "ASSEMBLE", "将固定前缀TXN_与SM3摘要组装为交易信息标识", operator); repository.update(value, expected, "ASSEMBLE", "将固定前缀TXN_与SM3摘要组装为交易信息标识", operator);
scoreService.markStepCompleted(subject.getUserId(), 4); scoreService.markStepCompleted(subject.getUserId(), 4);
return new TransactionIdentifierAssemblyResult(value); return new TransactionInformationIdentifierResult(value);
} }
@Transactional @Transactional(readOnly = true)
public QuotaRequestSourceDataResult quotaRequestSourceData(InstitutionKeySubject subject) { public QuotaRequestSourceDataResult quotaRequestSourceData(InstitutionKeySubject subject) {
TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(), TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(),
subject.getSchoolId(), subject.getClassId()).orElseThrow(() -> subject.getSchoolId(), subject.getClassId()).orElseThrow(() ->
new BusinessException(ErrorCode.VALIDATION_ERROR, new BusinessException(ErrorCode.VALIDATION_ERROR,
"步骤四尚未完成交易信息标识组装,不能查询步骤五前序数据")); "步骤四尚未完成交易信息标识组装,不能查询步骤五前序数据"));
if (value.getStatus().ordinal() < TransactionInformationIdentifier.Status.ASSEMBLED.ordinal() if (value.getStatus().ordinal() < TransactionInformationIdentifier.Status.ASSEMBLED.ordinal()
|| value.getTransactionIdentifier() == null || value.getTransactionIdentifier() == null) {
|| !value.getTransactionIdentifier().matches("TXN_[0-9A-F]{64}")) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"步骤四尚未生成交易信息标识,请先完成步骤四"); "步骤四尚未完成交易信息标识组装,不能查询步骤五前序数据");
} }
return new QuotaRequestSourceDataResult(value); return new QuotaRequestSourceDataResult(value);
} }
@Transactional @Transactional
public QuotaRequestDigestResult digestQuotaRequest(String originalText, public TransactionInformationIdentifierResult prepareQuotaRequest(InstitutionKeySubject subject, String operator) {
InstitutionKeySubject subject, TransactionInformationIdentifier value = requireStatus(subject,
String operator) { TransactionInformationIdentifier.Status.ASSEMBLED, "步骤四尚未完成交易信息标识组装,不能进入步骤五");
TransactionInformationIdentifier value = requireLatest(subject);
TransactionInformationIdentifier.Status expected = value.getStatus(); TransactionInformationIdentifier.Status expected = value.getStatus();
String requestId = "QR_" + LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC) String requestId = "QR_" + LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "_" + String.format("%03d", value.getId() % 1000); .format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "_" + String.format("%03d", value.getId() % 1000);
try { invoke(() -> value.prepareQuotaRequest(requestId));
value.prepareQuotaRequest(requestId, originalText); repository.update(value, expected, "QUOTA_PREPARE",
value.recordQuotaDigest(cryptography.sm3(originalText)); "从步骤四自动流转金额、机构标识和交易信息标识并构造额度请求原文", operator);
} catch (IllegalArgumentException | IllegalStateException exception) { return new TransactionInformationIdentifierResult(value);
errorRecorder.recordScoreError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数据拼接错误");
} }
repository.update(value, expected, "QUOTA_DIGEST",
"接收前端拼接的额度请求原文并使用SM3计算摘要", operator); @Transactional
return new QuotaRequestDigestResult(value.getQuotaDigest()); public TransactionInformationIdentifierResult digestQuotaRequest(InstitutionKeySubject subject, String operator) {
TransactionInformationIdentifier value = requireStatus(subject,
TransactionInformationIdentifier.Status.QUOTA_PREPARED, "请先构造步骤五额度请求");
TransactionInformationIdentifier.Status expected = value.getStatus();
String digest = cryptography.sm3(value.getQuotaOriginalText());
invoke(() -> value.recordQuotaDigest(digest));
repository.update(value, expected, "QUOTA_DIGEST", "使用SM3计算额度请求摘要", operator);
return new TransactionInformationIdentifierResult(value);
} }
@Transactional @Transactional
public TransactionInformationIdentifierResult signQuotaRequest(String privateKey, InstitutionKeySubject subject, public TransactionInformationIdentifierResult signQuotaRequest(String privateKey, InstitutionKeySubject subject,
String operator) { String operator) {
TransactionInformationIdentifier value = requireLatest(subject); TransactionInformationIdentifier value = requireStatus(subject,
if (value.getQuotaDigest() == null) { TransactionInformationIdentifier.Status.QUOTA_DIGESTED, "请先完成步骤五额度请求摘要");
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成步骤五额度请求摘要");
}
TransactionInformationIdentifier.Status expected = value.getStatus(); TransactionInformationIdentifier.Status expected = value.getStatus();
String signature = keyService.signCommercialBank(subject, privateKey, value.getQuotaDigest()); String signature = keyService.signCommercialBank(subject, privateKey, value.getQuotaDigest());
if (signature == null) { if (signature == null) {
@ -194,7 +149,7 @@ public class TransactionInformationIdentifierService {
} }
@Transactional @Transactional
public QuotaRequestMessageResult packageQuotaRequest(InstitutionKeySubject subject, public TransactionInformationIdentifierResult packageQuotaRequest(InstitutionKeySubject subject,
String operator) { String operator) {
TransactionInformationIdentifier value = requireStatus(subject, TransactionInformationIdentifier value = requireStatus(subject,
TransactionInformationIdentifier.Status.QUOTA_SIGNED, "请先完成步骤五额度请求签名"); TransactionInformationIdentifier.Status.QUOTA_SIGNED, "请先完成步骤五额度请求签名");
@ -202,24 +157,21 @@ public class TransactionInformationIdentifierService {
String message = buildQuotaRequestMessage(value); String message = buildQuotaRequestMessage(value);
invoke(() -> value.packageQuotaRequest(message)); invoke(() -> value.packageQuotaRequest(message));
repository.update(value, expected, "QUOTA_PACKAGE", "将额度请求原文、摘要和签名打包成标准报文", operator); repository.update(value, expected, "QUOTA_PACKAGE", "将额度请求原文、摘要和签名打包成标准报文", operator);
return new QuotaRequestMessageResult(value); return new TransactionInformationIdentifierResult(value);
} }
@Transactional @Transactional
public QuotaRequestMessageResult sendQuotaRequest(InstitutionKeySubject subject, String operator) { public TransactionInformationIdentifierResult sendQuotaRequest(InstitutionKeySubject subject, String operator) {
TransactionInformationIdentifier value = requireStatus(subject, TransactionInformationIdentifier value = requireStatus(subject,
TransactionInformationIdentifier.Status.QUOTA_PACKAGED, "请先打包步骤五额度请求报文"); TransactionInformationIdentifier.Status.QUOTA_PACKAGED, "请先打包步骤五额度请求报文");
TransactionInformationIdentifier.Status expected = value.getStatus(); TransactionInformationIdentifier.Status expected = value.getStatus();
invoke(value::sendQuotaRequest); invoke(value::sendQuotaRequest);
repository.update(value, expected, "QUOTA_SEND", "商业银行发送额度请求至央行", operator);
expected = value.getStatus();
invoke(value::receiveQuotaRequest); invoke(value::receiveQuotaRequest);
repository.update(value, expected, "QUOTA_SEND", "商业银行发送额度请求至央行,发送成功即视为央行已收到", operator); repository.update(value, expected, "QUOTA_RECEIVE", "央行成功接收额度请求报文", operator);
scoreService.markStepCompleted(subject.getUserId(), 5); scoreService.markStepCompleted(subject.getUserId(), 5);
return new QuotaRequestMessageResult(value); return new TransactionInformationIdentifierResult(value);
}
@Transactional(readOnly = true)
public CentralBankQuotaRequestResult centralBankQuotaRequest(InstitutionKeySubject subject) {
return new CentralBankQuotaRequestResult(requireLatest(subject));
} }
private static String buildQuotaRequestMessage(TransactionInformationIdentifier value) { private static String buildQuotaRequestMessage(TransactionInformationIdentifier value) {
@ -231,73 +183,28 @@ public class TransactionInformationIdentifierService {
+ "\",\"signature\":\"" + value.getQuotaSignature() + "\"}"; + "\",\"signature\":\"" + value.getQuotaSignature() + "\"}";
} }
@Transactional(readOnly = true)
public QuotaVerificationOriginalTextResult quotaVerificationOriginalText(InstitutionKeySubject subject) {
TransactionInformationIdentifier value = requireLatest(subject);
if (value.getStatus().ordinal() < TransactionInformationIdentifier.Status.QUOTA_RECEIVED.ordinal()
|| value.getQuotaRequestMessage() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"步骤五额度请求尚未发送并由央行接收,不能进入步骤六");
}
return new QuotaVerificationOriginalTextResult(value.getQuotaOriginalText());
}
@Transactional
public QuotaVerificationHashResult calculateQuotaH1(String originalText,
InstitutionKeySubject subject,
String operator) {
TransactionInformationIdentifier value = requireLatest(subject);
if (value.getStatus().ordinal() < TransactionInformationIdentifier.Status.QUOTA_RECEIVED.ordinal()
|| value.getQuotaRequestMessage() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先接收步骤五额度请求报文");
}
if (!value.getQuotaOriginalText().equals(originalText)) {
errorRecorder.recordScoreError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数据拼接错误");
}
TransactionInformationIdentifier.Status expected = value.getStatus();
invoke(() -> value.recordQuotaH1(cryptography.sm3(originalText)));
repository.update(value, expected, "QUOTA_HASH",
"校验前端拼接原文并进行SM3运算得到H1", operator);
return new QuotaVerificationHashResult(value.getQuotaRecomputedDigest());
}
@Transactional @Transactional
public QuotaVerificationSignatureDigestResult recoverQuotaH2(String publicKey, public QuotaVerificationResult verifyQuotaRequest(String publicKey, InstitutionKeySubject subject,
InstitutionKeySubject subject,
String operator) { String operator) {
TransactionInformationIdentifier value = requireLatest(subject); TransactionInformationIdentifier value = requireStatus(subject,
if (value.getQuotaRecomputedDigest() == null) { TransactionInformationIdentifier.Status.QUOTA_RECEIVED,
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤五额度请求尚未发送并由央行接收,不能进入步骤六");
"请先对拼接原文进行SM3哈希运算");
}
if (!keyService.matchesCommercialBankPublicKey(subject, publicKey)) { if (!keyService.matchesCommercialBankPublicKey(subject, publicKey)) {
throw keyError(subject, "必须使用当前实训主体备案的商业银行第二公钥"); throw keyError(subject, "额度验签必须使用当前实训主体备案的商业银行第二公钥");
} }
if (!keyService.verifyCommercialBank(subject, publicKey, String h1 = cryptography.sm3(value.getQuotaOriginalText());
value.getQuotaDigest(), value.getQuotaSignature())) { boolean signatureValid = keyService.verifyCommercialBank(subject, publicKey,
value.getQuotaDigest(), value.getQuotaSignature());
if (!signatureValid) {
throw keyError(subject, "商业银行第二公钥验签未通过"); throw keyError(subject, "商业银行第二公钥验签未通过");
} }
String h2 = signatureValid ? value.getQuotaDigest() : "SIGNATURE_INVALID";
TransactionInformationIdentifier.Status expected = value.getStatus(); TransactionInformationIdentifier.Status expected = value.getStatus();
invoke(() -> value.recordQuotaH2(InstitutionKeyService.BANK_SECOND_KEY, value.getQuotaDigest())); invoke(() -> value.verifyQuotaRequest(InstitutionKeyService.BANK_SECOND_KEY, h1, h2, signatureValid));
repository.update(value, expected, "QUOTA_RECOVER_DIGEST", repository.update(value, expected, "QUOTA_VERIFY",
"使用商业银行第二公钥验签并获取签名确认的摘要H2", operator); "央行使用商业银行第二公钥验签并比对H1与H2", operator);
return new QuotaVerificationSignatureDigestResult(value.getQuotaSignatureDigest());
}
@Transactional
public QuotaVerificationComparisonResult compareQuotaDigests(InstitutionKeySubject subject, String operator) {
TransactionInformationIdentifier value = requireLatest(subject);
if (value.getQuotaRecomputedDigest() == null || value.getQuotaSignatureDigest() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"请先使用商业银行第二公钥验签获取H2");
}
TransactionInformationIdentifier.Status expected = value.getStatus();
invoke(value::compareQuotaDigests);
repository.update(value, expected, "QUOTA_COMPARE",
"比对H1与H2并完成额度请求验签", operator);
scoreService.markStepCompleted(subject.getUserId(), 6); scoreService.markStepCompleted(subject.getUserId(), 6);
return new QuotaVerificationComparisonResult(Boolean.TRUE.equals(value.getQuotaDigestMatches())); return new QuotaVerificationResult(value);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -307,11 +214,6 @@ public class TransactionInformationIdentifierService {
return new QuotaVerificationResult(value); return new QuotaVerificationResult(value);
} }
@Transactional(readOnly = true)
public QuotaRequestMessageResult quotaRequestMessageDetail(InstitutionKeySubject subject) {
return new QuotaRequestMessageResult(requireLatest(subject));
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public TransactionInformationIdentifierResult detail(InstitutionKeySubject subject) { public TransactionInformationIdentifierResult detail(InstitutionKeySubject subject) {
TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(), TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(),
@ -370,12 +272,6 @@ public class TransactionInformationIdentifierService {
return value; return value;
} }
private TransactionInformationIdentifier requireLatest(InstitutionKeySubject subject) {
return repository.findLatestBySubject(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"当前没有额度请求记录"));
}
private TransactionInformationIdentifier requireStatus(InstitutionKeySubject subject, private TransactionInformationIdentifier requireStatus(InstitutionKeySubject subject,
TransactionInformationIdentifier.Status status, TransactionInformationIdentifier.Status status,
String message) { String message) {

@ -137,7 +137,7 @@ public class CurrencyGenerationVerification {
totalQuota = total; totalQuota = total;
usedQuota = used; usedQuota = used;
remainingQuota = remaining; remainingQuota = remaining;
quotaSufficient = used.compareTo(total) <= 0; quotaSufficient = amount.compareTo(remaining) <= 0;
if (quotaSufficient) { if (quotaSufficient) {
status = Status.QUOTA_VERIFIED; status = Status.QUOTA_VERIFIED;
} }

@ -9,7 +9,7 @@ public class TransactionInformationIdentifier {
public enum Status { public enum Status {
PREPARED, DIGESTED, ASSEMBLED, PREPARED, DIGESTED, ASSEMBLED,
QUOTA_PREPARED, QUOTA_DIGESTED, QUOTA_SIGNED, QUOTA_PACKAGED, QUOTA_SENT, QUOTA_RECEIVED, QUOTA_PREPARED, QUOTA_DIGESTED, QUOTA_SIGNED, QUOTA_PACKAGED, QUOTA_SENT, QUOTA_RECEIVED,
QUOTA_H1_CALCULATED, QUOTA_H2_RECOVERED, QUOTA_VERIFIED QUOTA_VERIFIED
} }
private final Long id; private final Long id;
@ -18,8 +18,8 @@ public class TransactionInformationIdentifier {
private final long identifierApplicationId; private final long identifierApplicationId;
private final BigDecimal amount; private final BigDecimal amount;
private final String institutionIdentifier; private final String institutionIdentifier;
private String transactionTimestamp; private final String transactionTimestamp;
private String originalText; private final String originalText;
private String digest; private String digest;
private String transactionIdentifier; private String transactionIdentifier;
private String quotaRequestId; private String quotaRequestId;
@ -112,116 +112,53 @@ public class TransactionInformationIdentifier {
quotaRecomputedDigest, quotaSignatureDigest, quotaSignatureValid, quotaDigestMatches, status); quotaRecomputedDigest, quotaSignatureDigest, quotaSignatureValid, quotaDigestMatches, status);
} }
public void recordDigest(String originalText, String digest) { public void recordDigest(String digest) {
if (status.ordinal() < Status.PREPARED.ordinal()) { requireStatus(Status.PREPARED, "请先获取前序流程数据并拼接请求原文");
throw new IllegalStateException("交易信息标识数据尚未初始化");
}
if (originalText == null || originalText.trim().isEmpty()) {
throw new IllegalArgumentException("拼接请求原文不能为空");
}
if (digest == null || !digest.matches("[0-9A-F]{64}")) { if (digest == null || !digest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("SM3摘要格式不正确"); throw new IllegalArgumentException("SM3摘要格式不正确");
} }
String[] parts = originalText.split("\\|", -1);
if (parts.length != 3 || !parts[2].matches("\\d{14}")) {
throw new IllegalArgumentException("拼接请求原文格式必须为:金额|机构标识|14位时间戳");
}
BigDecimal inputAmount;
try {
inputAmount = new BigDecimal(parts[0]);
} catch (NumberFormatException exception) {
throw new IllegalArgumentException("拼接请求原文中的金额格式不正确");
}
if (inputAmount.compareTo(amount) != 0 || !institutionIdentifier.equals(parts[1])) {
throw new IllegalArgumentException("拼接请求原文中的金额或机构标识与前序数据不一致");
}
this.originalText = originalText;
this.transactionTimestamp = parts[2];
this.digest = digest; this.digest = digest;
this.transactionIdentifier = null;
this.quotaRequestId = null;
this.quotaOriginalText = null;
this.quotaDigest = null;
this.quotaSignature = null;
this.quotaSigningKeyId = null;
this.quotaRequestMessage = null;
this.quotaVerificationKeyId = null;
this.quotaRecomputedDigest = null;
this.quotaSignatureDigest = null;
this.quotaSignatureValid = null;
this.quotaDigestMatches = null;
this.status = Status.DIGESTED; this.status = Status.DIGESTED;
} }
public void assemble() { public void assemble() {
if (digest == null || !digest.matches("[0-9A-F]{64}")) { requireStatus(Status.DIGESTED, "请先使用SM3生成摘要");
throw new IllegalStateException("请先使用SM3生成摘要");
}
this.transactionIdentifier = "TXN_" + digest; this.transactionIdentifier = "TXN_" + digest;
this.status = Status.ASSEMBLED; this.status = Status.ASSEMBLED;
} }
public void prepareQuotaRequest(String quotaRequestId, String quotaOriginalText) { public void prepareQuotaRequest(String quotaRequestId) {
if (transactionIdentifier == null || !transactionIdentifier.matches("TXN_[0-9A-F]{64}")) { requireStatus(Status.ASSEMBLED, "步骤四尚未完成交易信息标识组装");
throw new IllegalStateException("步骤四尚未完成交易信息标识组装");
}
if (quotaRequestId == null || !quotaRequestId.matches("QR_\\d{8}_\\d{3}")) { if (quotaRequestId == null || !quotaRequestId.matches("QR_\\d{8}_\\d{3}")) {
throw new IllegalArgumentException("额度请求编号格式不正确"); throw new IllegalArgumentException("额度请求编号格式不正确");
} }
String expectedOriginalText = amount.toPlainString() + "|" + institutionIdentifier
+ "|" + transactionIdentifier;
if (!expectedOriginalText.equals(quotaOriginalText)) {
throw new IllegalArgumentException("额度请求原文与步骤四数据不一致");
}
this.quotaRequestId = quotaRequestId; this.quotaRequestId = quotaRequestId;
this.quotaOriginalText = quotaOriginalText; this.quotaOriginalText = amount.toPlainString() + "|" + institutionIdentifier
+ "|" + transactionIdentifier;
this.status = Status.QUOTA_PREPARED; this.status = Status.QUOTA_PREPARED;
} }
public void recordQuotaDigest(String quotaDigest) { public void recordQuotaDigest(String quotaDigest) {
if (quotaOriginalText == null) { requireStatus(Status.QUOTA_PREPARED, "请先从步骤四流转金额、机构标识和交易信息标识并构造请求");
throw new IllegalStateException("请先提交步骤五额度请求原文");
}
if (quotaDigest == null || !quotaDigest.matches("[0-9A-F]{64}")) { if (quotaDigest == null || !quotaDigest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("额度请求SM3摘要格式不正确"); throw new IllegalArgumentException("额度请求SM3摘要格式不正确");
} }
this.quotaDigest = quotaDigest; this.quotaDigest = quotaDigest;
clearQuotaSignatureAndFollowingResults();
this.status = Status.QUOTA_DIGESTED; this.status = Status.QUOTA_DIGESTED;
} }
public void recordQuotaSignature(String keyId, String signature) { public void recordQuotaSignature(String keyId, String signature) {
if (quotaDigest == null || !quotaDigest.matches("[0-9A-F]{64}")) { requireStatus(Status.QUOTA_DIGESTED, "请先完成额度请求SM3摘要计算");
throw new IllegalStateException("请先完成步骤五额度请求SM3摘要计算");
}
if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) { if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) {
throw new IllegalArgumentException("额度请求SM2签名信息不正确"); throw new IllegalArgumentException("额度请求SM2签名信息不正确");
} }
this.quotaSigningKeyId = keyId; this.quotaSigningKeyId = keyId;
this.quotaSignature = signature; this.quotaSignature = signature;
clearQuotaPackageAndVerificationResults();
this.status = Status.QUOTA_SIGNED; this.status = Status.QUOTA_SIGNED;
} }
private void clearQuotaSignatureAndFollowingResults() {
this.quotaSigningKeyId = null;
this.quotaSignature = null;
clearQuotaPackageAndVerificationResults();
}
private void clearQuotaPackageAndVerificationResults() {
this.quotaRequestMessage = null;
this.quotaVerificationKeyId = null;
this.quotaRecomputedDigest = null;
this.quotaSignatureDigest = null;
this.quotaSignatureValid = null;
this.quotaDigestMatches = null;
}
public void packageQuotaRequest(String message) { public void packageQuotaRequest(String message) {
if (quotaSignature == null || quotaSignature.trim().isEmpty()) { requireStatus(Status.QUOTA_SIGNED, "请先使用商业银行第二私钥完成额度请求签名");
throw new IllegalStateException("请先使用商业银行第二私钥完成额度请求签名");
}
if (message == null || message.trim().isEmpty()) { if (message == null || message.trim().isEmpty()) {
throw new IllegalArgumentException("额度请求报文不能为空"); throw new IllegalArgumentException("额度请求报文不能为空");
} }
@ -230,57 +167,29 @@ public class TransactionInformationIdentifier {
} }
public void sendQuotaRequest() { public void sendQuotaRequest() {
if (quotaRequestMessage == null || quotaRequestMessage.trim().isEmpty()) { requireStatus(Status.QUOTA_PACKAGED, "请先打包额度请求报文");
throw new IllegalStateException("请先打包额度请求报文");
}
this.status = Status.QUOTA_SENT; this.status = Status.QUOTA_SENT;
} }
public void receiveQuotaRequest() { public void receiveQuotaRequest() {
if (quotaRequestMessage == null || quotaRequestMessage.trim().isEmpty()) { requireStatus(Status.QUOTA_SENT, "央行只能接收已发送的额度请求");
throw new IllegalStateException("央行只能接收已发送的额度请求");
}
this.status = Status.QUOTA_RECEIVED; this.status = Status.QUOTA_RECEIVED;
} }
public void recordQuotaH1(String recomputedDigest) { public void verifyQuotaRequest(String verificationKeyId, String recomputedDigest,
if (quotaRequestMessage == null || quotaSignature == null) { String signatureDigest, boolean signatureValid) {
throw new IllegalStateException("步骤五额度请求尚未发送并由央行接收"); requireStatus(Status.QUOTA_RECEIVED, "步骤五额度请求尚未发送并由央行接收");
} if (verificationKeyId == null || recomputedDigest == null || signatureDigest == null) {
if (recomputedDigest == null || !recomputedDigest.matches("[0-9A-F]{64}")) { throw new IllegalArgumentException("额度请求验证数据不完整");
throw new IllegalArgumentException("H1格式不正确");
}
this.quotaRecomputedDigest = recomputedDigest;
this.quotaVerificationKeyId = null;
this.quotaSignatureDigest = null;
this.quotaSignatureValid = null;
this.quotaDigestMatches = null;
this.status = Status.QUOTA_H1_CALCULATED;
}
public void recordQuotaH2(String verificationKeyId, String signatureDigest) {
if (quotaRecomputedDigest == null) {
throw new IllegalStateException("请先对拼接原文进行SM3哈希运算");
}
if (verificationKeyId == null || signatureDigest == null
|| !signatureDigest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("第二公钥验签获取摘要失败");
} }
this.quotaVerificationKeyId = verificationKeyId; this.quotaVerificationKeyId = verificationKeyId;
this.quotaRecomputedDigest = recomputedDigest;
this.quotaSignatureDigest = signatureDigest; this.quotaSignatureDigest = signatureDigest;
this.quotaSignatureValid = true; this.quotaSignatureValid = signatureValid;
this.quotaDigestMatches = null; this.quotaDigestMatches = recomputedDigest.equalsIgnoreCase(signatureDigest)
this.status = Status.QUOTA_H2_RECOVERED; && recomputedDigest.equalsIgnoreCase(quotaDigest);
} if (!signatureValid || !this.quotaDigestMatches) {
throw new IllegalArgumentException("额度请求验签或H1/H2比对未通过");
public void compareQuotaDigests() {
if (quotaRecomputedDigest == null || quotaSignatureDigest == null) {
throw new IllegalStateException("请先使用商业银行第二公钥验签获取H2");
}
this.quotaDigestMatches = quotaRecomputedDigest != null
&& quotaRecomputedDigest.equalsIgnoreCase(quotaSignatureDigest);
if (!Boolean.TRUE.equals(quotaSignatureValid) || !this.quotaDigestMatches) {
throw new IllegalArgumentException("H1与H2不一致,验签失败");
} }
this.status = Status.QUOTA_VERIFIED; this.status = Status.QUOTA_VERIFIED;
} }

@ -2,12 +2,14 @@ package com.yau.digitalrmb.institutionidentity.infrastructure;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal; import java.math.BigDecimal;
@Mapper @Mapper
public interface CurrencyGenerationRequestMapper extends BaseMapper<CurrencyGenerationRequestEntity> { public interface CurrencyGenerationRequestMapper extends BaseMapper<CurrencyGenerationRequestEntity> {
@Select("SELECT COALESCE(SUM(amount), 0) FROM currency_generation_request WHERE deleted = FALSE") @Select("SELECT COALESCE(SUM(amount), 0) FROM currency_generation_request "
BigDecimal sumActiveAmount(); + "WHERE deleted = FALSE AND id <> #{excludedRequestId}")
BigDecimal sumActiveAmountExcluding(@Param("excludedRequestId") long excludedRequestId);
} }

@ -33,8 +33,8 @@ public class MybatisTransactionInformationIdentifierRepository
entity.setUpdatedBy(operator); entity.setUpdatedBy(operator);
entity.setDeleted(false); entity.setDeleted(false);
mapper.insert(entity); mapper.insert(entity);
insertLog(entity.getId(), "DIGEST", null, value.getStatus().name(), insertLog(entity.getId(), "PREPARE", null, value.getStatus().name(),
"步骤三完成后首次提交交易原文并生成SM3摘要", operator); "从已完成的步骤一至步骤三复制金额和机构标识并生成交易时间戳", operator);
return entity.getId(); return entity.getId();
} }
@ -99,9 +99,6 @@ public class MybatisTransactionInformationIdentifierRepository
.eq(TransactionInformationIdentifierEntity::getId, value.getId()) .eq(TransactionInformationIdentifierEntity::getId, value.getId())
.eq(TransactionInformationIdentifierEntity::getStatus, expectedStatus.name()) .eq(TransactionInformationIdentifierEntity::getStatus, expectedStatus.name())
.eq(TransactionInformationIdentifierEntity::getDeleted, false) .eq(TransactionInformationIdentifierEntity::getDeleted, false)
.set(TransactionInformationIdentifierEntity::getTransactionTimestamp,
value.getTransactionTimestamp())
.set(TransactionInformationIdentifierEntity::getOriginalText, value.getOriginalText())
.set(TransactionInformationIdentifierEntity::getDigest, value.getDigest()) .set(TransactionInformationIdentifierEntity::getDigest, value.getDigest())
.set(TransactionInformationIdentifierEntity::getTransactionIdentifier, .set(TransactionInformationIdentifierEntity::getTransactionIdentifier,
value.getTransactionIdentifier()) value.getTransactionIdentifier())

@ -1,38 +0,0 @@
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
@ApiModel(description = "步骤三签名确权参数")
@Schema(description = "步骤三签名确权参数")
public class ConfirmCurrencyRequest {
@ApiModelProperty(value = "步骤三验证记录编号", required = true, example = "1900000000000000003")
@Schema(description = "步骤三验证记录编号", required = true, example = "1900000000000000003")
@NotNull(message = "步骤三验证编号不能为空")
@Positive(message = "步骤三验证编号必须大于0")
private Long verificationId;
@ApiModelProperty(value = "用户输入的步骤二请求摘要", required = true,
example = "5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B2C3D4E5F6A")
@Schema(description = "用户输入的步骤二请求摘要,必须与请求报文携带的摘要一致", required = true,
example = "5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B2C3D4E5F6A")
@NotBlank(message = "请求摘要不能为空")
private String requestDigest;
@ApiModelProperty(value = "用户输入的中央银行第一SM2私钥完整值", required = true)
@Schema(description = "用户输入的中央银行第一SM2私钥完整值", required = true)
@NotBlank(message = "中央银行第一私钥不能为空")
private String privateKey;
public Long getVerificationId() { return verificationId; }
public void setVerificationId(Long verificationId) { this.verificationId = verificationId; }
public String getRequestDigest() { return requestDigest; }
public void setRequestDigest(String requestDigest) { this.requestDigest = requestDigest; }
public String getPrivateKey() { return privateKey; }
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
}

@ -1,21 +0,0 @@
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotBlank;
@Schema(description = "步骤七控制系统签名哈希运算参数")
public class ControlSystemDigestRequest {
@NotBlank(message = "拼接请求原文不能为空")
@Schema(description = "页面拼接请求原文,格式:金额|机构标识|交易信息标识", required = true,
example = "1589.85|ORG_3A4B5C6D7E8F|TXN_7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F")
private String originalText;
public String getOriginalText() {
return originalText;
}
public void setOriginalText(String originalText) {
this.originalText = originalText;
}
}

@ -1,23 +0,0 @@
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
@ApiModel(description = "步骤四生成交易信息摘要参数")
@Schema(description = "步骤四生成交易信息摘要参数")
public class GenerateTransactionDigestRequest {
@ApiModelProperty(value = "页面拼接请求原文,格式:金额|机构标识|14位时间戳", required = true,
example = "1589.85|ORG_3A4B5C6D7E8F|20260730153330")
// @Schema(description = "页面拼接请求原文,格式:金额|机构标识|14位时间戳", required = true,
// example = "1589.85|ORG_3A4B5C6D7E8F|20260730153330")
private String originalText;
public String getOriginalText() {
return originalText;
}
public void setOriginalText(String originalText) {
this.originalText = originalText;
}
}

@ -8,17 +8,11 @@ import javax.validation.constraints.NotBlank;
public class QuotaControlSignRequest { public class QuotaControlSignRequest {
@NotBlank(message = "商业银行第二私钥不能为空") @NotBlank(message = "商业银行第二私钥不能为空")
private String privateKey; private String privateKey;
@NotBlank(message = "完整额度控制位不能为空")
@Schema(description = "前端拼接得到的完整额度控制位,格式为静态段|动态段|央行控制系统签名段",
example = "STATIC_7AD7D47A81F0230562F912AC5CFA6F44D5A851C978191F4DAB8241CF1BFD7971|DYN_YMUL9SQ0_20260807141518|CB_CTRL_30450220ABCDEF")
private String completeControlBit;
@NotBlank(message = "签名算法不能为空") @NotBlank(message = "签名算法不能为空")
@Schema(description = "必须为SM2", example = "SM2") @Schema(description = "必须为SM2", example = "SM2")
private String hashAlgorithm; private String hashAlgorithm;
public String getPrivateKey() { return privateKey; } public String getPrivateKey() { return privateKey; }
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
public String getCompleteControlBit() { return completeControlBit; }
public void setCompleteControlBit(String completeControlBit) { this.completeControlBit = completeControlBit; }
public String getHashAlgorithm() { return hashAlgorithm; } public String getHashAlgorithm() { return hashAlgorithm; }
public void setHashAlgorithm(String hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; } public void setHashAlgorithm(String hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; }
} }

@ -1,21 +0,0 @@
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotBlank;
@Schema(description = "步骤五生成额度请求摘要参数")
public class QuotaRequestDigestRequest {
@NotBlank(message = "额度请求原文不能为空")
@Schema(description = "前端拼接的额度请求原文,格式:金额|机构标识|交易信息标识", required = true,
example = "1589.85|ORG_3A4B5C6D7E8F|TXN_7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F")
private String originalText;
public String getOriginalText() {
return originalText;
}
public void setOriginalText(String originalText) {
this.originalText = originalText;
}
}

@ -1,21 +0,0 @@
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotBlank;
@Schema(description = "步骤六SM3哈希运算参数")
public class QuotaVerificationHashRequest {
@NotBlank(message = "待哈希原文不能为空")
@Schema(description = "前端从额度请求报文提取并拼接的原文,格式:金额|机构标识|交易信息标识", required = true,
example = "1589.85|ORG_3A4B5C6D7E8F|TXN_7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F")
private String originalText;
public String getOriginalText() {
return originalText;
}
public void setOriginalText(String originalText) {
this.originalText = originalText;
}
}

@ -11,7 +11,6 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResu
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.InstitutionVerificationProcessResult; import com.yau.digitalrmb.institutionidentity.application.InstitutionVerificationProcessResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmInstitutionIdentifierRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmInstitutionIdentifierRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.VerifyInstitutionApplicationRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.VerifyInstitutionApplicationRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
@ -82,13 +81,11 @@ public class CentralBankInstitutionIdentifierController {
@PostMapping("/applications/confirmation") @PostMapping("/applications/confirmation")
@Operation(summary = "央行签名确权并组装机构标识", @Operation(summary = "央行签名确权并组装机构标识",
description = "仅VERIFIED状态可操作;privateKey可选,传入时校验央行第一私钥。") description = "仅VERIFIED状态可操作;privateKey可选,传入时校验央行第一私钥。")
public ApiResponse<SignatureResult> confirmIdentifier( public ApiResponse<InstitutionIdentifierConfirmationResult> confirmIdentifier(
@Valid @RequestBody(required = false) ConfirmInstitutionIdentifierRequest request) { @Valid @RequestBody(required = false) ConfirmInstitutionIdentifierRequest request) {
JwtUser jwtUser = AuthContextHolder.get(); JwtUser jwtUser = AuthContextHolder.get();
InstitutionIdentifierConfirmationResult result = verificationService.confirm( return ApiResponse.success(verificationService.confirm(request == null ? null : request.getPrivateKey(),
request == null ? null : request.getPrivateKey(), InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername());
return ApiResponse.success(new SignatureResult(result.getCentralBankSignature()), traceId());
} }
@GetMapping("/applications/final-identifier") @GetMapping("/applications/final-identifier")

@ -1,9 +1,10 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.CentralBankQuotaControlBitResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitService; import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitService;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser;
import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter; import com.yau.digitalrmb.shared.web.TraceIdFilter;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -19,12 +20,17 @@ import javax.annotation.Resource;
public class CentralBankQuotaControlBitController { public class CentralBankQuotaControlBitController {
@Resource private QuotaControlBitService service; @Resource private QuotaControlBitService service;
@PostMapping("/steps/receive")
@Operation(summary = "中央银行接收额度控制位数据包报文", description = "接收后receiveStatus由1变为2。")
public ApiResponse<QuotaControlBitResult> receive() {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.receive(InstitutionKeySubject.from(user), user.getUsername()), traceId());
}
@GetMapping @GetMapping
@Operation(summary = "查询中央银行接收额度控制位报文信息", @Operation(summary = "查询央行接收状态和报文", description = "receiveStatus:1未收到,2已收到。")
description = "只读查询,不推进流程。商业银行未发送时status=1(待确认);发送成功后status=2(已确认),并返回额度控制位编号、静态段、动态段、央行签名段和商业银行签名。") public ApiResponse<QuotaControlBitResult> detail() {
public ApiResponse<CentralBankQuotaControlBitResult> detail() { return ApiResponse.success(service.detail(InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
return ApiResponse.success(service.centralBankMessage(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
private String traceId() { return MDC.get(TraceIdFilter.MDC_KEY); } private String traceId() { return MDC.get(TraceIdFilter.MDC_KEY); }

@ -1,36 +0,0 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.CentralBankQuotaRequestResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierService;
import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/v1/institution-identifiers/central-bank/quota-requests")
@Tag(name = "步骤五:中央银行接收额度请求")
public class CentralBankQuotaRequestController {
@Resource
private TransactionInformationIdentifierService service;
@GetMapping
@Operation(summary = "查询中央银行接收状态和额度请求信息",
description = "央行端仅保留本接口。仅返回六项额度请求报文字段和status;商业银行未发送时status=1,发送后status=2。")
public ApiResponse<CentralBankQuotaRequestResult> detail() {
return ApiResponse.success(service.centralBankQuotaRequest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -1,15 +1,17 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.CommercialBankControlSystemSignatureResult; import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureService; import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser;
import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter; import com.yau.digitalrmb.shared.web.TraceIdFilter;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.MDC; import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -17,19 +19,22 @@ import javax.annotation.Resource;
@RestController @RestController
@RequestMapping("/api/v1/institution-identifiers/commercial-bank/control-system-signatures") @RequestMapping("/api/v1/institution-identifiers/commercial-bank/control-system-signatures")
@Tag(name = "步骤七:商业银行接收控制系统签名报文") @Tag(name = "步骤七:商业银行接收控制系统签名")
public class CommercialBankControlSystemSignatureController { public class CommercialBankControlSystemSignatureController {
@Resource private ControlSystemSignatureService service; @Resource private ControlSystemSignatureService service;
@GetMapping @PostMapping("/steps/receive")
@Operation(summary = "查询商业银行接收控制系统签名报文信息", @Operation(summary = "商业银行接收中央银行控制系统签名反馈")
description = "只读查询,不推进流程。中央银行未发送时status=1;发送成功后status=2,并返回控制编号、金额、机构标识、交易信息标识和控制系统签名。") public ApiResponse<ControlSystemSignatureResult> receive() {
public ApiResponse<CommercialBankControlSystemSignatureResult> detail() { JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.commercialBankMessage( return ApiResponse.success(service.receive(InstitutionKeySubject.from(user), user.getUsername()), traceId());
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
private String traceId() { @GetMapping
return MDC.get(TraceIdFilter.MDC_KEY); @Operation(summary = "查询商业银行收到的控制系统签名反馈")
public ApiResponse<ControlSystemSignatureResult> detail() {
return ApiResponse.success(service.detail(InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
private String traceId() { return MDC.get(TraceIdFilter.MDC_KEY); }
} }

@ -1,11 +1,8 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.ControlSystemDigestResult;
import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureResult; import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureService; import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ControlSystemDigestRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ControlSystemSignRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.ControlSystemSignRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser; import com.yau.digitalrmb.security.context.JwtUser;
@ -14,6 +11,7 @@ import com.yau.digitalrmb.shared.web.TraceIdFilter;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.MDC; import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -28,24 +26,24 @@ import javax.validation.Valid;
public class ControlSystemSignatureController { public class ControlSystemSignatureController {
@Resource private ControlSystemSignatureService service; @Resource private ControlSystemSignatureService service;
@PostMapping("/steps/prepare")
@Operation(summary = "生成控制系统签名基础数据",
description = "自动读取步骤六已验证的交易金额、机构标识和交易信息标识,生成控制编号并拼接原文。")
public ApiResponse<ControlSystemSignatureResult> prepare() {
return execute((subject, operator) -> service.prepare(subject, operator));
}
@PostMapping("/steps/digest") @PostMapping("/steps/digest")
@Operation(summary = "对拼接请求原文进行SM3哈希运算", @Operation(summary = "计算控制系统签名摘要")
description = "前端提交页面输入框完整原文,格式为金额|机构标识|交易信息标识。后端按当前JWT主体校验三项前序数据,生成控制编号并对输入原文原样计算SM3摘要。") public ApiResponse<ControlSystemSignatureResult> digest() {
public ApiResponse<ControlSystemDigestResult> digest( return execute((subject, operator) -> service.digest(subject, operator));
@Valid @RequestBody ControlSystemDigestRequest request) {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.digest(request.getOriginalText(), InstitutionKeySubject.from(user),
user.getUsername()), traceId());
} }
@PostMapping("/steps/sign") @PostMapping("/steps/sign")
@Operation(summary = "使用中央银行第一私钥生成签名") @Operation(summary = "使用中央银行第一私钥生成签名")
public ApiResponse<SignatureResult> sign( public ApiResponse<ControlSystemSignatureResult> sign(
@Valid @RequestBody ControlSystemSignRequest request) { @Valid @RequestBody ControlSystemSignRequest request) {
JwtUser user = AuthContextHolder.get(); return execute((subject, operator) -> service.sign(request.getPrivateKey(), subject, operator));
ControlSystemSignatureResult result = service.sign(request.getPrivateKey(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new SignatureResult(result.getSignature()), traceId());
} }
@PostMapping("/steps/package") @PostMapping("/steps/package")
@ -60,6 +58,12 @@ public class ControlSystemSignatureController {
return execute((subject, operator) -> service.send(subject, operator)); return execute((subject, operator) -> service.send(subject, operator));
} }
@GetMapping
@Operation(summary = "查询中央银行控制系统签名数据")
public ApiResponse<ControlSystemSignatureResult> detail() {
return ApiResponse.success(service.detail(InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private ApiResponse<ControlSystemSignatureResult> execute(OperationExecutor executor) { private ApiResponse<ControlSystemSignatureResult> execute(OperationExecutor executor) {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
return ApiResponse.success(executor.execute(InstitutionKeySubject.from(user), user.getUsername()), traceId()); return ApiResponse.success(executor.execute(InstitutionKeySubject.from(user), user.getUsername()), traceId());

@ -1,10 +1,8 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.CurrencyDigestResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationRequestService; import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationRequestService;
import com.yau.digitalrmb.institutionidentity.application.CurrencyRequestResult; import com.yau.digitalrmb.institutionidentity.application.CurrencyRequestResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyRequestStepRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyRequestStepRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.GenerateCurrencyDigestRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.GenerateCurrencyDigestRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
@ -33,7 +31,7 @@ public class CurrencyGenerationRequestController {
@PostMapping("/steps/digest") @PostMapping("/steps/digest")
@Operation(summary = "生成请求SM3摘要", @Operation(summary = "生成请求SM3摘要",
description = "步骤二首个操作。前端提交金额、投放节点、步骤一反馈的ORG机构标识和页面当前时间戳;后端校验机构标识归属,按机构标识|金额|投放节点|时间戳拼接原文并全部持久化。同一ORG重复提交时更新同一条记录。") description = "步骤二首个操作。前端提交金额、投放节点、步骤一反馈的ORG机构标识和页面当前时间戳;后端校验机构标识归属,按机构标识|金额|投放节点|时间戳拼接原文并全部持久化。同一ORG重复提交时更新同一条记录。")
public ApiResponse<CurrencyDigestResult> digest( public ApiResponse<CurrencyRequestResult> digest(
@Valid @RequestBody GenerateCurrencyDigestRequest request) { @Valid @RequestBody GenerateCurrencyDigestRequest request) {
JwtUser jwtUser = AuthContextHolder.get(); JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(currencyRequestService.generateDigest(request.getAmount(), return ApiResponse.success(currencyRequestService.generateDigest(request.getAmount(),
@ -43,12 +41,11 @@ public class CurrencyGenerationRequestController {
@PostMapping("/steps/sign") @PostMapping("/steps/sign")
@Operation(summary = "使用商业银行第二私钥进行SM2签名") @Operation(summary = "使用商业银行第二私钥进行SM2签名")
public ApiResponse<SignatureResult> sign( public ApiResponse<CurrencyRequestResult> sign(
@Valid @RequestBody CurrencyRequestStepRequest request) { @Valid @RequestBody CurrencyRequestStepRequest request) {
JwtUser jwtUser = AuthContextHolder.get(); JwtUser jwtUser = AuthContextHolder.get();
CurrencyRequestResult result = currencyRequestService.sign(request.getPrivateKey(), return ApiResponse.success(currencyRequestService.sign(request.getPrivateKey(),
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()); InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
return ApiResponse.success(new SignatureResult(result.getBankSignature()), traceId());
} }
@PostMapping("/steps/package") @PostMapping("/steps/package")

@ -1,15 +1,10 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.CurrencyConfirmationMessageResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyConfirmationSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyDigestResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationVerificationResult; import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationVerificationResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyQuotaVerificationResult; import com.yau.digitalrmb.institutionidentity.application.CurrencyQuotaVerificationResult;
import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationVerificationService; import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationVerificationService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CalculateCurrencyDigestRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.CalculateCurrencyDigestRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmCurrencyRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyVerificationStepRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyVerificationStepRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ReceiveCurrencyVerificationRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.ReceiveCurrencyVerificationRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
@ -52,12 +47,11 @@ public class CurrencyGenerationVerificationController {
@PostMapping("/steps/calculate-digest") @PostMapping("/steps/calculate-digest")
@Operation(summary = "运算摘要一", @Operation(summary = "运算摘要一",
description = "对应页面“运算”按钮。请求原文由用户在页面输入,后端计算摘要一=SM3(requestOriginalText),不要求先完成第二公钥SM2验证,可重复运算并覆盖摘要一,不执行摘要匹配。") description = "对应页面“运算”按钮。请求原文由用户在页面输入,后端计算摘要一=SM3(requestOriginalText),不要求先完成第二公钥SM2验证,可重复运算并覆盖摘要一,不执行摘要匹配。")
public ApiResponse<CurrencyDigestResult> calculateDigest( public ApiResponse<CurrencyGenerationVerificationResult> calculateDigest(
@Valid @RequestBody CalculateCurrencyDigestRequest request) { @Valid @RequestBody CalculateCurrencyDigestRequest request) {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
CurrencyDigestResult result = service.calculateDigest(request.getVerificationId(), return ok(service.calculateDigest(request.getVerificationId(), request.getRequestOriginalText(),
request.getRequestOriginalText(), InstitutionKeySubject.from(user), user.getUsername()); InstitutionKeySubject.from(user), user.getUsername()));
return ApiResponse.success(result, MDC.get(TraceIdFilter.MDC_KEY));
} }
@PostMapping("/steps/match-digest") @PostMapping("/steps/match-digest")
@ -80,16 +74,12 @@ public class CurrencyGenerationVerificationController {
} }
@PostMapping("/steps/confirm") @PostMapping("/steps/confirm")
@Operation(summary = "央行第一私钥签名确权", @Operation(summary = "签名确权", description = "对应页面“签名确权”按钮,只生成央行签名和确权时间,不打包报文。")
description = "对应页面“签名确权”按钮。用户输入请求摘要和央行第一私钥;后端校验摘要与步骤二请求摘要一致后,使用该私钥对输入摘要执行SM2签名。只生成央行签名和确权时间,不打包报文。") public ApiResponse<CurrencyGenerationVerificationResult> confirm(
public ApiResponse<SignatureResult> confirm( @Valid @RequestBody CurrencyVerificationStepRequest request) {
@Valid @RequestBody ConfirmCurrencyRequest request) {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
CurrencyConfirmationSignatureResult result = service.confirm(request.getVerificationId(), return ok(service.confirm(request.getVerificationId(), request.getPrivateKey(),
request.getRequestDigest(), request.getPrivateKey(), InstitutionKeySubject.from(user), InstitutionKeySubject.from(user), user.getUsername()));
user.getUsername());
return ApiResponse.success(new SignatureResult(result.getCentralBankSignature()),
MDC.get(TraceIdFilter.MDC_KEY));
} }
@PostMapping("/steps/package-response") @PostMapping("/steps/package-response")
@ -109,14 +99,6 @@ public class CurrencyGenerationVerificationController {
return ok(service.returnResponse(request.getVerificationId(), InstitutionKeySubject.from(user), user.getUsername())); return ok(service.returnResponse(request.getVerificationId(), InstitutionKeySubject.from(user), user.getUsername()));
} }
@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));
}
@GetMapping("/current") @GetMapping("/current")
@Operation(summary = "查询当前步骤三验证记录", @Operation(summary = "查询当前步骤三验证记录",
description = "不传任何业务参数。根据当前Token中的用户、学校和班级,查询未逻辑删除的最新步骤三记录,返回verificationId及页面完整数据。") description = "不传任何业务参数。根据当前Token中的用户、学校和班级,查询未逻辑删除的最新步骤三记录,返回verificationId及页面完整数据。")

@ -11,7 +11,6 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResu
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.InstitutionSignatureResult; import com.yau.digitalrmb.institutionidentity.application.InstitutionSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionTrainingResetResult; import com.yau.digitalrmb.institutionidentity.application.InstitutionTrainingResetResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionTrainingResetService; import com.yau.digitalrmb.institutionidentity.application.InstitutionTrainingResetService;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.GenerateInstitutionIdentifierRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.GenerateInstitutionIdentifierRequest;
@ -85,12 +84,11 @@ public class InstitutionIdentifierController {
@PostMapping("/steps/sign") @PostMapping("/steps/sign")
@Operation(summary = "使用商业银行第二私钥进行SM2签名", @Operation(summary = "使用商业银行第二私钥进行SM2签名",
description = "仅DIGESTED状态可操作,完成后保存签名并进入SIGNED状态。") description = "仅DIGESTED状态可操作,完成后保存签名并进入SIGNED状态。")
public ApiResponse<SignatureResult> sign( public ApiResponse<InstitutionSignatureResult> sign(
@Valid @RequestBody InstitutionIdentifierStepRequest request) { @Valid @RequestBody InstitutionIdentifierStepRequest request) {
JwtUser jwtUser = AuthContextHolder.get(); JwtUser jwtUser = AuthContextHolder.get();
InstitutionSignatureResult result = stepService.sign(request.getPrivateKey(), return ApiResponse.success(stepService.sign(request.getPrivateKey(),
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()); InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
return ApiResponse.success(new SignatureResult(result.getSignature()), traceId());
} }
@PostMapping("/steps/package") @PostMapping("/steps/package")

@ -1,13 +1,11 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; 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.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitResult; import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitService; import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitService;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlDynamicSegmentResult; import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaControlDynamicRequest;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlStaticSegmentResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaControlSignRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaControlSignRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaControlStaticRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser; import com.yau.digitalrmb.security.context.JwtUser;
import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ApiResponse;
@ -27,46 +25,22 @@ public class QuotaControlBitController {
@Resource private QuotaControlBitService service; @Resource private QuotaControlBitService service;
@PostMapping("/steps/static-segment") @PostMapping("/steps/static-segment")
@Operation(summary = "生成静态段", @Operation(summary = "生成静态段", description = "用户输入金额、机构标识和交易信息标识;后端与步骤七数据逐项校验后计算SM3静态段。")
description = "无需请求体。后端按当前JWT主体读取步骤七金额、机构标识和交易信息标识,计算SM3并仅返回staticSegment。") public ApiResponse<QuotaControlBitResult> staticSegment(@Valid @RequestBody QuotaControlStaticRequest request) {
public ApiResponse<QuotaControlStaticSegmentResult> staticSegment() { return execute((s, o) -> service.staticSegment(request.getAmount(), request.getInstitutionIdentifier(),
JwtUser user = AuthContextHolder.get(); request.getTransactionIdentifier(), s, o));
return ApiResponse.success(service.staticSegment(
InstitutionKeySubject.from(user), user.getUsername()), traceId());
} }
@PostMapping("/steps/dynamic-segment") @PostMapping("/steps/dynamic-segment")
@Operation(summary = "生成动态段", @Operation(summary = "生成动态段", description = "用户输入8位随机数和14位时间戳。")
description = "无需请求体。后端生成8位随机值和当前14位时间戳,并仅返回dynamicSegment。") public ApiResponse<QuotaControlBitResult> dynamicSegment(@Valid @RequestBody QuotaControlDynamicRequest request) {
public ApiResponse<QuotaControlDynamicSegmentResult> dynamicSegment() { return execute((s, o) -> service.dynamicSegment(request.getRandomValue(), request.getTimestamp(), s, o));
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.dynamicSegment(
InstitutionKeySubject.from(user), user.getUsername()), traceId());
}
@GetMapping("/segments/static")
@Operation(summary = "查询静态段",
description = "按当前JWT实训主体查询已生成的步骤八静态段,仅返回staticSegment,不重新计算且不推进状态。")
public ApiResponse<QuotaControlStaticSegmentResult> queryStaticSegment() {
return ApiResponse.success(service.queryStaticSegment(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@GetMapping("/segments/dynamic")
@Operation(summary = "查询动态段",
description = "按当前JWT实训主体查询已生成的步骤八动态段,仅返回dynamicSegment,不重新生成且不推进状态。")
public ApiResponse<QuotaControlDynamicSegmentResult> queryDynamicSegment() {
return ApiResponse.success(service.queryDynamicSegment(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
@PostMapping("/steps/read-central-bank-signature") @PostMapping("/steps/read-central-bank-signature")
@Operation(summary = "读取步骤七央行控制系统签名段", @Operation(summary = "读取步骤七央行控制系统签名段")
description = "成功响应的data仅返回centralBankSignatureSegment。") public ApiResponse<QuotaControlBitResult> readCentralBankSignature() {
public ApiResponse<CentralBankSignatureSegmentResult> readCentralBankSignature() { return execute((s, o) -> service.readCentralBankSignature(s, o));
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.readCentralBankSignature(
InstitutionKeySubject.from(user), user.getUsername()), traceId());
} }
@PostMapping("/steps/assemble") @PostMapping("/steps/assemble")
@ -74,13 +48,9 @@ public class QuotaControlBitController {
public ApiResponse<QuotaControlBitResult> assemble() { return execute((s, o) -> service.assemble(s, o)); } public ApiResponse<QuotaControlBitResult> assemble() { return execute((s, o) -> service.assemble(s, o)); }
@PostMapping("/steps/sign") @PostMapping("/steps/sign")
@Operation(summary = "商业银行第二私钥签名确认", @Operation(summary = "商业银行第二私钥签名确认", description = "前端提交完整商业银行第二SM2私钥;错误私钥或错误算法会累计实训错误次数。")
description = "前端提交完整额度控制位、商业银行第二SM2私钥和SM2算法。后端校验完整额度控制位与步骤八拼接结果一致后进行签名;错误内容、私钥或算法会累计实训错误次数。") public ApiResponse<QuotaControlBitResult> sign(@Valid @RequestBody QuotaControlSignRequest request) {
public ApiResponse<SignatureResult> sign(@Valid @RequestBody QuotaControlSignRequest request) { return execute((s, o) -> service.sign(request.getPrivateKey(), request.getHashAlgorithm(), s, o));
JwtUser user = AuthContextHolder.get();
QuotaControlBitResult result = service.sign(request.getPrivateKey(), request.getCompleteControlBit(),
request.getHashAlgorithm(), InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new SignatureResult(result.getBankSignature()), traceId());
} }
@PostMapping("/steps/package") @PostMapping("/steps/package")

@ -1,13 +1,9 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.QuotaRequestDigestResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaRequestMessageResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaRequestSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaRequestSourceDataResult; import com.yau.digitalrmb.institutionidentity.application.QuotaRequestSourceDataResult;
import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierResult; import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierResult;
import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierService; import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierService;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaRequestDigestRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaRequestSignRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaRequestSignRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser; import com.yau.digitalrmb.security.context.JwtUser;
@ -27,65 +23,65 @@ import javax.validation.Valid;
@RestController @RestController
@RequestMapping("/api/v1/institution-identifiers/quota-requests") @RequestMapping("/api/v1/institution-identifiers/quota-requests")
@Tag(name = "步骤五:商业银行生成额度请求") @Tag(name = "步骤五:额度请求")
public class QuotaRequestController { public class QuotaRequestController {
@Resource @Resource
private TransactionInformationIdentifierService service; private TransactionInformationIdentifierService service;
@GetMapping("/source-data") @GetMapping("/source-data")
@Operation(summary = "查询额度请求前序参数", @Operation(summary = "查询步骤五前序数据",
description = "读取步骤四生成的金额、机构标识和交易信息标识,不推进业务状态。") description = "只读查询步骤二货币金额、步骤一机构标识和步骤四交易信息标识;步骤四未完成组装时禁止查询,不创建或修改步骤五记录。")
public ApiResponse<QuotaRequestSourceDataResult> sourceData() { public ApiResponse<QuotaRequestSourceDataResult> sourceData() {
return ApiResponse.success(service.quotaRequestSourceData( return ApiResponse.success(service.quotaRequestSourceData(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), MDC.get(TraceIdFilter.MDC_KEY));
}
@PostMapping("/steps/prepare")
@Operation(summary = "构造额度请求",
description = "从已完成的步骤四自动流转金额、机构标识和交易信息标识,生成quotaRequestId并拼接额度请求原文。")
public ApiResponse<TransactionInformationIdentifierResult> prepare() {
JwtUser user = AuthContextHolder.get();
return ok(service.prepareQuotaRequest(InstitutionKeySubject.from(user), user.getUsername()));
} }
@PostMapping("/steps/digest") @PostMapping("/steps/digest")
@Operation(summary = "生成额度请求SM3摘要", @Operation(summary = "使用SM3生成额度请求摘要")
description = "前端按金额|机构标识|交易信息标识拼接完整原文后提交;后端只校验原文与步骤四数据一致并计算SM3,不提供原文拼接接口。") public ApiResponse<TransactionInformationIdentifierResult> digest() {
public ApiResponse<QuotaRequestDigestResult> digest(
@Valid @RequestBody QuotaRequestDigestRequest request) {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.digestQuotaRequest(request.getOriginalText(), return ok(service.digestQuotaRequest(InstitutionKeySubject.from(user), user.getUsername()));
InstitutionKeySubject.from(user), user.getUsername()), traceId());
} }
@PostMapping("/steps/sign") @PostMapping("/steps/sign")
@Operation(summary = "使用商业银行第二私钥计算SM2签名", @Operation(summary = "使用商业银行第二私钥进行SM2签名")
description = "校验商业银行第二私钥并完成签名落库,响应只返回signature签名值。") public ApiResponse<TransactionInformationIdentifierResult> sign(
public ApiResponse<QuotaRequestSignatureResult> sign(
@Valid @RequestBody QuotaRequestSignRequest request) { @Valid @RequestBody QuotaRequestSignRequest request) {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
TransactionInformationIdentifierResult result = service.signQuotaRequest( return ok(service.signQuotaRequest(request.getPrivateKey(),
request.getPrivateKey(), InstitutionKeySubject.from(user), user.getUsername()); InstitutionKeySubject.from(user), user.getUsername()));
return ApiResponse.success(new QuotaRequestSignatureResult(result.getQuotaSignature()), traceId());
} }
@PostMapping("/steps/package") @PostMapping("/steps/package")
@Operation(summary = "打包额度请求报文") @Operation(summary = "打包额度请求标准报文")
public ApiResponse<QuotaRequestMessageResult> packageRequest() { public ApiResponse<TransactionInformationIdentifierResult> packageRequest() {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.packageQuotaRequest( return ok(service.packageQuotaRequest(InstitutionKeySubject.from(user), user.getUsername()));
InstitutionKeySubject.from(user), user.getUsername()), traceId());
} }
@PostMapping("/steps/send") @PostMapping("/steps/send")
@Operation(summary = "商业银行发送额度请求报文", @Operation(summary = "发送额度请求至央行",
description = "发送成功即视为央行已收到,央行查询状态由1(未发送)变为2(已发送)。") description = "只允许发送已打包报文;发送成功后央行接收状态变为RECEIVED。")
public ApiResponse<QuotaRequestMessageResult> send() { public ApiResponse<TransactionInformationIdentifierResult> send() {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.sendQuotaRequest( return ok(service.sendQuotaRequest(InstitutionKeySubject.from(user), user.getUsername()));
InstitutionKeySubject.from(user), user.getUsername()), traceId());
} }
@GetMapping @GetMapping
@Operation(summary = "查询商业银行额度请求处理信息") @Operation(summary = "查询当前用户最新额度请求及央行接收状态")
public ApiResponse<QuotaRequestMessageResult> detail() { public ApiResponse<TransactionInformationIdentifierResult> detail() {
return ApiResponse.success(service.quotaRequestMessageDetail( return ok(service.detail(InstitutionKeySubject.from(AuthContextHolder.get())));
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
private String traceId() { private ApiResponse<TransactionInformationIdentifierResult> ok(TransactionInformationIdentifierResult result) {
return MDC.get(TraceIdFilter.MDC_KEY); return ApiResponse.success(result, MDC.get(TraceIdFilter.MDC_KEY));
} }
} }

@ -1,13 +1,8 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.QuotaVerificationComparisonResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaVerificationHashResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaVerificationOriginalTextResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaVerificationResult; import com.yau.digitalrmb.institutionidentity.application.QuotaVerificationResult;
import com.yau.digitalrmb.institutionidentity.application.QuotaVerificationSignatureDigestResult;
import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierService; import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierService;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaVerificationHashRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaVerificationRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.QuotaVerificationRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser; import com.yau.digitalrmb.security.context.JwtUser;
@ -32,41 +27,14 @@ public class QuotaVerificationController {
@Resource @Resource
private TransactionInformationIdentifierService service; private TransactionInformationIdentifierService service;
@PostMapping("/steps/concatenate-original-text") @PostMapping("/verify")
@Operation(summary = "提取并拼接额度请求原文", @Operation(summary = "使用商业银行第二公钥验签并比对H1和H2",
description = "从央行已接收的步骤五额度请求报文提取amount、orgId、txnId,并返回金额|机构标识|交易信息标识;接口只读,不计算哈希、不推进状态。") description = "仅允许读取步骤五已由央行接收的报文;H1为请求原文重算SM3,H2为第二公钥验签恢复并确认的摘要。")
public ApiResponse<QuotaVerificationOriginalTextResult> originalText() { public ApiResponse<QuotaVerificationResult> verify(
return ApiResponse.success(service.quotaVerificationOriginalText(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/steps/hash")
@Operation(summary = "对前端拼接原文进行SM3哈希运算",
description = "前端从步骤五报文提取amount、orgId、txnId并按金额|机构标识|交易信息标识拼接后提交;后端校验原文与已接收报文一致,计算并持久化H1。")
public ApiResponse<QuotaVerificationHashResult> hash(
@Valid @RequestBody QuotaVerificationHashRequest request) {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.calculateQuotaH1(request.getOriginalText(),
InstitutionKeySubject.from(user), user.getUsername()), traceId());
}
@PostMapping("/steps/recover-signature-digest")
@Operation(summary = "使用商业银行第二公钥验签并获取摘要H2",
description = "SM2签名不可逆;本接口用第二公钥验证签名确实对应报文摘要,验证成功后返回签名所确认的H2。")
public ApiResponse<QuotaVerificationSignatureDigestResult> recoverSignatureDigest(
@Valid @RequestBody QuotaVerificationRequest request) { @Valid @RequestBody QuotaVerificationRequest request) {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.recoverQuotaH2(request.getPublicKey(), return ok(service.verifyQuotaRequest(request.getPublicKey(), InstitutionKeySubject.from(user),
InstitutionKeySubject.from(user), user.getUsername()), traceId()); user.getUsername()));
}
@PostMapping("/steps/compare-digests")
@Operation(summary = "比对H1与H2",
description = "H1和H2一致且第二公钥验签有效时完成步骤六。")
public ApiResponse<QuotaVerificationComparisonResult> compareDigests() {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.compareQuotaDigests(
InstitutionKeySubject.from(user), user.getUsername()), traceId());
} }
@GetMapping @GetMapping
@ -76,10 +44,6 @@ public class QuotaVerificationController {
} }
private ApiResponse<QuotaVerificationResult> ok(QuotaVerificationResult result) { private ApiResponse<QuotaVerificationResult> ok(QuotaVerificationResult result) {
return ApiResponse.success(result, traceId()); return ApiResponse.success(result, MDC.get(TraceIdFilter.MDC_KEY));
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
} }
} }

@ -1,11 +1,8 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest; package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.TransactionDigestResult;
import com.yau.digitalrmb.institutionidentity.application.TransactionIdentifierAssemblyResult;
import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierResult; import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierResult;
import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierService; import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierService;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.GenerateTransactionDigestRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser; import com.yau.digitalrmb.security.context.JwtUser;
import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ApiResponse;
@ -15,12 +12,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.MDC; import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Valid;
@RestController @RestController
@RequestMapping("/api/v1/institution-identifiers/transaction-information-identifiers") @RequestMapping("/api/v1/institution-identifiers/transaction-information-identifiers")
@ -29,24 +24,27 @@ public class TransactionInformationIdentifierController {
@Resource @Resource
private TransactionInformationIdentifierService service; private TransactionInformationIdentifierService service;
@PostMapping("/steps/prepare")
@Operation(summary = "获取前序数据并拼接交易信息原文",
description = "步骤一必须FEEDBACKED、步骤二必须RECEIVED、步骤三必须RESPONSE_RETURNED;金额和机构标识由后端自动流转。")
public ApiResponse<TransactionInformationIdentifierResult> prepare() {
JwtUser user = AuthContextHolder.get();
return ok(service.prepare(InstitutionKeySubject.from(user), user.getUsername()));
}
@PostMapping("/steps/digest") @PostMapping("/steps/digest")
@Operation(summary = "使用SM3生成交易信息摘要", @Operation(summary = "使用SM3生成交易信息摘要")
description = "仅要求步骤三已完成确认报文返回。首次正确提交创建步骤四记录;后续正确提交更新同一记录," public ApiResponse<TransactionInformationIdentifierResult> digest() {
+ "不新增数据且允许在组装后重新计算。格式或内容错误时累计一次实训错误并返回“数据拼接错误”。")
public ApiResponse<TransactionDigestResult> digest(
@Valid @RequestBody GenerateTransactionDigestRequest request) {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.digest(request.getOriginalText(), InstitutionKeySubject.from(user), return ok(service.digest(InstitutionKeySubject.from(user), user.getUsername()));
user.getUsername()), MDC.get(TraceIdFilter.MDC_KEY));
} }
@PostMapping("/steps/assemble") @PostMapping("/steps/assemble")
@Operation(summary = "组装最终交易信息标识", @Operation(summary = "组装最终交易信息标识",
description = "将固定前缀TXN_与已保存的SM3摘要拼接,结果写入数据库供后续模块读取。") description = "将固定前缀TXN_与已保存的SM3摘要拼接,结果写入数据库供后续模块读取。")
public ApiResponse<TransactionIdentifierAssemblyResult> assemble() { public ApiResponse<TransactionInformationIdentifierResult> assemble() {
JwtUser user = AuthContextHolder.get(); JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.assemble(InstitutionKeySubject.from(user), user.getUsername()), return ok(service.assemble(InstitutionKeySubject.from(user), user.getUsername()));
MDC.get(TraceIdFilter.MDC_KEY));
} }
@GetMapping @GetMapping

@ -100,11 +100,9 @@ class CurrencyGenerationVerificationControllerTest {
String userInput = "用户输入的请求原文"; String userInput = "用户输入的请求原文";
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
.contentType("application/json").content(digestStep(verificationId, userInput))) .contentType("application/json").content(digestStep(verificationId, userInput)))
.andExpect(status().isOk()) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_CALCULATED"))
.andExpect(jsonPath("$.data.digest").value(cryptography.sm3(userInput))) .andExpect(jsonPath("$.data.recomputedDigest").value(cryptography.sm3(userInput)))
.andExpect(jsonPath("$.data.status").doesNotExist()) .andExpect(jsonPath("$.data.digestMatches").value(org.hamcrest.Matchers.nullValue()));
.andExpect(jsonPath("$.data.verificationId").doesNotExist())
.andExpect(jsonPath("$.data.recomputedDigest").doesNotExist());
mvc.perform(post(BASE_PATH + "/steps/receive").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/receive").header("Authorization", authorization)
.contentType("application/json").content("{\"requestId\":" + requestId + "}")) .contentType("application/json").content("{\"requestId\":" + requestId + "}"))
@ -118,10 +116,7 @@ class CurrencyGenerationVerificationControllerTest {
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
.contentType("application/json").content(digestStep(verificationId, .contentType("application/json").content(digestStep(verificationId,
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100"))) "ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
.andExpect(status().isOk()) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_CALCULATED"));
.andExpect(jsonPath("$.data.digest").value(cryptography.sm3(
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
.andExpect(jsonPath("$.data.status").doesNotExist());
mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization)
.contentType("application/json").content(step(verificationId))) .contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_MATCHED")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_MATCHED"))
@ -130,77 +125,35 @@ class CurrencyGenerationVerificationControllerTest {
.contentType("application/json").content(step(verificationId))) .contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.totalQuota").value(50000.00)) .andExpect(jsonPath("$.data.totalQuota").value(50000.00))
.andExpect(jsonPath("$.data.usedQuota").value(1589.85)) .andExpect(jsonPath("$.data.usedQuota").value(0))
.andExpect(jsonPath("$.data.remainingQuota").value(48410.15)) .andExpect(jsonPath("$.data.remainingQuota").value(50000.00))
.andExpect(jsonPath("$.data.currentAmount").value(1589.85)) .andExpect(jsonPath("$.data.currentAmount").value(1589.85))
.andExpect(jsonPath("$.data.quotaSufficient").value(true)) .andExpect(jsonPath("$.data.quotaSufficient").value(true))
.andExpect(jsonPath("$.data.verificationId").doesNotExist()) .andExpect(jsonPath("$.data.verificationId").doesNotExist())
.andExpect(jsonPath("$.data.status").doesNotExist()); .andExpect(jsonPath("$.data.status").doesNotExist());
mvc.perform(post(BASE_PATH + "/steps/verify-quota").header("Authorization", authorization)
.contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.totalQuota").value(50000.00))
.andExpect(jsonPath("$.data.usedQuota").value(1589.85))
.andExpect(jsonPath("$.data.remainingQuota").value(48410.15))
.andExpect(jsonPath("$.data.quotaSufficient").value(true));
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
.contentType("application/json").content(digestStep(verificationId, .contentType("application/json").content(digestStep(verificationId,
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100"))) "ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(cryptography.sm3( .andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED"));
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
.andExpect(jsonPath("$.data.status").doesNotExist());
mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization)
.contentType("application/json").content(step(verificationId))) .contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED")) .andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED"))
.andExpect(jsonPath("$.data.digestMatches").value(true)); .andExpect(jsonPath("$.data.digestMatches").value(true));
mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization)
.contentType("application/json").content(confirmStep(verificationId, requestMapper.selectById(requestId).getRequestDigest(), centralPrivateKey))) .contentType("application/json").content(step(verificationId, "privateKey", centralPrivateKey)))
.andExpect(status().isOk()) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("CONFIRMED"))
.andExpect(jsonPath("$.data.signature") .andExpect(jsonPath("$.data.centralBankSignature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+"))) .andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.nullValue()));
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.verificationId").doesNotExist())
.andExpect(jsonPath("$.data.confirmationTime").doesNotExist());
CurrencyGenerationVerificationEntity confirmed = verificationMapper.selectById(verificationId);
String confirmationTime = confirmed.getConfirmationTime();
String centralBankSignature = confirmed.getCentralBankSignature();
mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization)
.contentType("application/json").content(confirmStep(verificationId,
requestMapper.selectById(requestId).getRequestDigest(), centralPrivateKey)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(centralBankSignature))
.andExpect(jsonPath("$.data.length()").value(1));
assertThat(verificationMapper.selectById(verificationId).getConfirmationTime()).isEqualTo(confirmationTime);
mvc.perform(post(BASE_PATH + "/steps/verify-quota").header("Authorization", authorization)
.contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.totalQuota").value(50000.00))
.andExpect(jsonPath("$.data.usedQuota").value(1589.85))
.andExpect(jsonPath("$.data.remainingQuota").value(48410.15))
.andExpect(jsonPath("$.data.quotaSufficient").value(true));
mvc.perform(post(BASE_PATH + "/steps/package-response").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/package-response").header("Authorization", authorization)
.contentType("application/json").content(step(verificationId))) .contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.allOf( .andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.containsString(
org.hamcrest.Matchers.containsString("\"requestId\":\"REQ_"), "\"status\":\"CONFIRMED\"")));
org.hamcrest.Matchers.containsString("\"status\":\"CONFIRMED\""),
org.hamcrest.Matchers.containsString("\"cbSignature\":\"CB_SIG_" + centralBankSignature + "\""),
org.hamcrest.Matchers.containsString("\"confirmTime\":\"" + confirmationTime + "\""))));
mvc.perform(post(BASE_PATH + "/steps/return-response").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/return-response").header("Authorization", authorization)
.contentType("application/json").content(step(verificationId))) .contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED")); .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED"));
mvc.perform(post(BASE_PATH + "/steps/receive-confirmation").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.confirmationStatus").value("已确认"))
.andExpect(jsonPath("$.data.requestId").value(
org.hamcrest.Matchers.matchesPattern("REQ_\\d{8}_\\d{3}")))
.andExpect(jsonPath("$.data.status").value("CONFIRMED"))
.andExpect(jsonPath("$.data.cbSignature").value("CB_SIG_" + centralBankSignature))
.andExpect(jsonPath("$.data.confirmTime").value(confirmationTime))
.andExpect(jsonPath("$.data.length()").value(5));
mvc.perform(get(BASE_PATH + "/current").header("Authorization", authorization)) mvc.perform(get(BASE_PATH + "/current").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.verificationId").value(verificationId)) .andExpect(status().isOk()).andExpect(jsonPath("$.data.verificationId").value(verificationId))
.andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED")); .andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED"));
@ -208,33 +161,10 @@ class CurrencyGenerationVerificationControllerTest {
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED")); .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED"));
CurrencyGenerationVerificationEntity stored = verificationMapper.selectById(verificationId); CurrencyGenerationVerificationEntity stored = verificationMapper.selectById(verificationId);
assertThat(stored.getTotalQuota()).isEqualByComparingTo("50000.00");
assertThat(stored.getUsedQuota()).isEqualByComparingTo("1589.85");
assertThat(stored.getRemainingQuota()).isEqualByComparingTo("48410.15");
assertThat(stored.getAmount()).isEqualByComparingTo("1589.85");
assertThat(stored.getConfirmationMessage()).contains("CONFIRMED", "cbSignature", "confirmTime"); assertThat(stored.getConfirmationMessage()).contains("CONFIRMED", "cbSignature", "confirmTime");
assertThat(stored.getDeleted()).isFalse(); assertThat(stored.getDeleted()).isFalse();
} }
@Test
void rejectsReceivingConfirmationUntilCentralBankReturnsResponse() throws Exception {
mvc.perform(post(BASE_PATH + "/steps/receive-confirmation").header("Authorization", authorization))
.andExpect(status().isNotFound());
mvc.perform(post(BASE_PATH + "/steps/receive").header("Authorization", authorization)
.contentType("application/json").content("{\"requestId\":" + requestId + "}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/receive-confirmation").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.confirmationStatus").value("待确认"))
.andExpect(jsonPath("$.data.requestId").value(
org.hamcrest.Matchers.matchesPattern("REQ_\\d{8}_\\d{3}")))
.andExpect(jsonPath("$.data.status").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.cbSignature").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.confirmTime").value(org.hamcrest.Matchers.nullValue()));
assertThat(scoreMapper.selectList(null)).isEmpty();
}
@Test @Test
void matchDigestWithoutCalculatedDigestReturnsReminderWithoutCountingError() throws Exception { void matchDigestWithoutCalculatedDigestReturnsReminderWithoutCountingError() throws Exception {
String received = mvc.perform(post(BASE_PATH + "/steps/receive") String received = mvc.perform(post(BASE_PATH + "/steps/receive")
@ -325,8 +255,7 @@ class CurrencyGenerationVerificationControllerTest {
mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization)
.contentType("application/json") .contentType("application/json")
.content(confirmStep(verificationId, requestMapper.selectById(requestId).getRequestDigest(), .content(step(verificationId, "privateKey", InstitutionKeyService.CENTRAL_FIRST_KEY)))
InstitutionKeyService.CENTRAL_FIRST_KEY)))
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误"))); .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
@ -344,7 +273,8 @@ class CurrencyGenerationVerificationControllerTest {
CurrencyGenerationRequestEntity deleted = quotaRequest("deleted-user", new BigDecimal("7000.00"), true); CurrencyGenerationRequestEntity deleted = quotaRequest("deleted-user", new BigDecimal("7000.00"), true);
requestMapper.insert(deleted); requestMapper.insert(deleted);
assertThat(requestMapper.sumActiveAmount()).isEqualByComparingTo("43589.85"); assertThat(requestMapper.sumActiveAmountExcluding(requestId)).isEqualByComparingTo("42000.00");
assertThat(requestMapper.sumActiveAmountExcluding(active.getId())).isEqualByComparingTo("1589.85");
} }
private CurrencyGenerationRequestEntity quotaRequest(String userId, BigDecimal amount, boolean deleted) { private CurrencyGenerationRequestEntity quotaRequest(String userId, BigDecimal amount, boolean deleted) {
@ -398,10 +328,6 @@ class CurrencyGenerationVerificationControllerTest {
private static String step(long id, String key, String value) { private static String step(long id, String key, String value) {
return "{\"verificationId\":" + id + ",\"" + key + "\":\"" + value + "\"}"; return "{\"verificationId\":" + id + ",\"" + key + "\":\"" + value + "\"}";
} }
private static String confirmStep(long id, String requestDigest, String privateKey) {
return "{\"verificationId\":" + id + ",\"requestDigest\":\"" + requestDigest
+ "\",\"privateKey\":\"" + privateKey + "\"}";
}
private static long number(String json, String path) { private static long number(String json, String path) {
return com.jayway.jsonpath.JsonPath.<Number>read(json, path).longValue(); return com.jayway.jsonpath.JsonPath.<Number>read(json, path).longValue();
} }

@ -69,7 +69,7 @@ class CurrencyGenerationVerificationDomainTest {
value.calculateDigest("A123456789012345678901234567890123456789012345678901234567890123"); value.calculateDigest("A123456789012345678901234567890123456789012345678901234567890123");
value.matchDigest(); value.matchDigest();
value.verifyQuota(new BigDecimal("500000000.00"), value.verifyQuota(new BigDecimal("500000000.00"),
new BigDecimal("500000001.00"), BigDecimal.ZERO); new BigDecimal("499999999.00"), new BigDecimal("1.00"));
assertThat(value.getStatus()).isEqualTo(CurrencyGenerationVerification.Status.DIGEST_MATCHED); assertThat(value.getStatus()).isEqualTo(CurrencyGenerationVerification.Status.DIGEST_MATCHED);
assertThat(value.getQuotaSufficient()).isFalse(); assertThat(value.getQuotaSufficient()).isFalse();
} }

@ -287,9 +287,7 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"BKCHCNBJ20101|20260730143218\"}")) .content("{\"bankCode\":\"BKCHCNBJ20101|20260730143218\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value( .andExpect(jsonPath("$.data.status").value("DIGESTED"));
org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1));
InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream() InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId())) .filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
@ -301,8 +299,9 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}")) .content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").isNotEmpty()) .andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(jsonPath("$.data.length()").value(1)); .andExpect(jsonPath("$.data.keyId").value(InstitutionKeyService.BANK_SECOND_KEY))
.andExpect(jsonPath("$.data.signature").isNotEmpty());
} }
@Test @Test
@ -501,8 +500,9 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}")) .content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").isNotEmpty()) .andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(jsonPath("$.data.length()").value(1)); .andExpect(jsonPath("$.data.signatureAlgorithm").value("SM2_WITH_SM3"))
.andExpect(jsonPath("$.data.signatureHashAlgorithm").value("SM3"));
mvc.perform(get(BASE_PATH + "/applications/commercial-bank") mvc.perform(get(BASE_PATH + "/applications/commercial-bank")
.header("Authorization", authorization)) .header("Authorization", authorization))
@ -562,8 +562,11 @@ class InstitutionIdentifierControllerTest {
mvc.perform(post(BASE_PATH + "/applications/confirmation") mvc.perform(post(BASE_PATH + "/applications/confirmation")
.header("Authorization", authorization)) .header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").isNotEmpty()) .andExpect(jsonPath("$.data.status").value("ISSUED"))
.andExpect(jsonPath("$.data.length()").value(1)); .andExpect(jsonPath("$.data.signingKeyId")
.value(InstitutionKeyService.CENTRAL_FIRST_KEY))
.andExpect(jsonPath("$.data.signatureAlgorithm").value("SM2_WITH_SM3"))
.andExpect(jsonPath("$.data.institutionIdentifier").value(org.hamcrest.Matchers.startsWith("ORG_")));
mvc.perform(get(BASE_PATH + "/applications/final-identifier") mvc.perform(get(BASE_PATH + "/applications/final-identifier")
.header("Authorization", authorization)) .header("Authorization", authorization))
@ -616,20 +619,29 @@ class InstitutionIdentifierControllerTest {
String issuedOrg = jdbcTemplate.queryForObject( String issuedOrg = jdbcTemplate.queryForObject(
"SELECT institution_identifier FROM institution_identifier_application WHERE id = ?", "SELECT institution_identifier FROM institution_identifier_application WHERE id = ?",
String.class, applicationId); String.class, applicationId);
mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest") String digestResponse = mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
.header("Authorization", authorization) .header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\":1589.85,\"deliveryNodeCode\":\"SYS_DC_001\"," .content("{\"amount\":1589.85,\"deliveryNodeCode\":\"SYS_DC_001\","
+ "\"institutionIdentifier\":\"" + issuedOrg + "\"," + "\"institutionIdentifier\":\"" + issuedOrg + "\","
+ "\"requestTimestamp\":\"20260730153218\"}")) + "\"requestTimestamp\":\"20260730153218\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest") .andExpect(jsonPath("$.data.status").value("DIGESTED"))
.andExpect(jsonPath("$.data.requestDigest")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.identifierApplicationId").value(applicationId))
.andExpect(jsonPath("$.data.institutionIdentifier")
.value(org.hamcrest.Matchers.startsWith("ORG_")))
.andExpect(jsonPath("$.data.fullInstitutionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))) .value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1)); .andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.deliveryNodeCode").value("SYS_DC_001"))
.andExpect(jsonPath("$.data.requestTimestamp").value("20260730153218"))
.andExpect(jsonPath("$.data.requestOriginalText")
.value(issuedOrg + "|1589.85|SYS_DC_001|20260730153218"))
.andReturn().getResponse().getContentAsString();
Number currencyRequestId = jdbcTemplate.queryForObject( Number currencyRequestId = JsonPath.read(digestResponse, "$.data.requestId");
"SELECT id FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE",
Number.class, issuedOrg);
java.util.Map<String, Object> storedRequest = jdbcTemplate.queryForMap( java.util.Map<String, Object> storedRequest = jdbcTemplate.queryForMap(
"SELECT institution_identifier, amount, delivery_node_code, request_timestamp, " "SELECT institution_identifier, amount, delivery_node_code, request_timestamp, "
+ "request_original_text, request_digest FROM currency_generation_request WHERE id = ?", + "request_original_text, request_digest FROM currency_generation_request WHERE id = ?",
@ -656,20 +668,22 @@ class InstitutionIdentifierControllerTest {
.andExpect(jsonPath("$.data.requestDigest") .andExpect(jsonPath("$.data.requestDigest")
.value(storedRequest.get("request_digest"))); .value(storedRequest.get("request_digest")));
mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest") String secondDigestResponse = mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
.header("Authorization", authorization) .header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\":2000.00,\"deliveryNodeCode\":\"SYS_DC_002\"," .content("{\"amount\":2000.00,\"deliveryNodeCode\":\"SYS_DC_002\","
+ "\"institutionIdentifier\":\"" + issuedOrg + "\"," + "\"institutionIdentifier\":\"" + issuedOrg + "\","
+ "\"requestTimestamp\":\"20260730160000\"}")) + "\"requestTimestamp\":\"20260730160000\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest") .andExpect(jsonPath("$.data.requestId").value(currencyRequestId.longValue()))
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))) .andExpect(jsonPath("$.data.amount").value(2000.00))
.andExpect(jsonPath("$.data.length()").value(1)); .andExpect(jsonPath("$.data.deliveryNodeCode").value("SYS_DC_002"))
Number repeatedRequestId = jdbcTemplate.queryForObject( .andExpect(jsonPath("$.data.requestTimestamp").value("20260730160000"))
"SELECT id FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE", .andExpect(jsonPath("$.data.requestOriginalText")
Number.class, issuedOrg); .value(issuedOrg + "|2000.00|SYS_DC_002|20260730160000"))
assertThat(repeatedRequestId.longValue()).isEqualTo(currencyRequestId.longValue()); .andReturn().getResponse().getContentAsString();
assertThat(JsonPath.<Number>read(secondDigestResponse, "$.data.requestId").longValue())
.isEqualTo(currencyRequestId.longValue());
Integer requestCount = jdbcTemplate.queryForObject( Integer requestCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE", "SELECT COUNT(*) FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE",
Integer.class, issuedOrg); Integer.class, issuedOrg);
@ -693,26 +707,10 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}")) .content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature") .andExpect(jsonPath("$.data.status").value("SIGNED"))
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+"))) .andExpect(jsonPath("$.data.signingKeyId").value(InstitutionKeyService.BANK_SECOND_KEY))
.andExpect(jsonPath("$.data.length()").value(1)); .andExpect(jsonPath("$.data.bankSignature")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")));
mvc.perform(post(BASE_PATH + "/currency-requests/steps/package")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("\"requestId\":\"REQ_20260730_"),
org.hamcrest.Matchers.containsString("\"orgId\":\"" + issuedOrg + "\""),
org.hamcrest.Matchers.containsString("\"amount\":\"2000.00\""),
org.hamcrest.Matchers.containsString("\"systemId\":\"SYS_DC_002\""),
org.hamcrest.Matchers.containsString("\"timestamp\":\"20260730160000\""),
org.hamcrest.Matchers.containsString("\"digest\":"),
org.hamcrest.Matchers.containsString("\"signature\":"))))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.not(
org.hamcrest.Matchers.containsString("\"originalText\""))))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.not(
org.hamcrest.Matchers.containsString("\"keyId\""))));
jdbcTemplate.update("UPDATE currency_generation_request SET deleted = TRUE WHERE id = ?", jdbcTemplate.update("UPDATE currency_generation_request SET deleted = TRUE WHERE id = ?",
currencyRequestId.longValue()); currencyRequestId.longValue());
@ -846,8 +844,9 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}")) .content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").isNotEmpty()) .andExpect(jsonPath("$.data.status").value("ISSUED"))
.andExpect(jsonPath("$.data.length()").value(1)); .andExpect(jsonPath("$.data.signingKeyId")
.value(InstitutionKeyService.CENTRAL_FIRST_KEY));
} }
private long advanceToVerified(String bankCode) throws Exception { private long advanceToVerified(String bankCode) throws Exception {
@ -900,19 +899,17 @@ class InstitutionIdentifierControllerTest {
} }
private long digest(String bankCode) throws Exception { private long digest(String bankCode) throws Exception {
mvc.perform(post(BASE_PATH + "/steps/digest") String response = mvc.perform(post(BASE_PATH + "/steps/digest")
.header("Authorization", authorization) .header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"" + bankCode + "|20260730143218\"}")) .content("{\"bankCode\":\"" + bankCode + "|20260730143218\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.applicationId").isNumber())
.andExpect(jsonPath("$.data.status").value("DIGESTED"))
.andExpect(jsonPath("$.data.algorithm").value("SM3"))
.andExpect(jsonPath("$.data.digest").isNotEmpty()) .andExpect(jsonPath("$.data.digest").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1)) .andReturn().getResponse().getContentAsString();
.andExpect(jsonPath("$.data.applicationId").doesNotExist()) Number applicationId = JsonPath.read(response, "$.data.applicationId");
.andExpect(jsonPath("$.data.status").doesNotExist()) return applicationId.longValue();
.andExpect(jsonPath("$.data.algorithm").doesNotExist());
return applicationMapper.selectList(null).stream()
.max(java.util.Comparator.comparing(InstitutionIdentifierApplicationEntity::getCreatedAt)
.thenComparing(InstitutionIdentifierApplicationEntity::getId))
.orElseThrow(() -> new AssertionError("机构标识申请记录未创建")).getId();
} }
} }

@ -54,13 +54,11 @@ class TransactionInformationIdentifierControllerTest {
"/api/v1/institution-identifiers/transaction-information-identifiers"; "/api/v1/institution-identifiers/transaction-information-identifiers";
private static final String QUOTA_BASE_PATH = private static final String QUOTA_BASE_PATH =
"/api/v1/institution-identifiers/quota-requests"; "/api/v1/institution-identifiers/quota-requests";
private static final String CENTRAL_QUOTA_REQUEST_BASE_PATH =
"/api/v1/institution-identifiers/central-bank/quota-requests";
private static final String VERIFICATION_BASE_PATH = private static final String VERIFICATION_BASE_PATH =
"/api/v1/institution-identifiers/quota-verifications"; "/api/v1/institution-identifiers/quota-verifications";
private static final String CONTROL_SIGNATURE_BASE_PATH = private static final String CONTROL_SIGNATURE_BASE_PATH =
"/api/v1/institution-identifiers/control-system-signatures"; "/api/v1/institution-identifiers/control-system-signatures";
private static final String COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH = private static final String BANK_CONTROL_SIGNATURE_BASE_PATH =
"/api/v1/institution-identifiers/commercial-bank/control-system-signatures"; "/api/v1/institution-identifiers/commercial-bank/control-system-signatures";
private static final String QUOTA_CONTROL_BASE_PATH = private static final String QUOTA_CONTROL_BASE_PATH =
"/api/v1/institution-identifiers/quota-control-bits"; "/api/v1/institution-identifiers/quota-control-bits";
@ -113,63 +111,52 @@ class TransactionInformationIdentifierControllerTest {
} }
@Test @Test
void rejectsDigestWhenStepThreeHasNotReturnedResponseAndPrepareRouteIsRemoved() throws Exception { void rejectsModuleFourWhenStepThreeHasNotReturnedResponse() throws Exception {
mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization)) mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isNotFound());
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤三尚未完成确认报文返回"))); "步骤三尚未完成确认报文返回")));
assertThat(transactionMapper.selectList(null)).isEmpty(); assertThat(transactionMapper.selectList(null)).isEmpty();
} }
@Test
void recordsEveryConcatenationErrorAndReturnsFixedMessage() throws Exception {
insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85[ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("数据拼接错误"));
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"999.00|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("数据拼接错误"));
assertThat(transactionMapper.selectList(null)).isEmpty();
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
.eq(StuModuleScoreDetailsEntity::getSerialNumber, 3));
assertThat(score).isNotNull();
assertThat(score.getCompletionStatus()).isEqualTo("2");
}
@Test @Test
void flowsCompletedPreviousDataAndPersistsEveryModuleFourStep() throws Exception { void flowsCompletedPreviousDataAndPersistsEveryModuleFourStep() throws Exception {
PrecedingData preceding = insertCompletedPrecedingData(); PrecedingData preceding = insertCompletedPrecedingData();
String originalText = "1589.85|ORG_3A4B5C6D7E8F|20260730153330"; mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization) .andExpect(status().isOk())
.contentType("application/json") .andExpect(jsonPath("$.data.status").value("PREPARED"))
.content("{\"originalText\":\"" + originalText + "\"}")) .andExpect(jsonPath("$.data.verificationId").value(preceding.verificationId))
.andExpect(jsonPath("$.data.requestId").value(preceding.requestId))
.andExpect(jsonPath("$.data.identifierApplicationId").value(preceding.applicationId))
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.transactionTimestamp")
.value(org.hamcrest.Matchers.matchesPattern("\\d{14}")))
.andExpect(jsonPath("$.data.originalText")
.value(org.hamcrest.Matchers.matchesPattern(
"1589\\.85\\|ORG_3A4B5C6D7E8F\\|\\d{14}")))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.transactionIdentifier")
.value(org.hamcrest.Matchers.nullValue()));
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"请先使用SM3生成摘要")));
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("DIGESTED"))
.andExpect(jsonPath("$.data.digestAlgorithm").value("SM3"))
.andExpect(jsonPath("$.data.digest") .andExpect(jsonPath("$.data.digest")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))) .value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.originalText").doesNotExist())
.andExpect(jsonPath("$.data.transactionTimestamp").doesNotExist())
.andExpect(jsonPath("$.data.digestAlgorithm").doesNotExist());
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization)) mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("ASSEMBLED"))
.andExpect(jsonPath("$.data.transactionIdentifier") .andExpect(jsonPath("$.data.transactionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}"))) .value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")));
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.digest").doesNotExist());
mvc.perform(get(BASE_PATH).header("Authorization", authorization)) mvc.perform(get(BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
@ -185,15 +172,15 @@ class TransactionInformationIdentifierControllerTest {
assertThat(persisted.getIdentifierApplicationId()).isEqualTo(preceding.applicationId); assertThat(persisted.getIdentifierApplicationId()).isEqualTo(preceding.applicationId);
assertThat(persisted.getAmount()).isEqualByComparingTo("1589.85"); assertThat(persisted.getAmount()).isEqualByComparingTo("1589.85");
assertThat(persisted.getInstitutionIdentifier()).isEqualTo("ORG_3A4B5C6D7E8F"); assertThat(persisted.getInstitutionIdentifier()).isEqualTo("ORG_3A4B5C6D7E8F");
assertThat(persisted.getOriginalText()).isEqualTo(originalText); assertThat(persisted.getOriginalText()).isEqualTo("1589.85|ORG_3A4B5C6D7E8F|"
assertThat(persisted.getTransactionTimestamp()).isEqualTo("20260730153330"); + persisted.getTransactionTimestamp());
assertThat(persisted.getDigest()).isEqualTo(cryptography.sm3(originalText)); assertThat(persisted.getDigest()).matches("[0-9A-F]{64}");
assertThat(persisted.getTransactionIdentifier()).isEqualTo("TXN_" + persisted.getDigest()); assertThat(persisted.getTransactionIdentifier()).isEqualTo("TXN_" + persisted.getDigest());
assertThat(persisted.getStatus()).isEqualTo("ASSEMBLED"); assertThat(persisted.getStatus()).isEqualTo("ASSEMBLED");
assertThat(transactionLogMapper.selectList(null)) assertThat(transactionLogMapper.selectList(null))
.extracting(TransactionInformationIdentifierOperationLogEntity::getOperation) .extracting(TransactionInformationIdentifierOperationLogEntity::getOperation)
.containsExactly("DIGEST", "ASSEMBLE"); .containsExactly("PREPARE", "DIGEST", "ASSEMBLE");
StuModuleScoreDetailsEntity score = scoreMapper.selectOne( StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>() new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
@ -205,11 +192,11 @@ class TransactionInformationIdentifierControllerTest {
} }
@Test @Test
void flowsCompleteStepFiveFromSourceDataThroughCentralBankReceive() throws Exception { void flowsModuleFourOutputIntoCompleteQuotaRequestWorkflow() throws Exception {
insertCompletedPrecedingData(); insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization) mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.contentType("application/json") .andExpect(status().isOk());
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}")) mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization))
.andExpect(status().isOk()); .andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization)) mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk()); .andExpect(status().isOk());
@ -220,240 +207,126 @@ class TransactionInformationIdentifierControllerTest {
.andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F")) .andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.transactionIdentifier") .andExpect(jsonPath("$.data.transactionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}"))) .value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(3)) .andExpect(jsonPath("$.data.quotaRequestId").doesNotExist());
.andExpect(jsonPath("$.data.transactionIdentifierExists").doesNotExist()) TransactionInformationIdentifierEntity sourceDataRecord = transactionMapper.selectList(null).get(0);
.andExpect(jsonPath("$.data.conclusion").doesNotExist()); assertThat(sourceDataRecord.getStatus()).isEqualTo("ASSEMBLED");
assertThat(sourceDataRecord.getQuotaRequestId()).isNull();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/prepare").header("Authorization", authorization)) mvc.perform(post(QUOTA_BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isNotFound());
String quotaOriginalText = "1589.85|ORG_3A4B5C6D7E8F|"
+ transactionMapper.selectList(null).get(0).getTransactionIdentifier();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + quotaOriginalText + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(
org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.quotaRequestId").doesNotExist())
.andExpect(jsonPath("$.data.quotaOriginalText").doesNotExist());
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
String bankPrivateKey = keyService.commercialBankKey(subject, "module-four-tester").getPrivateKey();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(
org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.quotaSigningKeyId").doesNotExist())
.andExpect(jsonPath("$.data.quotaSignature").doesNotExist());
long stepFiveRecordId = transactionMapper.selectList(null).get(0).getId();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + quotaOriginalText + "\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))); .andExpect(jsonPath("$.data.status").value("QUOTA_PREPARED"))
assertThat(transactionMapper.selectList(null)).hasSize(1); .andExpect(jsonPath("$.data.amount").value(1589.85))
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepFiveRecordId); .andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
assertThat(transactionMapper.selectList(null).get(0).getQuotaSignature()).isNull(); .andExpect(jsonPath("$.data.transactionIdentifier")
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign").header("Authorization", authorization) .value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.quotaRequestId")
.value(org.hamcrest.Matchers.matchesPattern("QR_\\d{8}_\\d{3}")))
.andExpect(jsonPath("$.data.quotaOriginalText")
.value(org.hamcrest.Matchers.matchesPattern(
"1589\\.85\\|ORG_3A4B5C6D7E8F\\|TXN_[0-9A-F]{64}")));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("QUOTA_DIGESTED"))
.andExpect(jsonPath("$.data.quotaDigest")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResult bankKey =
keyService.commercialBankKey(
new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID), "module-four-tester");
String bankPublicKey = bankKey.getPublicKey();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType("application/json") .contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\"}")) .content("{\"privateKey\":\"WRONG_BANK_PRIVATE_KEY\"}"))
.andExpect(status().isOk()); .andExpect(status().isBadRequest())
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign").header("Authorization", authorization) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType("application/json") .contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\"}")) .content("{\"privateKey\":\"" + bankKey.getPrivateKey() + "\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+"))); .andExpect(jsonPath("$.data.status").value("QUOTA_SIGNED"))
assertThat(transactionMapper.selectList(null)).hasSize(1); .andExpect(jsonPath("$.data.quotaSigningKeyId").value("BANK_SECOND_SM2_KEY"))
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepFiveRecordId); .andExpect(jsonPath("$.data.quotaSignature")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/package").header("Authorization", authorization)) mvc.perform(post(QUOTA_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6)) .andExpect(jsonPath("$.data.status").value("QUOTA_PACKAGED"))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty()) .andExpect(jsonPath("$.data.quotaRequestMessage")
.andExpect(jsonPath("$.data.amount").value(1589.85)) .value(org.hamcrest.Matchers.containsString("\"quotaRequestId\"")))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F")) .andExpect(jsonPath("$.data.quotaRequestMessage")
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_"))) .value(org.hamcrest.Matchers.containsString("\"txnId\"")));
.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());
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)) mvc.perform(post(QUOTA_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6)) .andExpect(jsonPath("$.data.status").value("QUOTA_RECEIVED"))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty()) .andExpect(jsonPath("$.data.centralBankReceiveStatus").value("RECEIVED"));
.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());
mvc.perform(get(QUOTA_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6));
mvc.perform(get(CENTRAL_QUOTA_REQUEST_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(2))
.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.length()").value(7))
.andExpect(jsonPath("$.data.requestMessage").doesNotExist());
mvc.perform(post(CENTRAL_QUOTA_REQUEST_BASE_PATH + "/steps/receive")
.header("Authorization", authorization))
.andExpect(status().isNotFound());
TransactionInformationIdentifierEntity persisted = transactionMapper.selectList(null).get(0); mvc.perform(post(VERIFICATION_BASE_PATH + "/verify")
assertThat(persisted.getStatus()).isEqualTo("QUOTA_RECEIVED");
assertThat(persisted.getQuotaDigest()).isEqualTo(cryptography.sm3(persisted.getQuotaOriginalText()));
assertThat(cryptography.verify(keyService.commercialBankKey(subject, "module-four-tester").getPublicKey(),
persisted.getQuotaDigest(), persisted.getQuotaSignature())).isTrue();
assertThat(persisted.getQuotaRequestMessage()).contains(persisted.getQuotaRequestId(),
persisted.getQuotaDigest(), persisted.getQuotaSignature());
assertThat(transactionLogMapper.selectList(null))
.extracting(TransactionInformationIdentifierOperationLogEntity::getOperation)
.containsExactly("DIGEST", "ASSEMBLE", "QUOTA_DIGEST", "QUOTA_SIGN",
"QUOTA_DIGEST", "QUOTA_SIGN", "QUOTA_SIGN",
"QUOTA_PACKAGE", "QUOTA_SEND");
}
@Test
void flowsStepSixDataThroughCentralBankSignatureAndCommercialBankReceive() throws Exception {
flowsCompleteStepFiveFromSourceDataThroughCentralBankReceive();
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
String bankPublicKey = keyService.commercialBankKey(subject, "module-four-tester").getPublicKey();
String centralPrivateKey = keyService.centralBankKey(subject, "module-four-tester").getPrivateKey();
String verificationOriginalText = transactionMapper.selectList(null).get(0).getQuotaOriginalText();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/concatenate-original-text")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.originalText").value(verificationOriginalText))
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization) .header("Authorization", authorization)
.contentType("application/json") .contentType("application/json")
.content("{\"originalText\":\"" + verificationOriginalText + "\"}")) .content("{\"publicKey\":\"WRONG_BANK_PUBLIC_KEY\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h1").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.h2").doesNotExist());
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"WRONG_BANK_SECOND_PUBLIC_KEY\"}"))
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误"))); .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest") mvc.perform(post(VERIFICATION_BASE_PATH + "/verify")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h2").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.h1").doesNotExist());
long stepSixRecordId = transactionMapper.selectList(null).get(0).getId();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization) .header("Authorization", authorization)
.contentType("application/json") .contentType("application/json")
.content("{\"originalText\":\"" + verificationOriginalText + "\"}"))
.andExpect(status().isOk());
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepSixRecordId);
assertThat(transactionMapper.selectList(null).get(0).getQuotaSignatureDigest()).isNull();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}")) .content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(status().isOk());
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepSixRecordId);
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/compare-digests")
.header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED"))
.andExpect(jsonPath("$.data.signatureValid").value(true))
.andExpect(jsonPath("$.data.h1EqualsH2").value(true)) .andExpect(jsonPath("$.data.h1EqualsH2").value(true))
.andExpect(jsonPath("$.data.h1").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.h2").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.conclusion").value("一致,验签通过")); .andExpect(jsonPath("$.data.conclusion").value("一致,验签通过"));
TransactionInformationIdentifierEntity verified = transactionMapper.selectList(null).get(0); TransactionInformationIdentifierEntity persisted = transactionMapper.selectList(null).get(0);
assertThat(verified.getStatus()).isEqualTo("QUOTA_VERIFIED"); assertThat(persisted.getQuotaOriginalText()).isEqualTo(persisted.getAmount().toPlainString() + "|"
assertThat(verified.getQuotaRecomputedDigest()).isEqualTo( + persisted.getInstitutionIdentifier() + "|" + persisted.getTransactionIdentifier());
cryptography.sm3(verified.getQuotaOriginalText())); assertThat(persisted.getQuotaDigest()).matches("[0-9A-F]{64}");
assertThat(verified.getQuotaSignatureDigest()).isEqualTo(verified.getQuotaDigest()); assertThat(persisted.getQuotaSigningKeyId()).isEqualTo(InstitutionKeyService.BANK_SECOND_KEY);
assertThat(verified.getQuotaSignatureValid()).isTrue(); assertThat(persisted.getQuotaRequestMessage()).contains(persisted.getQuotaRequestId(),
assertThat(verified.getQuotaDigestMatches()).isTrue(); persisted.getTransactionIdentifier(), persisted.getQuotaDigest(), persisted.getQuotaSignature());
assertThat(persisted.getQuotaRecomputedDigest()).isEqualTo(persisted.getQuotaDigest());
assertThat(persisted.getQuotaSignatureDigest()).isEqualTo(persisted.getQuotaDigest());
assertThat(persisted.getQuotaSignatureValid()).isTrue();
assertThat(persisted.getQuotaDigestMatches()).isTrue();
assertThat(persisted.getStatus()).isEqualTo("QUOTA_VERIFIED");
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/concatenate-original-text") assertThat(transactionLogMapper.selectList(null))
.header("Authorization", authorization)) .extracting(TransactionInformationIdentifierOperationLogEntity::getOperation)
.andExpect(status().isOk()) .containsExactly("PREPARE", "DIGEST", "ASSEMBLE", "QUOTA_PREPARE", "QUOTA_DIGEST",
.andExpect(jsonPath("$.data.originalText").value(verificationOriginalText)); "QUOTA_SIGN", "QUOTA_PACKAGE", "QUOTA_SEND", "QUOTA_RECEIVE", "QUOTA_VERIFY");
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash") }
.header("Authorization", authorization)
.contentType("application/json") @Test
.content("{\"originalText\":\"" + verificationOriginalText + "\"}")) void flowsStepSixDataThroughCentralBankSignatureAndCommercialBankReceive() throws Exception {
.andExpect(status().isOk()); flowsModuleFourOutputIntoCompleteQuotaRequestWorkflow();
assertThat(transactionMapper.selectList(null).get(0).getQuotaSignatureDigest()).isNull(); InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest") String centralPrivateKey = keyService.centralBankKey(subject, "module-four-tester").getPrivateKey();
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/compare-digests")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h1EqualsH2").value(true));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/compare-digests")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h1EqualsH2").value(true));
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepSixRecordId);
verified = transactionMapper.selectList(null).get(0);
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/prepare") String firstPrepare = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/prepare")
.header("Authorization", authorization)) .header("Authorization", authorization))
.andExpect(status().isNotFound());
String controlOriginalText = verified.getAmount().setScale(2).toPlainString() + "|"
+ verified.getInstitutionIdentifier() + "|" + verified.getTransactionIdentifier();
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"999.00|"
+ verified.getInstitutionIdentifier() + "|" + verified.getTransactionIdentifier()
+ "\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("与当前用户前序数据不一致"),
org.hamcrest.Matchers.containsString("第2次错误"))));
assertThat(controlSignatureMapper.selectList(null)).isEmpty();
String firstDigest = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + controlOriginalText + "\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(cryptography.sm3(controlOriginalText))) .andExpect(jsonPath("$.data.status").value("PREPARED"))
.andExpect(jsonPath("$.data.length()").value(1)) .andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.status").doesNotExist()) .andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.originalText").doesNotExist()) .andExpect(jsonPath("$.data.controlId").value(org.hamcrest.Matchers.matchesPattern(
.andExpect(jsonPath("$.data.controlId").doesNotExist()) "CTRL_\\d{8}_\\d{3}")))
.andReturn().getResponse().getContentAsString(); .andReturn().getResponse().getContentAsString();
String repeatedDigest = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest") String repeatedPrepare = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/prepare")
.header("Authorization", authorization) .header("Authorization", authorization))
.contentType("application/json")
.content("{\"originalText\":\"" + controlOriginalText + "\"}"))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
assertThat(com.jayway.jsonpath.JsonPath.<String>read(repeatedDigest, "$.data.digest")) assertThat(com.jayway.jsonpath.JsonPath.<Number>read(repeatedPrepare, "$.data.id").longValue())
.isEqualTo(com.jayway.jsonpath.JsonPath.<String>read(firstDigest, "$.data.digest")); .isEqualTo(com.jayway.jsonpath.JsonPath.<Number>read(firstPrepare, "$.data.id").longValue());
assertThat(controlSignatureMapper.selectList(null)).hasSize(1); assertThat(controlSignatureMapper.selectList(null)).hasSize(1);
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("DIGESTED"))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/sign") mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/sign")
.header("Authorization", authorization) .header("Authorization", authorization)
.contentType("application/json") .contentType("application/json")
@ -465,47 +338,32 @@ class TransactionInformationIdentifierControllerTest {
.contentType("application/json") .contentType("application/json")
.content("{\"privateKey\":\"" + centralPrivateKey + "\"}")) .content("{\"privateKey\":\"" + centralPrivateKey + "\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+"))) .andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(jsonPath("$.data.length()").value(1)); .andExpect(jsonPath("$.data.signingKeyId").value(InstitutionKeyService.CENTRAL_FIRST_KEY))
.andExpect(jsonPath("$.data.signature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")));
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/package").header("Authorization", authorization)) mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PACKAGED")) .andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.responseMessage").value(org.hamcrest.Matchers.containsString( .andExpect(jsonPath("$.data.responseMessage").value(org.hamcrest.Matchers.containsString(
"\"cbCtrlSignature\""))); "\"cbCtrlSignature\"")));
mvc.perform(get(COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.ctrlId").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.cbCtrlSignature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(6));
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/send").header("Authorization", authorization)) mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SENT")); .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SENT"));
mvc.perform(get(COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH).header("Authorization", authorization)) mvc.perform(post(BANK_CONTROL_SIGNATURE_BASE_PATH + "/steps/receive")
.andExpect(status().isOk()) .header("Authorization", authorization))
.andExpect(jsonPath("$.data.status").value(2)) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RECEIVED"));
.andExpect(jsonPath("$.data.ctrlId").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.cbCtrlSignature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(6));
ControlSystemSignatureEntity persisted = controlSignatureMapper.selectList(null).get(0); ControlSystemSignatureEntity persisted = controlSignatureMapper.selectList(null).get(0);
TransactionInformationIdentifierEntity transaction = transactionMapper.selectList(null).get(0); TransactionInformationIdentifierEntity transaction = transactionMapper.selectList(null).get(0);
assertThat(persisted.getTransactionIdentifierId()).isEqualTo(transaction.getId()); assertThat(persisted.getTransactionIdentifierId()).isEqualTo(transaction.getId());
assertThat(persisted.getOriginalText()).isEqualTo( assertThat(persisted.getOriginalText()).isEqualTo(persisted.getControlId() + "|"
transaction.getAmount().setScale(2).toPlainString() + "|" + transaction.getAmount().setScale(2).toPlainString() + "|"
+ transaction.getInstitutionIdentifier() + "|" + transaction.getTransactionIdentifier()); + transaction.getInstitutionIdentifier() + "|" + transaction.getTransactionIdentifier());
assertThat(persisted.getDigest()).isEqualTo(cryptography.sm3(persisted.getOriginalText()));
assertThat(persisted.getControlSignature()).isEqualTo(persisted.getSignature()); assertThat(persisted.getControlSignature()).isEqualTo(persisted.getSignature());
assertThat(cryptography.verify(keyService.centralBankKey( assertThat(cryptography.verify(keyService.centralBankKey(
new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID), "module-four-tester").getPublicKey(), new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID), "module-four-tester").getPublicKey(),
persisted.getDigest(), persisted.getControlSignature())).isTrue(); persisted.getDigest(), persisted.getControlSignature())).isTrue();
assertThat(persisted.getResponseMessage()).contains(persisted.getControlId(), persisted.getControlSignature()); assertThat(persisted.getResponseMessage()).contains(persisted.getControlId(), persisted.getControlSignature());
assertThat(persisted.getStatus()).isEqualTo("SENT"); assertThat(persisted.getStatus()).isEqualTo("RECEIVED");
StuModuleScoreDetailsEntity score = scoreMapper.selectOne( StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>() new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID) .eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
@ -518,155 +376,64 @@ class TransactionInformationIdentifierControllerTest {
flowsStepSixDataThroughCentralBankSignatureAndCommercialBankReceive(); flowsStepSixDataThroughCentralBankSignatureAndCommercialBankReceive();
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID); InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
String bankPrivateKey = keyService.commercialBankKey(subject, "module-four-tester").getPrivateKey(); String bankPrivateKey = keyService.commercialBankKey(subject, "module-four-tester").getPrivateKey();
mvc.perform(get(CENTRAL_QUOTA_CONTROL_BASE_PATH).header("Authorization", authorization)) String transactionIdentifier = transactionMapper.selectList(null).get(0).getTransactionIdentifier();
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1)) mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.andExpect(jsonPath("$.data.quotaId").value(org.hamcrest.Matchers.nullValue())) .header("Authorization", authorization).contentType("application/json")
.andExpect(jsonPath("$.data.length()").value(6)); .content("{\"amount\":1590.85,\"institutionIdentifier\":\"ORG_3A4B5C6D7E8F\"," +
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/static") "\"transactionIdentifier\":\"" + transactionIdentifier + "\"}"))
.header("Authorization", authorization)) .andExpect(status().isBadRequest())
.andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第4次错误")));
.andExpect(jsonPath("$.message").value("当前没有步骤八额度控制位记录")); mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/dynamic") .header("Authorization", authorization).contentType("application/json")
.header("Authorization", authorization)) .content("{\"amount\":1589.85,\"institutionIdentifier\":\"ORG_3A4B5C6D7E8F\"," +
.andExpect(status().isNotFound()) "\"transactionIdentifier\":\"" + transactionIdentifier + "\"}"))
.andExpect(jsonPath("$.message").value("当前没有步骤八额度控制位记录"));
String staticSegmentResponse = mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("STATIC_READY"))
.andExpect(jsonPath("$.data.staticSegment").value( .andExpect(jsonPath("$.data.staticSegment").value(
org.hamcrest.Matchers.matchesPattern("STATIC_[0-9A-F]{64}"))) org.hamcrest.Matchers.matchesPattern("STATIC_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1)) .andExpect(jsonPath("$.data.receiveStatus").value(1));
.andExpect(jsonPath("$.data.status").doesNotExist()) mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/dynamic-segment")
.andReturn().getResponse().getContentAsString(); .header("Authorization", authorization).contentType("application/json")
String staticSegment = com.jayway.jsonpath.JsonPath.read(staticSegmentResponse, "$.data.staticSegment"); .content("{\"randomValue\":\"7G8H9I0J\",\"timestamp\":\"20260730153600\"}"))
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/static")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.staticSegment").value(staticSegment))
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/dynamic")
.header("Authorization", authorization))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("当前尚未生成步骤八动态段"));
String firstDynamicSegmentResponse = mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/dynamic-segment")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.dynamicSegment").value(
org.hamcrest.Matchers.matchesPattern("DYN_[A-Z0-9]{8}_\\d{14}")))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andReturn().getResponse().getContentAsString();
String firstDynamicSegment = com.jayway.jsonpath.JsonPath.read(
firstDynamicSegmentResponse, "$.data.dynamicSegment");
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/dynamic")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.dynamicSegment").value(firstDynamicSegment))
.andExpect(jsonPath("$.data.length()").value(1));
long stepEightRecordId = quotaControlBitMapper.selectList(null).get(0).getId();
String secondDynamicSegmentResponse = mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/dynamic-segment")
.header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.dynamicSegment").value( .andExpect(jsonPath("$.data.dynamicSegment").value("DYN_7G8H9I0J_20260730153600"));
org.hamcrest.Matchers.matchesPattern("DYN_[A-Z0-9]{8}_\\d{14}")))
.andReturn().getResponse().getContentAsString();
String secondDynamicSegment = com.jayway.jsonpath.JsonPath.read(
secondDynamicSegmentResponse, "$.data.dynamicSegment");
assertThat(secondDynamicSegment).isNotEqualTo(firstDynamicSegment);
assertThat(quotaControlBitMapper.selectList(null)).hasSize(1);
assertThat(quotaControlBitMapper.selectList(null).get(0).getId()).isEqualTo(stepEightRecordId);
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/read-central-bank-signature") mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/read-central-bank-signature")
.header("Authorization", authorization)) .header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.centralBankSignatureSegment").value( .andExpect(jsonPath("$.data.centralBankSignatureSegment").value(
org.hamcrest.Matchers.startsWith("CB_CTRL_"))) org.hamcrest.Matchers.startsWith("CB_CTRL_")));
.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)) mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.completeControlBit").value(org.hamcrest.Matchers.allOf( .andExpect(jsonPath("$.data.completeControlBit").value(
org.hamcrest.Matchers.containsString("|DYN_"), org.hamcrest.Matchers.containsString("|DYN_7G8H9I0J_20260730153600|CB_CTRL_")));
org.hamcrest.Matchers.containsString("|CB_CTRL_"))));
String completeControlBit = quotaControlBitMapper.selectList(null).get(0).getCompleteControlBit();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign") mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json") .header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\"" .content("{\"privateKey\":\"" + bankPrivateKey + "\",\"hashAlgorithm\":\"SM3\"}"))
+ completeControlBit + "\",\"hashAlgorithm\":\"SM3\"}"))
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第4次错误"))); .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第5次错误")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign") mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json") .header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\"" .content("{\"privateKey\":\"WRONG_BANK_PRIVATE_KEY\",\"hashAlgorithm\":\"SM2\"}"))
+ completeControlBit + "_TAMPERED\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("完整额度控制位与步骤八拼接结果不一致"),
org.hamcrest.Matchers.containsString("第5次错误"))));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"WRONG_BANK_PRIVATE_KEY\",\"completeControlBit\":\""
+ completeControlBit + "\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第6次错误"))); .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第6次错误")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign") mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json") .header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\"" .content("{\"privateKey\":\"" + bankPrivateKey + "\",\"hashAlgorithm\":\"SM2\"}"))
+ completeControlBit + "\",\"hashAlgorithm\":\"SM2\"}")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(status().isOk()) .andExpect(jsonPath("$.data.bankSignature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")));
.andExpect(jsonPath("$.data.signature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/package").header("Authorization", authorization)) mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.containsString("\"quotaId\""))); .andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.containsString("\"quotaId\"")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.staticSegment").value(
org.hamcrest.Matchers.matchesPattern("STATIC_[0-9A-F]{64}")));
QuotaControlBitEntity resetByStatic = quotaControlBitMapper.selectList(null).get(0);
assertThat(resetByStatic.getId()).isEqualTo(stepEightRecordId);
assertThat(resetByStatic.getStatus()).isEqualTo("STATIC_READY");
assertThat(resetByStatic.getDynamicSegment()).isNull();
assertThat(resetByStatic.getCentralBankSignatureSegment()).isNull();
assertThat(resetByStatic.getCompleteControlBit()).isNull();
assertThat(resetByStatic.getBankSignature()).isNull();
assertThat(resetByStatic.getRequestMessage()).isNull();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/dynamic-segment")
.header("Authorization", authorization)).andExpect(status().isOk());
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/read-central-bank-signature")
.header("Authorization", authorization)).andExpect(status().isOk());
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/assemble")
.header("Authorization", authorization)).andExpect(status().isOk());
String regeneratedCompleteControlBit = quotaControlBitMapper.selectList(null).get(0).getCompleteControlBit();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\""
+ regeneratedCompleteControlBit + "\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isOk());
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)) mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SENT"))
.andExpect(jsonPath("$.data.receiveStatus").value(1));
mvc.perform(get(CENTRAL_QUOTA_CONTROL_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.receiveStatus").value(1));
mvc.perform(post(CENTRAL_QUOTA_CONTROL_BASE_PATH + "/steps/receive").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RECEIVED")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RECEIVED"))
.andExpect(jsonPath("$.data.receiveStatus").value(2)); .andExpect(jsonPath("$.data.receiveStatus").value(2));
mvc.perform(get(CENTRAL_QUOTA_CONTROL_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(2))
.andExpect(jsonPath("$.data.quotaId").value(org.hamcrest.Matchers.startsWith("QC_")))
.andExpect(jsonPath("$.data.staticSegment").value(
org.hamcrest.Matchers.matchesPattern("STATIC_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.dynamicSegment").value(
org.hamcrest.Matchers.matchesPattern("DYN_[A-Z0-9]{8}_\\d{14}")))
.andExpect(jsonPath("$.data.cbSignatureSegment").value(
org.hamcrest.Matchers.startsWith("CB_CTRL_")))
.andExpect(jsonPath("$.data.bankSignature").value(
org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(6));
mvc.perform(post(CENTRAL_QUOTA_CONTROL_BASE_PATH + "/steps/receive")
.header("Authorization", authorization))
.andExpect(status().isNotFound());
QuotaControlBitEntity stored = quotaControlBitMapper.selectList(null).get(0); QuotaControlBitEntity stored = quotaControlBitMapper.selectList(null).get(0);
assertThat(stored.getStatus()).isEqualTo("RECEIVED"); assertThat(stored.getStatus()).isEqualTo("RECEIVED");
@ -729,18 +496,10 @@ class TransactionInformationIdentifierControllerTest {
assertThat(currencies.stream().map(StandardCurrencyEntity::getDenomination) assertThat(currencies.stream().map(StandardCurrencyEntity::getDenomination)
.reduce(BigDecimal.ZERO, BigDecimal::add)).isEqualByComparingTo("1589.85"); .reduce(BigDecimal.ZERO, BigDecimal::add)).isEqualByComparingTo("1589.85");
assertThat(currencies.stream().map(StandardCurrencyEntity::getCurrencyId).distinct().count()).isEqualTo(27); assertThat(currencies.stream().map(StandardCurrencyEntity::getCurrencyId).distinct().count()).isEqualTo(27);
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)) mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/generate").header("Authorization", authorization))
.andExpect(status().isOk()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.data.status").value("GENERATED")) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("当前状态:GENERATED")));
.andExpect(jsonPath("$.data.currencies.length()").value(27));
assertThat(standardCurrencyBatchMapper.selectList(null)).hasSize(1);
assertThat(standardCurrencyBatchMapper.selectList(null).get(0).getId()).isEqualTo(batchId);
assertThat(standardCurrencyMapper.selectList(null)).hasSize(27); assertThat(standardCurrencyMapper.selectList(null)).hasSize(27);
assertThat(standardCurrencyMapper.selectList(null).stream().map(StandardCurrencyEntity::getId)
.collect(java.util.stream.Collectors.toList())).containsExactlyElementsOf(currencyIds);
} }
@Test @Test
@ -766,36 +525,18 @@ class TransactionInformationIdentifierControllerTest {
assertThat(quotaControlBitMapper.selectList(null)).isEmpty(); assertThat(quotaControlBitMapper.selectList(null)).isEmpty();
} }
@Test
void rejectsIncorrectStepFiveOriginalTextAndRecordsError() throws Exception {
insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"999.00|ORG_3A4B5C6D7E8F|TXN_"
+ "7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("数据拼接错误"));
assertThat(transactionMapper.selectList(null).get(0).getStatus()).isEqualTo("ASSEMBLED");
}
@Test @Test
void rejectsQuotaVerificationUntilStepFiveIsReceived() throws Exception { void rejectsQuotaVerificationUntilStepFiveIsReceived() throws Exception {
insertCompletedPrecedingData(); insertCompletedPrecedingData();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash") String publicKey = keyService.commercialBankKey(
new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID), "module-four-tester").getPublicKey();
mvc.perform(post(VERIFICATION_BASE_PATH + "/verify")
.header("Authorization", authorization) .header("Authorization", authorization)
.contentType("application/json") .contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|TXN_" .content("{\"publicKey\":\"" + publicKey + "\"}"))
+ "7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F\"}")) .andExpect(status().isBadRequest())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"当前没有额度请求记录"))); "步骤五额度请求尚未发送并由央行接收")));
} }
@Test @Test
@ -804,36 +545,24 @@ class TransactionInformationIdentifierControllerTest {
mvc.perform(get(QUOTA_BASE_PATH + "/source-data").header("Authorization", authorization)) mvc.perform(get(QUOTA_BASE_PATH + "/source-data").header("Authorization", authorization))
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤四尚未完成交易信息标识"))); "步骤四尚未完成交易信息标识组装")));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization) mvc.perform(post(QUOTA_BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.contentType("application/json") .andExpect(status().isBadRequest())
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|TXN_" .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
+ "7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F\"}")) "步骤四尚未完成交易信息标识组装")));
.andExpect(status().isBadRequest());
assertThat(transactionMapper.selectList(null)).isEmpty(); assertThat(transactionMapper.selectList(null)).isEmpty();
} }
@Test @Test
void repeatedCorrectDigestUpdatesExistingRecordWithoutAddingData() throws Exception { void duplicatePreparationReturnsLatestRecordWithoutAddingData() throws Exception {
insertCompletedPrecedingData(); insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization) String first = mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.contentType("application/json") .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}")) String second = mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isOk()); .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
long id = transactionMapper.selectList(null).get(0).getId(); assertThat(com.jayway.jsonpath.JsonPath.<Number>read(second, "$.data.id").longValue())
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization)) .isEqualTo(com.jayway.jsonpath.JsonPath.<Number>read(first, "$.data.id").longValue());
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730154444\"}"))
.andExpect(status().isOk());
assertThat(transactionMapper.selectList(null)).hasSize(1); assertThat(transactionMapper.selectList(null)).hasSize(1);
TransactionInformationIdentifierEntity updated = transactionMapper.selectList(null).get(0);
assertThat(updated.getId()).isEqualTo(id);
assertThat(updated.getTransactionTimestamp()).isEqualTo("20260730154444");
assertThat(updated.getStatus()).isEqualTo("DIGESTED");
assertThat(updated.getTransactionIdentifier()).isNull();
} }
private PrecedingData insertCompletedPrecedingData() { private PrecedingData insertCompletedPrecedingData() {

Loading…
Cancel
Save