Compare commits

...

3 Commits

@ -0,0 +1,37 @@
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; }
}

@ -0,0 +1,45 @@
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; }
}

@ -0,0 +1,17 @@
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; }
}

@ -0,0 +1,38 @@
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; }
}

@ -0,0 +1,17 @@
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,6 +13,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@ -28,25 +29,37 @@ public class ControlSystemSignatureService {
@Resource private InstitutionTrainingErrorRecorder errorRecorder;
@Transactional
public ControlSystemSignatureResult prepare(InstitutionKeySubject subject, String operator) {
public ControlSystemDigestResult digest(String originalText, InstitutionKeySubject subject, String operator) {
TransactionInformationIdentifierEntity transaction = requireQuotaVerified(subject);
String normalizedOriginalText = validateOriginalText(originalText, transaction, subject);
ControlSystemSignatureEntity current = latest(subject);
if (current != null) {
if (current.getTransactionIdentifierId().equals(transaction.getId())) {
return new ControlSystemSignatureResult(current);
}
if (!current.getTransactionIdentifierId().equals(transaction.getId())) {
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();
value.setTransactionIdentifierId(transaction.getId());
value.setControlId("CTRL_" + LocalDateTime.now().format(DATE) + "_" + String.format("%03d", transaction.getId() % 1000));
value.setControlId("CTRL_" + LocalDateTime.now().format(DATE) + "_"
+ String.format("%03d", transaction.getId() % 1000));
value.setAmount(transaction.getAmount());
value.setInstitutionIdentifier(transaction.getInstitutionIdentifier());
value.setTransactionIdentifier(transaction.getTransactionIdentifier());
value.setOriginalText(value.getControlId() + "|" + transaction.getAmount().setScale(2).toPlainString() + "|"
+ transaction.getInstitutionIdentifier() + "|" + transaction.getTransactionIdentifier());
value.setStatus("PREPARED");
value.setOriginalText(normalizedOriginalText);
value.setDigest(cryptography.sm3(normalizedOriginalText));
value.setStatus("DIGESTED");
value.setUserId(subject.getUserId());
value.setSchoolId(subject.getSchoolId());
value.setClassId(subject.getClassId());
@ -56,44 +69,38 @@ public class ControlSystemSignatureService {
value.setUpdatedBy(operator);
value.setDeleted(false);
mapper.insert(value);
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));
return new ControlSystemDigestResult(value.getDigest());
}
@Transactional
public ControlSystemSignatureResult sign(String privateKey, InstitutionKeySubject subject, String operator) {
ControlSystemSignatureEntity value = requireStatus(subject, "DIGESTED", "请先计算控制系统签名摘要");
ControlSystemSignatureEntity value = requireLatest(subject);
requireResult(value.getDigest(), "请先计算控制系统签名摘要");
String signature = keyService.signCentralBank(subject, privateKey, value.getDigest());
if (signature == null) {
int errorSequence = errorRecorder.recordScoreError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"中央银行第一私钥不正确,本次实训第" + errorSequence + "次错误");
}
update(value, "DIGESTED", "SIGNED", operator,
update(value, value.getStatus(), "SIGNED", operator,
new LambdaUpdateWrapper<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getSigningKeyId, InstitutionKeyService.CENTRAL_FIRST_KEY)
.set(ControlSystemSignatureEntity::getSignature, signature)
.set(ControlSystemSignatureEntity::getControlSignature, signature));
.set(ControlSystemSignatureEntity::getControlSignature, signature)
.set(ControlSystemSignatureEntity::getResponseMessage, null));
return new ControlSystemSignatureResult(requireLatest(subject));
}
@Transactional
public ControlSystemSignatureResult packageResponse(InstitutionKeySubject subject, String operator) {
ControlSystemSignatureEntity value = requireStatus(subject, "SIGNED", "请先完成中央银行控制系统签名");
ControlSystemSignatureEntity value = requireLatest(subject);
requireResult(value.getControlSignature(), "请先完成中央银行控制系统签名");
String message = "{\"ctrlId\":\"" + value.getControlId() + "\",\"amount\":\""
+ value.getAmount().setScale(2).toPlainString() + "\",\"orgId\":\""
+ value.getInstitutionIdentifier() + "\",\"txnId\":\""
+ value.getTransactionIdentifier() + "\",\"cbCtrlSignature\":\""
+ value.getControlSignature() + "\"}";
update(value, "SIGNED", "PACKAGED", operator,
update(value, value.getStatus(), "PACKAGED", operator,
new LambdaUpdateWrapper<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getResponseMessage, message));
return new ControlSystemSignatureResult(requireLatest(subject));
@ -101,22 +108,47 @@ public class ControlSystemSignatureService {
@Transactional
public ControlSystemSignatureResult send(InstitutionKeySubject subject, String operator) {
ControlSystemSignatureEntity value = requireStatus(subject, "PACKAGED", "请先打包控制系统签名反馈报文");
update(value, "PACKAGED", "SENT", operator, new LambdaUpdateWrapper<ControlSystemSignatureEntity>());
return new ControlSystemSignatureResult(requireLatest(subject));
}
@Transactional
public ControlSystemSignatureResult receive(InstitutionKeySubject subject, String operator) {
ControlSystemSignatureEntity value = requireStatus(subject, "SENT", "中央银行尚未发送控制系统签名报文");
update(value, "SENT", "RECEIVED", operator, new LambdaUpdateWrapper<ControlSystemSignatureEntity>());
ControlSystemSignatureEntity value = requireLatest(subject);
requireResult(value.getResponseMessage(), "请先打包控制系统签名反馈报文");
update(value, value.getStatus(), "SENT", operator, new LambdaUpdateWrapper<ControlSystemSignatureEntity>());
scoreService.markStepCompleted(subject.getUserId(), 7);
return new ControlSystemSignatureResult(requireLatest(subject));
}
@Transactional(readOnly = true)
public ControlSystemSignatureResult detail(InstitutionKeySubject subject) {
return new ControlSystemSignatureResult(requireLatest(subject));
public CommercialBankControlSystemSignatureResult commercialBankMessage(InstitutionKeySubject subject) {
return new CommercialBankControlSystemSignatureResult(latest(subject));
}
private String validateOriginalText(String originalText, TransactionInformationIdentifierEntity transaction,
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) {
int errorSequence = errorRecorder.recordScoreError(subject.getUserId());
return new BusinessException(ErrorCode.VALIDATION_ERROR,
message + ",本次实训第" + errorSequence + "次错误");
}
private TransactionInformationIdentifierEntity requireQuotaVerified(InstitutionKeySubject subject) {
@ -132,6 +164,12 @@ public class ControlSystemSignatureService {
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) {
ControlSystemSignatureEntity value = requireLatest(subject);
if (!status.equals(value.getStatus())) {

@ -0,0 +1,32 @@
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; }
}

@ -0,0 +1,19 @@
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;
}
}

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

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

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

@ -5,45 +5,16 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@ApiModel(description = "接口返回数据")
@Schema(description = "接口返回数据")
@ApiModel(description = "机构标识申请SM3摘要结果")
@Schema(description = "机构标识申请SM3摘要结果")
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摘要值")
private final 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;
public InstitutionDigestResult(String 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() {
return digest;
}

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

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

@ -0,0 +1,17 @@
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;
}
}

@ -0,0 +1,17 @@
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;
}
}

@ -0,0 +1,17 @@
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;
}
}

@ -0,0 +1,38 @@
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; }
}

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

@ -0,0 +1,24 @@
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;
}
}

@ -0,0 +1,17 @@
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;
}
}

@ -0,0 +1,17 @@
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;
}
}

@ -0,0 +1,17 @@
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;
}
}

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

@ -0,0 +1,18 @@
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; }
}

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

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

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

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

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

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

@ -0,0 +1,38 @@
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; }
}

@ -0,0 +1,21 @@
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;
}
}

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

@ -0,0 +1,21 @@
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;
}
}

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

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

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

@ -1,8 +1,11 @@
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.ControlSystemSignatureService;
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.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser;
@ -11,7 +14,6 @@ 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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -26,24 +28,24 @@ import javax.validation.Valid;
public class ControlSystemSignatureController {
@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")
@Operation(summary = "计算控制系统签名摘要")
public ApiResponse<ControlSystemSignatureResult> digest() {
return execute((subject, operator) -> service.digest(subject, operator));
@Operation(summary = "对拼接请求原文进行SM3哈希运算",
description = "前端提交页面输入框完整原文,格式为金额|机构标识|交易信息标识。后端按当前JWT主体校验三项前序数据,生成控制编号并对输入原文原样计算SM3摘要。")
public ApiResponse<ControlSystemDigestResult> digest(
@Valid @RequestBody ControlSystemDigestRequest request) {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.digest(request.getOriginalText(), InstitutionKeySubject.from(user),
user.getUsername()), traceId());
}
@PostMapping("/steps/sign")
@Operation(summary = "使用中央银行第一私钥生成签名")
public ApiResponse<ControlSystemSignatureResult> sign(
public ApiResponse<SignatureResult> sign(
@Valid @RequestBody ControlSystemSignRequest request) {
return execute((subject, operator) -> service.sign(request.getPrivateKey(), subject, operator));
JwtUser user = AuthContextHolder.get();
ControlSystemSignatureResult result = service.sign(request.getPrivateKey(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new SignatureResult(result.getSignature()), traceId());
}
@PostMapping("/steps/package")
@ -58,12 +60,6 @@ public class ControlSystemSignatureController {
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) {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(executor.execute(InstitutionKeySubject.from(user), user.getUsername()), traceId());

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

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

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

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

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

@ -1,8 +1,13 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest;
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.QuotaVerificationSignatureDigestResult;
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.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser;
@ -27,14 +32,41 @@ public class QuotaVerificationController {
@Resource
private TransactionInformationIdentifierService service;
@PostMapping("/verify")
@Operation(summary = "使用商业银行第二公钥验签并比对H1和H2",
description = "仅允许读取步骤五已由央行接收的报文;H1为请求原文重算SM3,H2为第二公钥验签恢复并确认的摘要。")
public ApiResponse<QuotaVerificationResult> verify(
@PostMapping("/steps/concatenate-original-text")
@Operation(summary = "提取并拼接额度请求原文",
description = "从央行已接收的步骤五额度请求报文提取amount、orgId、txnId,并返回金额|机构标识|交易信息标识;接口只读,不计算哈希、不推进状态。")
public ApiResponse<QuotaVerificationOriginalTextResult> originalText() {
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) {
JwtUser user = AuthContextHolder.get();
return ok(service.verifyQuotaRequest(request.getPublicKey(), InstitutionKeySubject.from(user),
user.getUsername()));
return ApiResponse.success(service.recoverQuotaH2(request.getPublicKey(),
InstitutionKeySubject.from(user), user.getUsername()), traceId());
}
@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
@ -44,6 +76,10 @@ public class QuotaVerificationController {
}
private ApiResponse<QuotaVerificationResult> ok(QuotaVerificationResult result) {
return ApiResponse.success(result, MDC.get(TraceIdFilter.MDC_KEY));
return ApiResponse.success(result, traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

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

@ -100,9 +100,11 @@ class CurrencyGenerationVerificationControllerTest {
String userInput = "用户输入的请求原文";
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
.contentType("application/json").content(digestStep(verificationId, userInput)))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_CALCULATED"))
.andExpect(jsonPath("$.data.recomputedDigest").value(cryptography.sm3(userInput)))
.andExpect(jsonPath("$.data.digestMatches").value(org.hamcrest.Matchers.nullValue()));
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(cryptography.sm3(userInput)))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.verificationId").doesNotExist())
.andExpect(jsonPath("$.data.recomputedDigest").doesNotExist());
mvc.perform(post(BASE_PATH + "/steps/receive").header("Authorization", authorization)
.contentType("application/json").content("{\"requestId\":" + requestId + "}"))
@ -116,7 +118,10 @@ class CurrencyGenerationVerificationControllerTest {
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
.contentType("application/json").content(digestStep(verificationId,
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_CALCULATED"));
.andExpect(status().isOk())
.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)
.contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_MATCHED"))
@ -125,35 +130,77 @@ class CurrencyGenerationVerificationControllerTest {
.contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.totalQuota").value(50000.00))
.andExpect(jsonPath("$.data.usedQuota").value(0))
.andExpect(jsonPath("$.data.remainingQuota").value(50000.00))
.andExpect(jsonPath("$.data.usedQuota").value(1589.85))
.andExpect(jsonPath("$.data.remainingQuota").value(48410.15))
.andExpect(jsonPath("$.data.currentAmount").value(1589.85))
.andExpect(jsonPath("$.data.quotaSufficient").value(true))
.andExpect(jsonPath("$.data.verificationId").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)
.contentType("application/json").content(digestStep(verificationId,
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED"));
.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)
.contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED"))
.andExpect(jsonPath("$.data.digestMatches").value(true));
mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization)
.contentType("application/json").content(step(verificationId, "privateKey", centralPrivateKey)))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("CONFIRMED"))
.andExpect(jsonPath("$.data.centralBankSignature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.nullValue()));
.contentType("application/json").content(confirmStep(verificationId, requestMapper.selectById(requestId).getRequestDigest(), centralPrivateKey)))
.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.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)
.contentType("application/json").content(step(verificationId)))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.containsString(
"\"status\":\"CONFIRMED\"")));
.andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("\"requestId\":\"REQ_"),
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)
.contentType("application/json").content(step(verificationId)))
.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))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.verificationId").value(verificationId))
.andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED"));
@ -161,10 +208,33 @@ class CurrencyGenerationVerificationControllerTest {
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED"));
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.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
void matchDigestWithoutCalculatedDigestReturnsReminderWithoutCountingError() throws Exception {
String received = mvc.perform(post(BASE_PATH + "/steps/receive")
@ -255,7 +325,8 @@ class CurrencyGenerationVerificationControllerTest {
mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization)
.contentType("application/json")
.content(step(verificationId, "privateKey", InstitutionKeyService.CENTRAL_FIRST_KEY)))
.content(confirmStep(verificationId, requestMapper.selectById(requestId).getRequestDigest(),
InstitutionKeyService.CENTRAL_FIRST_KEY)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
@ -273,8 +344,7 @@ class CurrencyGenerationVerificationControllerTest {
CurrencyGenerationRequestEntity deleted = quotaRequest("deleted-user", new BigDecimal("7000.00"), true);
requestMapper.insert(deleted);
assertThat(requestMapper.sumActiveAmountExcluding(requestId)).isEqualByComparingTo("42000.00");
assertThat(requestMapper.sumActiveAmountExcluding(active.getId())).isEqualByComparingTo("1589.85");
assertThat(requestMapper.sumActiveAmount()).isEqualByComparingTo("43589.85");
}
private CurrencyGenerationRequestEntity quotaRequest(String userId, BigDecimal amount, boolean deleted) {
@ -328,6 +398,10 @@ class CurrencyGenerationVerificationControllerTest {
private static String step(long id, String key, String 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) {
return com.jayway.jsonpath.JsonPath.<Number>read(json, path).longValue();
}

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

@ -287,7 +287,9 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"BKCHCNBJ20101|20260730143218\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("DIGESTED"));
.andExpect(jsonPath("$.data.digest").value(
org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1));
InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
@ -299,9 +301,8 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(jsonPath("$.data.keyId").value(InstitutionKeyService.BANK_SECOND_KEY))
.andExpect(jsonPath("$.data.signature").isNotEmpty());
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1));
}
@Test
@ -500,9 +501,8 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(jsonPath("$.data.signatureAlgorithm").value("SM2_WITH_SM3"))
.andExpect(jsonPath("$.data.signatureHashAlgorithm").value("SM3"));
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(get(BASE_PATH + "/applications/commercial-bank")
.header("Authorization", authorization))
@ -562,11 +562,8 @@ class InstitutionIdentifierControllerTest {
mvc.perform(post(BASE_PATH + "/applications/confirmation")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("ISSUED"))
.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_")));
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(get(BASE_PATH + "/applications/final-identifier")
.header("Authorization", authorization))
@ -619,29 +616,20 @@ class InstitutionIdentifierControllerTest {
String issuedOrg = jdbcTemplate.queryForObject(
"SELECT institution_identifier FROM institution_identifier_application WHERE id = ?",
String.class, applicationId);
String digestResponse = mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\":1589.85,\"deliveryNodeCode\":\"SYS_DC_001\","
+ "\"institutionIdentifier\":\"" + issuedOrg + "\","
+ "\"requestTimestamp\":\"20260730153218\"}"))
.andExpect(status().isOk())
.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")
.andExpect(jsonPath("$.data.digest")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.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();
.andExpect(jsonPath("$.data.length()").value(1));
Number currencyRequestId = JsonPath.read(digestResponse, "$.data.requestId");
Number currencyRequestId = jdbcTemplate.queryForObject(
"SELECT id FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE",
Number.class, issuedOrg);
java.util.Map<String, Object> storedRequest = jdbcTemplate.queryForMap(
"SELECT institution_identifier, amount, delivery_node_code, request_timestamp, "
+ "request_original_text, request_digest FROM currency_generation_request WHERE id = ?",
@ -668,22 +656,20 @@ class InstitutionIdentifierControllerTest {
.andExpect(jsonPath("$.data.requestDigest")
.value(storedRequest.get("request_digest")));
String secondDigestResponse = mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\":2000.00,\"deliveryNodeCode\":\"SYS_DC_002\","
+ "\"institutionIdentifier\":\"" + issuedOrg + "\","
+ "\"requestTimestamp\":\"20260730160000\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.requestId").value(currencyRequestId.longValue()))
.andExpect(jsonPath("$.data.amount").value(2000.00))
.andExpect(jsonPath("$.data.deliveryNodeCode").value("SYS_DC_002"))
.andExpect(jsonPath("$.data.requestTimestamp").value("20260730160000"))
.andExpect(jsonPath("$.data.requestOriginalText")
.value(issuedOrg + "|2000.00|SYS_DC_002|20260730160000"))
.andReturn().getResponse().getContentAsString();
assertThat(JsonPath.<Number>read(secondDigestResponse, "$.data.requestId").longValue())
.isEqualTo(currencyRequestId.longValue());
.andExpect(jsonPath("$.data.digest")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1));
Number repeatedRequestId = jdbcTemplate.queryForObject(
"SELECT id FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE",
Number.class, issuedOrg);
assertThat(repeatedRequestId.longValue()).isEqualTo(currencyRequestId.longValue());
Integer requestCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE",
Integer.class, issuedOrg);
@ -707,10 +693,26 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(jsonPath("$.data.signingKeyId").value(InstitutionKeyService.BANK_SECOND_KEY))
.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(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 = ?",
currencyRequestId.longValue());
@ -844,9 +846,8 @@ class InstitutionIdentifierControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("ISSUED"))
.andExpect(jsonPath("$.data.signingKeyId")
.value(InstitutionKeyService.CENTRAL_FIRST_KEY));
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1));
}
private long advanceToVerified(String bankCode) throws Exception {
@ -899,17 +900,19 @@ class InstitutionIdentifierControllerTest {
}
private long digest(String bankCode) throws Exception {
String response = mvc.perform(post(BASE_PATH + "/steps/digest")
mvc.perform(post(BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"" + bankCode + "|20260730143218\"}"))
.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())
.andReturn().getResponse().getContentAsString();
Number applicationId = JsonPath.read(response, "$.data.applicationId");
return applicationId.longValue();
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.applicationId").doesNotExist())
.andExpect(jsonPath("$.data.status").doesNotExist())
.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,11 +54,13 @@ class TransactionInformationIdentifierControllerTest {
"/api/v1/institution-identifiers/transaction-information-identifiers";
private static final String QUOTA_BASE_PATH =
"/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 =
"/api/v1/institution-identifiers/quota-verifications";
private static final String CONTROL_SIGNATURE_BASE_PATH =
"/api/v1/institution-identifiers/control-system-signatures";
private static final String BANK_CONTROL_SIGNATURE_BASE_PATH =
private static final String COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH =
"/api/v1/institution-identifiers/commercial-bank/control-system-signatures";
private static final String QUOTA_CONTROL_BASE_PATH =
"/api/v1/institution-identifiers/quota-control-bits";
@ -111,8 +113,12 @@ class TransactionInformationIdentifierControllerTest {
}
@Test
void rejectsModuleFourWhenStepThreeHasNotReturnedResponse() throws Exception {
void rejectsDigestWhenStepThreeHasNotReturnedResponseAndPrepareRouteIsRemoved() throws Exception {
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(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤三尚未完成确认报文返回")));
@ -120,43 +126,50 @@ class TransactionInformationIdentifierControllerTest {
}
@Test
void flowsCompletedPreviousDataAndPersistsEveryModuleFourStep() throws Exception {
PrecedingData preceding = insertCompletedPrecedingData();
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("数据拼接错误"));
mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PREPARED"))
.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()));
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");
}
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"请先使用SM3生成摘要")));
@Test
void flowsCompletedPreviousDataAndPersistsEveryModuleFourStep() throws Exception {
PrecedingData preceding = insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization))
String originalText = "1589.85|ORG_3A4B5C6D7E8F|20260730153330";
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + originalText + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("DIGESTED"))
.andExpect(jsonPath("$.data.digestAlgorithm").value("SM3"))
.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))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("ASSEMBLED"))
.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))
.andExpect(status().isOk())
@ -172,15 +185,15 @@ class TransactionInformationIdentifierControllerTest {
assertThat(persisted.getIdentifierApplicationId()).isEqualTo(preceding.applicationId);
assertThat(persisted.getAmount()).isEqualByComparingTo("1589.85");
assertThat(persisted.getInstitutionIdentifier()).isEqualTo("ORG_3A4B5C6D7E8F");
assertThat(persisted.getOriginalText()).isEqualTo("1589.85|ORG_3A4B5C6D7E8F|"
+ persisted.getTransactionTimestamp());
assertThat(persisted.getDigest()).matches("[0-9A-F]{64}");
assertThat(persisted.getOriginalText()).isEqualTo(originalText);
assertThat(persisted.getTransactionTimestamp()).isEqualTo("20260730153330");
assertThat(persisted.getDigest()).isEqualTo(cryptography.sm3(originalText));
assertThat(persisted.getTransactionIdentifier()).isEqualTo("TXN_" + persisted.getDigest());
assertThat(persisted.getStatus()).isEqualTo("ASSEMBLED");
assertThat(transactionLogMapper.selectList(null))
.extracting(TransactionInformationIdentifierOperationLogEntity::getOperation)
.containsExactly("PREPARE", "DIGEST", "ASSEMBLE");
.containsExactly("DIGEST", "ASSEMBLE");
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
@ -192,11 +205,11 @@ class TransactionInformationIdentifierControllerTest {
}
@Test
void flowsModuleFourOutputIntoCompleteQuotaRequestWorkflow() throws Exception {
void flowsCompleteStepFiveFromSourceDataThroughCentralBankReceive() throws Exception {
insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization))
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());
@ -207,126 +220,240 @@ class TransactionInformationIdentifierControllerTest {
.andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.transactionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.quotaRequestId").doesNotExist());
TransactionInformationIdentifierEntity sourceDataRecord = transactionMapper.selectList(null).get(0);
assertThat(sourceDataRecord.getStatus()).isEqualTo("ASSEMBLED");
assertThat(sourceDataRecord.getQuotaRequestId()).isNull();
.andExpect(jsonPath("$.data.length()").value(3))
.andExpect(jsonPath("$.data.transactionIdentifierExists").doesNotExist())
.andExpect(jsonPath("$.data.conclusion").doesNotExist());
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.status").value("QUOTA_PREPARED"))
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.transactionIdentifier")
.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)
.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\":\"WRONG_BANK_PRIVATE_KEY\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.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("{\"privateKey\":\"" + bankKey.getPrivateKey() + "\"}"))
.content("{\"originalText\":\"" + quotaOriginalText + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("QUOTA_SIGNED"))
.andExpect(jsonPath("$.data.quotaSigningKeyId").value("BANK_SECOND_SM2_KEY"))
.andExpect(jsonPath("$.data.quotaSignature")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")));
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepFiveRecordId);
assertThat(transactionMapper.selectList(null).get(0).getQuotaSignature()).isNull();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\"}"))
.andExpect(status().isOk());
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]+")));
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepFiveRecordId);
mvc.perform(post(QUOTA_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("QUOTA_PACKAGED"))
.andExpect(jsonPath("$.data.quotaRequestMessage")
.value(org.hamcrest.Matchers.containsString("\"quotaRequestId\"")))
.andExpect(jsonPath("$.data.quotaRequestMessage")
.value(org.hamcrest.Matchers.containsString("\"txnId\"")));
.andExpect(jsonPath("$.data.length()").value(6))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.quotaRequestMessage").doesNotExist());
mvc.perform(get(CENTRAL_QUOTA_REQUEST_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.length()").value(7));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("QUOTA_RECEIVED"))
.andExpect(jsonPath("$.data.centralBankReceiveStatus").value("RECEIVED"));
mvc.perform(post(VERIFICATION_BASE_PATH + "/verify")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"publicKey\":\"WRONG_BANK_PUBLIC_KEY\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
mvc.perform(post(VERIFICATION_BASE_PATH + "/verify")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(jsonPath("$.data.length()").value(6))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.centralBankReceiveStatus").doesNotExist());
mvc.perform(get(QUOTA_BASE_PATH).header("Authorization", authorization))
.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.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.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);
assertThat(persisted.getQuotaOriginalText()).isEqualTo(persisted.getAmount().toPlainString() + "|"
+ persisted.getInstitutionIdentifier() + "|" + persisted.getTransactionIdentifier());
assertThat(persisted.getQuotaDigest()).matches("[0-9A-F]{64}");
assertThat(persisted.getQuotaSigningKeyId()).isEqualTo(InstitutionKeyService.BANK_SECOND_KEY);
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.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");
persisted.getQuotaDigest(), persisted.getQuotaSignature());
assertThat(transactionLogMapper.selectList(null))
.extracting(TransactionInformationIdentifierOperationLogEntity::getOperation)
.containsExactly("PREPARE", "DIGEST", "ASSEMBLE", "QUOTA_PREPARE", "QUOTA_DIGEST",
"QUOTA_SIGN", "QUOTA_PACKAGE", "QUOTA_SEND", "QUOTA_RECEIVE", "QUOTA_VERIFY");
.containsExactly("DIGEST", "ASSEMBLE", "QUOTA_DIGEST", "QUOTA_SIGN",
"QUOTA_DIGEST", "QUOTA_SIGN", "QUOTA_SIGN",
"QUOTA_PACKAGE", "QUOTA_SEND");
}
@Test
void flowsStepSixDataThroughCentralBankSignatureAndCommercialBankReceive() throws Exception {
flowsModuleFourOutputIntoCompleteQuotaRequestWorkflow();
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 firstPrepare = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/prepare")
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.status").value("PREPARED"))
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.controlId").value(org.hamcrest.Matchers.matchesPattern(
"CTRL_\\d{8}_\\d{3}")))
.andReturn().getResponse().getContentAsString();
String repeatedPrepare = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/prepare")
.andExpect(jsonPath("$.data.originalText").value(verificationOriginalText))
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + verificationOriginalText + "\"}"))
.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(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.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)
.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 + "\"}"))
.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()).andReturn().getResponse().getContentAsString();
assertThat(com.jayway.jsonpath.JsonPath.<Number>read(repeatedPrepare, "$.data.id").longValue())
.isEqualTo(com.jayway.jsonpath.JsonPath.<Number>read(firstPrepare, "$.data.id").longValue());
assertThat(controlSignatureMapper.selectList(null)).hasSize(1);
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h1EqualsH2").value(true))
.andExpect(jsonPath("$.data.conclusion").value("一致,验签通过"));
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest").header("Authorization", authorization))
TransactionInformationIdentifierEntity verified = transactionMapper.selectList(null).get(0);
assertThat(verified.getStatus()).isEqualTo("QUOTA_VERIFIED");
assertThat(verified.getQuotaRecomputedDigest()).isEqualTo(
cryptography.sm3(verified.getQuotaOriginalText()));
assertThat(verified.getQuotaSignatureDigest()).isEqualTo(verified.getQuotaDigest());
assertThat(verified.getQuotaSignatureValid()).isTrue();
assertThat(verified.getQuotaDigestMatches()).isTrue();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/concatenate-original-text")
.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}")));
.andExpect(jsonPath("$.data.originalText").value(verificationOriginalText));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + verificationOriginalText + "\"}"))
.andExpect(status().isOk());
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 + "\"}"))
.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")
.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(jsonPath("$.data.digest").value(cryptography.sm3(controlOriginalText)))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.originalText").doesNotExist())
.andExpect(jsonPath("$.data.controlId").doesNotExist())
.andReturn().getResponse().getContentAsString();
String repeatedDigest = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + controlOriginalText + "\"}"))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
assertThat(com.jayway.jsonpath.JsonPath.<String>read(repeatedDigest, "$.data.digest"))
.isEqualTo(com.jayway.jsonpath.JsonPath.<String>read(firstDigest, "$.data.digest"));
assertThat(controlSignatureMapper.selectList(null)).hasSize(1);
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType("application/json")
@ -338,32 +465,47 @@ class TransactionInformationIdentifierControllerTest {
.contentType("application/json")
.content("{\"privateKey\":\"" + centralPrivateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(jsonPath("$.data.signingKeyId").value(InstitutionKeyService.CENTRAL_FIRST_KEY))
.andExpect(jsonPath("$.data.signature").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(CONTROL_SIGNATURE_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.responseMessage").value(org.hamcrest.Matchers.containsString(
"\"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))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SENT"));
mvc.perform(post(BANK_CONTROL_SIGNATURE_BASE_PATH + "/steps/receive")
.header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RECEIVED"));
mvc.perform(get(COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(2))
.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);
TransactionInformationIdentifierEntity transaction = transactionMapper.selectList(null).get(0);
assertThat(persisted.getTransactionIdentifierId()).isEqualTo(transaction.getId());
assertThat(persisted.getOriginalText()).isEqualTo(persisted.getControlId() + "|"
+ transaction.getAmount().setScale(2).toPlainString() + "|"
assertThat(persisted.getOriginalText()).isEqualTo(
transaction.getAmount().setScale(2).toPlainString() + "|"
+ transaction.getInstitutionIdentifier() + "|" + transaction.getTransactionIdentifier());
assertThat(persisted.getDigest()).isEqualTo(cryptography.sm3(persisted.getOriginalText()));
assertThat(persisted.getControlSignature()).isEqualTo(persisted.getSignature());
assertThat(cryptography.verify(keyService.centralBankKey(
new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID), "module-four-tester").getPublicKey(),
persisted.getDigest(), persisted.getControlSignature())).isTrue();
assertThat(persisted.getResponseMessage()).contains(persisted.getControlId(), persisted.getControlSignature());
assertThat(persisted.getStatus()).isEqualTo("RECEIVED");
assertThat(persisted.getStatus()).isEqualTo("SENT");
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
@ -376,64 +518,155 @@ class TransactionInformationIdentifierControllerTest {
flowsStepSixDataThroughCentralBankSignatureAndCommercialBankReceive();
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
String bankPrivateKey = keyService.commercialBankKey(subject, "module-four-tester").getPrivateKey();
String transactionIdentifier = transactionMapper.selectList(null).get(0).getTransactionIdentifier();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization).contentType("application/json")
.content("{\"amount\":1590.85,\"institutionIdentifier\":\"ORG_3A4B5C6D7E8F\"," +
"\"transactionIdentifier\":\"" + transactionIdentifier + "\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第4次错误")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization).contentType("application/json")
.content("{\"amount\":1589.85,\"institutionIdentifier\":\"ORG_3A4B5C6D7E8F\"," +
"\"transactionIdentifier\":\"" + transactionIdentifier + "\"}"))
mvc.perform(get(CENTRAL_QUOTA_CONTROL_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.quotaId").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.length()").value(6));
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/static")
.header("Authorization", authorization))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("当前没有步骤八额度控制位记录"));
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/dynamic")
.header("Authorization", authorization))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("当前没有步骤八额度控制位记录"));
String staticSegmentResponse = mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("STATIC_READY"))
.andExpect(jsonPath("$.data.staticSegment").value(
org.hamcrest.Matchers.matchesPattern("STATIC_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.receiveStatus").value(1));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/dynamic-segment")
.header("Authorization", authorization).contentType("application/json")
.content("{\"randomValue\":\"7G8H9I0J\",\"timestamp\":\"20260730153600\"}"))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andReturn().getResponse().getContentAsString();
String staticSegment = com.jayway.jsonpath.JsonPath.read(staticSegmentResponse, "$.data.staticSegment");
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/static")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.dynamicSegment").value("DYN_7G8H9I0J_20260730153600"));
.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(jsonPath("$.data.dynamicSegment").value(
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")
.header("Authorization", authorization))
.andExpect(status().isOk())
.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))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.completeControlBit").value(
org.hamcrest.Matchers.containsString("|DYN_7G8H9I0J_20260730153600|CB_CTRL_")));
.andExpect(jsonPath("$.data.completeControlBit").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("|DYN_"),
org.hamcrest.Matchers.containsString("|CB_CTRL_"))));
String completeControlBit = quotaControlBitMapper.selectList(null).get(0).getCompleteControlBit();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"hashAlgorithm\":\"SM3\"}"))
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\""
+ completeControlBit + "\",\"hashAlgorithm\":\"SM3\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第5次错误")));
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第4次错误")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"WRONG_BANK_PRIVATE_KEY\",\"hashAlgorithm\":\"SM2\"}"))
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\""
+ 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(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第6次错误")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SIGNED"))
.andExpect(jsonPath("$.data.bankSignature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")));
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\""
+ completeControlBit + "\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isOk())
.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))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED"))
.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))
.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(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);
assertThat(stored.getStatus()).isEqualTo("RECEIVED");
@ -496,10 +729,18 @@ class TransactionInformationIdentifierControllerTest {
assertThat(currencies.stream().map(StandardCurrencyEntity::getDenomination)
.reduce(BigDecimal.ZERO, BigDecimal::add)).isEqualByComparingTo("1589.85");
assertThat(currencies.stream().map(StandardCurrencyEntity::getCurrencyId).distinct().count()).isEqualTo(27);
List<Long> currencyIds = currencies.stream().map(StandardCurrencyEntity::getId)
.collect(java.util.stream.Collectors.toList());
long batchId = standardCurrencyBatchMapper.selectList(null).get(0).getId();
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/generate").header("Authorization", authorization))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("当前状态:GENERATED")));
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("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).stream().map(StandardCurrencyEntity::getId)
.collect(java.util.stream.Collectors.toList())).containsExactlyElementsOf(currencyIds);
}
@Test
@ -525,18 +766,36 @@ class TransactionInformationIdentifierControllerTest {
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
void rejectsQuotaVerificationUntilStepFiveIsReceived() throws Exception {
insertCompletedPrecedingData();
String publicKey = keyService.commercialBankKey(
new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID), "module-four-tester").getPublicKey();
mvc.perform(post(VERIFICATION_BASE_PATH + "/verify")
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"publicKey\":\"" + publicKey + "\"}"))
.andExpect(status().isBadRequest())
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|TXN_"
+ "7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F\"}"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤五额度请求尚未发送并由央行接收")));
"当前没有额度请求记录")));
}
@Test
@ -545,24 +804,36 @@ class TransactionInformationIdentifierControllerTest {
mvc.perform(get(QUOTA_BASE_PATH + "/source-data").header("Authorization", authorization))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤四尚未完成交易信息标识组装")));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤四尚未完成交易信息标识组装")));
"步骤四尚未完成交易信息标识")));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|TXN_"
+ "7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F\"}"))
.andExpect(status().isBadRequest());
assertThat(transactionMapper.selectList(null)).isEmpty();
}
@Test
void duplicatePreparationReturnsLatestRecordWithoutAddingData() throws Exception {
void repeatedCorrectDigestUpdatesExistingRecordWithoutAddingData() throws Exception {
insertCompletedPrecedingData();
String first = mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
String second = mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
assertThat(com.jayway.jsonpath.JsonPath.<Number>read(second, "$.data.id").longValue())
.isEqualTo(com.jayway.jsonpath.JsonPath.<Number>read(first, "$.data.id").longValue());
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isOk());
long id = transactionMapper.selectList(null).get(0).getId();
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.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);
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() {

Loading…
Cancel
Save