From a9eb46d70095921e15745292894f668aff272d56 Mon Sep 17 00:00:00 2001
From: "jiazheng.zhao" <2624533894@qq.com>
Date: Tue, 4 Aug 2026 09:47:08 +0800
Subject: [PATCH 1/2] =?UTF-8?q?feat:=E5=8F=91=E8=A1=8C=E8=B4=A7=E5=B8=81?=
=?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 6 +
.../yau/digitalrmb/DigitalRmbApplication.java | 2 -
.../InstitutionApplicationPackageResult.java | 44 ++++++
...titutionApplicationVerificationResult.java | 40 +++++
...itutionApplicationVerificationService.java | 102 ++++++++++++
.../application/InstitutionDigestResult.java | 38 +++++
...stitutionIdentifierConfirmationResult.java | 29 ++++
.../InstitutionIdentifierStepService.java | 124 +++++++++++++++
...stitutionInformationPreparationResult.java | 50 ++++++
.../application/InstitutionKeyPairResult.java | 57 +++++++
.../application/InstitutionKeyService.java | 147 ++++++++++++++++++
.../application/InstitutionKeySubject.java | 38 +++++
.../InstitutionPublicKeyResult.java | 67 ++++++++
.../InstitutionSignatureResult.java | 56 +++++++
.../InstitutionTrainingErrorRecorder.java | 61 ++++++++
.../InstitutionIdentifierApplication.java | 140 +++++++++++++++++
.../InstitutionIdentifierRepository.java | 17 ++
.../InstitutionIdentityCryptography.java | 8 +
.../domain/InstitutionSm2KeyPair.java | 14 ++
...nstitutionIdentifierApplicationEntity.java | 25 +++
...nstitutionIdentifierApplicationMapper.java | 7 +
...stitutionIdentifierOperationLogEntity.java | 31 ++++
...stitutionIdentifierOperationLogMapper.java | 8 +
...titutionIdentifierTrainingErrorEntity.java | 38 +++++
...titutionIdentifierTrainingErrorMapper.java | 8 +
.../InstitutionPrivateKeyCipher.java | 60 +++++++
.../InstitutionSm2KeyAuditEntity.java | 35 +++++
.../InstitutionSm2KeyAuditMapper.java | 8 +
.../InstitutionSm2KeyEntity.java | 31 ++++
.../InstitutionSm2KeyMapper.java | 8 +
...ybatisInstitutionIdentifierRepository.java | 132 ++++++++++++++++
.../SmInstitutionIdentityCryptography.java | 109 +++++++++++++
.../ConfirmInstitutionIdentifierRequest.java | 28 ++++
.../GenerateInstitutionIdentifierRequest.java | 12 ++
.../dto/InstitutionIdentifierStepRequest.java | 26 ++++
.../VerifyInstitutionApplicationRequest.java | 28 ++++
.../rest/InstitutionIdentifierController.java | 136 ++++++++++++++++
.../application/CasTicketValidator.java | 2 +
.../application/PlatformTokenVerifier.java | 2 +
.../JdbcPlatformIdentityRepository.java | 2 +
.../PlatformReadOnlyDataSourceConfig.java | 4 +
.../CasAuthenticationController.java | 2 +
.../interfaces/PlatformSsoController.java | 2 +
.../security/application/JwtTokenService.java | 21 ++-
.../shared/web/GlobalExceptionHandler.java | 21 ++-
src/main/resources/application-test.yml | 1 +
src/main/resources/application.yml | 2 +
src/main/resources/schema.sql | 73 +++++++++
.../PlatformReadOnlyDataSourceConfigTest.java | 1 +
49 files changed, 1894 insertions(+), 9 deletions(-)
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationPackageResult.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationResult.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionDigestResult.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierConfirmationResult.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionInformationPreparationResult.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyPairResult.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeySubject.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionPublicKeyResult.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionSignatureResult.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionSm2KeyPair.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationEntity.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationMapper.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogMapper.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorMapper.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionPrivateKeyCipher.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditMapper.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyEntity.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyMapper.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/GenerateInstitutionIdentifierRequest.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java
create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java
diff --git a/pom.xml b/pom.xml
index 918239d..b0e4355 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,6 +23,7 @@
1.8
3.5.17
1.8.0
+ 1.70
@@ -71,6 +72,11 @@
lombok
true
+
+ org.bouncycastle
+ bcprov-jdk15on
+ ${bouncycastle.version}
+
com.h2database
h2
diff --git a/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java b/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java
index 74edcfb..93d0329 100644
--- a/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java
+++ b/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java
@@ -2,11 +2,9 @@ package com.yau.digitalrmb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
-@ConfigurationPropertiesScan
@EnableScheduling
public class DigitalRmbApplication {
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationPackageResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationPackageResult.java
new file mode 100644
index 0000000..c6e2676
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationPackageResult.java
@@ -0,0 +1,44 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+public class InstitutionApplicationPackageResult {
+ private final long applicationId;
+ private final String status;
+ private final String bankCode;
+ private final String timestamp;
+ private final String digest;
+ private final String signature;
+
+ public InstitutionApplicationPackageResult(long applicationId, String status, String bankCode, String timestamp,
+ String digest, String signature) {
+ this.applicationId = applicationId;
+ this.status = status;
+ this.bankCode = bankCode;
+ this.timestamp = timestamp;
+ this.digest = digest;
+ this.signature = signature;
+ }
+
+ public long getApplicationId() {
+ return applicationId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getBankCode() {
+ return bankCode;
+ }
+
+ public String getTimestamp() {
+ return timestamp;
+ }
+
+ public String getDigest() {
+ return digest;
+ }
+
+ public String getSignature() {
+ return signature;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationResult.java
new file mode 100644
index 0000000..8076e9b
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationResult.java
@@ -0,0 +1,40 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+public class InstitutionApplicationVerificationResult {
+ private final long applicationId;
+ private final String status;
+ private final String verificationAlgorithm;
+ private final String digestAlgorithm;
+ private final String signatureHashAlgorithm;
+ private final String verificationKeyId;
+ private final String originalText;
+ private final String submittedDigest;
+ private final String recalculatedDigest;
+ private final boolean signatureValid;
+ private final boolean digestMatches;
+ private final boolean verified;
+ private final String signatureConclusion;
+ private final String integrityConclusion;
+
+ public InstitutionApplicationVerificationResult(long applicationId, String status, String verificationAlgorithm,
+ String verificationKeyId, String originalText, String submittedDigest, String recalculatedDigest,
+ boolean signatureValid, boolean digestMatches) {
+ this.applicationId = applicationId; this.status = status;
+ this.verificationAlgorithm = verificationAlgorithm; this.digestAlgorithm = "SM3";
+ this.signatureHashAlgorithm = "SM3"; this.verificationKeyId = verificationKeyId; this.originalText = originalText;
+ this.submittedDigest = submittedDigest; this.recalculatedDigest = recalculatedDigest; this.signatureValid = signatureValid;
+ this.digestMatches = digestMatches; this.verified = signatureValid && digestMatches;
+ this.signatureConclusion = signatureValid ? "公钥与商业银行第二私钥匹配,身份真实性验证通过" : "签名验证失败,无法确认申请方身份";
+ this.integrityConclusion = digestMatches ? "重算摘要与申请摘要一致,数据完整性验证通过" : "重算摘要与申请摘要不一致,申请数据可能已被篡改";
+ }
+ public long getApplicationId() { return applicationId; }
+ public String getStatus() { return status; }
+ public String getVerificationAlgorithm() { return verificationAlgorithm; }
+ public String getDigestAlgorithm() { return digestAlgorithm; }
+ public String getSignatureHashAlgorithm() { return signatureHashAlgorithm; }
+ public String getVerificationKeyId() { return verificationKeyId; }
+ public String getOriginalText() { return originalText; } public String getSubmittedDigest() { return submittedDigest; }
+ public String getRecalculatedDigest() { return recalculatedDigest; } public boolean isSignatureValid() { return signatureValid; }
+ public boolean isDigestMatches() { return digestMatches; } public boolean isVerified() { return verified; }
+ public String getSignatureConclusion() { return signatureConclusion; } public String getIntegrityConclusion() { return integrityConclusion; }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java
new file mode 100644
index 0000000..1dd07e7
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java
@@ -0,0 +1,102 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierRepository;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
+import com.yau.digitalrmb.shared.api.ErrorCode;
+import com.yau.digitalrmb.shared.exception.BusinessException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+
+@Service
+public class InstitutionApplicationVerificationService {
+ @Resource
+ private InstitutionIdentityCryptography cryptography;
+
+ @Resource
+ private InstitutionKeyService keyService;
+
+ @Resource
+ private InstitutionIdentifierRepository repository;
+
+ @Resource
+ private InstitutionTrainingErrorRecorder errorRecorder;
+
+ @Transactional
+ public InstitutionApplicationVerificationResult verify(long applicationId, String verificationKeyId,
+ InstitutionKeySubject subject, String operator) {
+ InstitutionIdentifierApplication application = requireApplication(applicationId);
+ if (!InstitutionKeyService.BANK_SECOND_KEY.equals(verificationKeyId)) {
+ recordAndThrow(application, "WRONG_KEY", "VERIFY", verificationKeyId,
+ "验证必须选择商业银行第二密钥的公钥", operator);
+ }
+ requireStatus(application, InstitutionIdentifierApplication.Status.PACKAGED,
+ "当前步骤不是央行验证,不能操作", operator);
+ InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
+ String originalText = application.getBankCode() + "|" + application.getTimestamp();
+ String recalculatedDigest = cryptography.sm3(originalText);
+ boolean signatureValid = keyService.verifyCommercialBank(subject, application.getDigest(),
+ application.getBankSignature());
+ boolean digestMatches = recalculatedDigest.equalsIgnoreCase(application.getDigest());
+ invokeDomain(() -> application.recordVerification(verificationKeyId, signatureValid, digestMatches));
+ repository.update(application, expectedStatus, "VERIFY",
+ signatureValid && digestMatches ? "SM2身份验证和SM3完整性验证通过" : "申请验证失败",
+ operator);
+ return new InstitutionApplicationVerificationResult(applicationId, application.getStatus().name(),
+ "SM2_WITH_SM3_VERIFY_AND_SM3_RECALCULATE", verificationKeyId, originalText,
+ application.getDigest(), recalculatedDigest, signatureValid, digestMatches);
+ }
+
+ @Transactional
+ public InstitutionIdentifierConfirmationResult confirm(long applicationId, String signingKeyId,
+ InstitutionKeySubject subject, String operator) {
+ InstitutionIdentifierApplication application = requireApplication(applicationId);
+ if (!InstitutionKeyService.CENTRAL_FIRST_KEY.equals(signingKeyId)) {
+ recordAndThrow(application, "WRONG_KEY", "CONFIRM", signingKeyId,
+ "签名确权必须选择中央银行第一密钥", operator);
+ }
+ requireStatus(application, InstitutionIdentifierApplication.Status.VERIFIED,
+ "申请尚未验证通过,不能进行央行签名确权", operator);
+ InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
+ String digest = application.getDigest();
+ String identifier = "ORG_" + digest.substring(0, 12);
+ String centralBankSignature = keyService.signCentralBank(subject, digest);
+ invokeDomain(() -> application.issue(signingKeyId, identifier, centralBankSignature));
+ repository.update(application, expectedStatus, "CONFIRM",
+ "中央银行第一私钥完成SM2签名确权并签发机构标识", operator);
+ return new InstitutionIdentifierConfirmationResult(applicationId, application.getStatus().name(),
+ signingKeyId, centralBankSignature, identifier, digest);
+ }
+
+ private InstitutionIdentifierApplication requireApplication(long applicationId) {
+ return repository.findById(applicationId).orElseThrow(() ->
+ new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "机构标识申请不存在"));
+ }
+
+ private void requireStatus(InstitutionIdentifierApplication application,
+ InstitutionIdentifierApplication.Status expectedStatus,
+ String message, String operator) {
+ if (application.getStatus() != expectedStatus) {
+ recordAndThrow(application, "INVALID_STEP", expectedStatus.name(), null,
+ message + ",当前状态:" + application.getStatus().name(), operator);
+ }
+ }
+
+ private void recordAndThrow(InstitutionIdentifierApplication application, String errorType, String operationStep,
+ String providedKeyId, String message, String operator) {
+ int errorSequence = errorRecorder.record(application.getId(), errorType, operationStep, providedKeyId,
+ message, operator);
+ throw new BusinessException(ErrorCode.VALIDATION_ERROR,
+ message + ",本次实训第" + errorSequence + "次错误");
+ }
+
+ private static void invokeDomain(Runnable operation) {
+ try {
+ operation.run();
+ } catch (IllegalArgumentException | IllegalStateException exception) {
+ throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionDigestResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionDigestResult.java
new file mode 100644
index 0000000..1729bbb
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionDigestResult.java
@@ -0,0 +1,38 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+public class InstitutionDigestResult {
+ private final long applicationId;
+ private final String status;
+ private final String algorithm;
+ private final String concatenatedText;
+ 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;
+ 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;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierConfirmationResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierConfirmationResult.java
new file mode 100644
index 0000000..e60e5c6
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierConfirmationResult.java
@@ -0,0 +1,29 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+public class InstitutionIdentifierConfirmationResult {
+ private final long applicationId;
+ private final String status;
+ private final String signingAlgorithm;
+ private final String signatureHashAlgorithm;
+ private final String signatureAlgorithm;
+ private final String signingKeyId;
+ private final String centralBankSignature;
+ private final String institutionIdentifier;
+ private final String fullDigest;
+ public InstitutionIdentifierConfirmationResult(long applicationId, String status, String signingKeyId,
+ String centralBankSignature, String institutionIdentifier, String fullDigest) {
+ this.applicationId = applicationId; this.status = status;
+ this.signingAlgorithm = "SM2"; this.signatureHashAlgorithm = "SM3";
+ this.signatureAlgorithm = "SM2_WITH_SM3";
+ this.signingKeyId = signingKeyId; this.centralBankSignature = centralBankSignature;
+ this.institutionIdentifier = institutionIdentifier; this.fullDigest = fullDigest;
+ }
+ public long getApplicationId() { return applicationId; }
+ public String getStatus() { return status; }
+ public String getSigningAlgorithm() { return signingAlgorithm; }
+ public String getSignatureHashAlgorithm() { return signatureHashAlgorithm; }
+ public String getSignatureAlgorithm() { return signatureAlgorithm; }
+ public String getSigningKeyId() { return signingKeyId; }
+ public String getCentralBankSignature() { return centralBankSignature; } public String getInstitutionIdentifier() { return institutionIdentifier; }
+ public String getFullDigest() { return fullDigest; }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java
new file mode 100644
index 0000000..484b76b
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java
@@ -0,0 +1,124 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierRepository;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
+import com.yau.digitalrmb.shared.api.ErrorCode;
+import com.yau.digitalrmb.shared.exception.BusinessException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+
+@Service
+public class InstitutionIdentifierStepService {
+ private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+ @Resource
+ private InstitutionIdentityCryptography cryptography;
+
+ @Resource
+ private InstitutionKeyService keyService;
+
+ @Resource
+ private InstitutionIdentifierRepository repository;
+
+ @Resource
+ private InstitutionTrainingErrorRecorder errorRecorder;
+
+ private final Clock clock = Clock.systemUTC();
+
+ @Transactional
+ public InstitutionInformationPreparationResult prepare(String bankCode, String operator) {
+ String normalized = normalize(bankCode);
+ int trainingRound = repository.nextTrainingRound(normalized, operator);
+ String timestamp = LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC).format(TIMESTAMP);
+ InstitutionIdentifierApplication application = InstitutionIdentifierApplication.prepare(normalized, timestamp);
+ long applicationId = repository.save(application, trainingRound, operator);
+ return new InstitutionInformationPreparationResult(applicationId, application.getStatus().name(), normalized,
+ timestamp, normalized + "|" + timestamp, trainingRound, 0);
+ }
+
+ @Transactional
+ public InstitutionDigestResult digest(long applicationId, String operator) {
+ InstitutionIdentifierApplication application = requireApplication(applicationId);
+ requireStatus(application, InstitutionIdentifierApplication.Status.PREPARED, "当前步骤不是摘要计算,不能操作", operator);
+ InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
+ String text = application.getBankCode() + "|" + application.getTimestamp();
+ String digest = cryptography.sm3(text);
+ invokeDomain(() -> application.recordDigest(digest));
+ repository.update(application, expectedStatus, "DIGEST", "使用SM3计算并保存业务摘要", operator);
+ return new InstitutionDigestResult(applicationId, application.getStatus().name(), "SM3", text, digest);
+ }
+
+ @Transactional
+ public InstitutionSignatureResult sign(long applicationId, String keyId, InstitutionKeySubject subject,
+ String operator) {
+ InstitutionIdentifierApplication application = requireApplication(applicationId);
+ if (!InstitutionKeyService.BANK_SECOND_KEY.equals(keyId)) {
+ recordAndThrow(application, "WRONG_KEY", "SIGN", keyId,
+ "签名必须选择商业银行第二密钥", operator);
+ }
+ requireStatus(application, InstitutionIdentifierApplication.Status.DIGESTED,
+ "当前步骤不是商业银行签名,不能操作", operator);
+ InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
+ String signature = keyService.signCommercialBank(subject, application.getDigest());
+ invokeDomain(() -> application.recordBankSignature(signature));
+ repository.update(application, expectedStatus, "SIGN", "使用商业银行第二私钥完成SM2签名", operator);
+ return new InstitutionSignatureResult(applicationId, application.getStatus().name(), "SM2", keyId,
+ "COMMERCIAL_BANK_SECOND_PRIVATE_KEY", signature);
+ }
+
+ @Transactional
+ public InstitutionApplicationPackageResult packageApplication(long applicationId, String operator) {
+ InstitutionIdentifierApplication application = requireApplication(applicationId);
+ requireStatus(application, InstitutionIdentifierApplication.Status.SIGNED, "当前步骤不是申请组装,不能操作", operator);
+ InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
+ invokeDomain(application::packageApplication);
+ repository.update(application, expectedStatus, "PACKAGE", "组装机构标识申请材料并提交央行待验证", operator);
+ return new InstitutionApplicationPackageResult(applicationId, application.getStatus().name(),
+ application.getBankCode(), application.getTimestamp(), application.getDigest(),
+ application.getBankSignature());
+ }
+
+ private InstitutionIdentifierApplication requireApplication(long applicationId) {
+ return repository.findById(applicationId).orElseThrow(() ->
+ new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "机构标识申请不存在"));
+ }
+
+ private void requireStatus(InstitutionIdentifierApplication application,
+ InstitutionIdentifierApplication.Status expectedStatus,
+ String message, String operator) {
+ if (application.getStatus() != expectedStatus) {
+ recordAndThrow(application, "INVALID_STEP", expectedStatus.name(), null,
+ message + ",当前状态:" + application.getStatus().name(), operator);
+ }
+ }
+
+ private void recordAndThrow(InstitutionIdentifierApplication application, String errorType, String operationStep,
+ String providedKeyId, String message, String operator) {
+ int errorSequence = errorRecorder.record(application.getId(), errorType, operationStep, providedKeyId,
+ message, operator);
+ invalid(message + ",本次实训第" + errorSequence + "次错误");
+ }
+
+ private static void invokeDomain(Runnable operation) {
+ try {
+ operation.run();
+ } catch (IllegalArgumentException | IllegalStateException exception) {
+ invalid(exception.getMessage());
+ }
+ }
+
+ private static String normalize(String value) {
+ return value.trim().toUpperCase();
+ }
+
+ private static void invalid(String message) {
+ throw new BusinessException(ErrorCode.VALIDATION_ERROR, message);
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionInformationPreparationResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionInformationPreparationResult.java
new file mode 100644
index 0000000..a15a49e
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionInformationPreparationResult.java
@@ -0,0 +1,50 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+public class InstitutionInformationPreparationResult {
+ private final long applicationId;
+ private final String status;
+ private final String bankCode;
+ private final String timestamp;
+ private final String concatenatedText;
+ private final int trainingRound;
+ private final int scoringCriteria;
+
+ public InstitutionInformationPreparationResult(long applicationId, String status, String bankCode, String timestamp,
+ String concatenatedText, int trainingRound, int scoringCriteria) {
+ this.applicationId = applicationId;
+ this.status = status;
+ this.bankCode = bankCode;
+ this.timestamp = timestamp;
+ this.concatenatedText = concatenatedText;
+ this.trainingRound = trainingRound;
+ this.scoringCriteria = scoringCriteria;
+ }
+
+ public long getApplicationId() {
+ return applicationId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getBankCode() {
+ return bankCode;
+ }
+
+ public String getTimestamp() {
+ return timestamp;
+ }
+
+ public String getConcatenatedText() {
+ return concatenatedText;
+ }
+
+ public int getTrainingRound() {
+ return trainingRound;
+ }
+
+ public int getScoringCriteria() {
+ return scoringCriteria;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyPairResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyPairResult.java
new file mode 100644
index 0000000..cf02d54
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyPairResult.java
@@ -0,0 +1,57 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+public class InstitutionKeyPairResult {
+ private final long recordId;
+ private final String keyId;
+ private final String name;
+ private final String owner;
+ private final String purpose;
+ private final long userId;
+ private final long schoolId;
+ private final long classId;
+ private final String algorithm;
+ private final String signatureHashAlgorithm;
+ private final String signatureAlgorithm;
+ private final String curve;
+ private final String publicKeyFormat;
+ private final String privateKeyFormat;
+ private final String publicKey;
+ private final String privateKey;
+
+ public InstitutionKeyPairResult(long recordId, String keyId, String name, String owner, String purpose,
+ InstitutionKeySubject subject, String publicKey, String privateKey) {
+ this.recordId = recordId;
+ this.keyId = keyId;
+ this.name = name;
+ this.owner = owner;
+ this.purpose = purpose;
+ this.userId = subject.getUserId();
+ this.schoolId = subject.getSchoolId();
+ this.classId = subject.getClassId();
+ this.algorithm = "SM2";
+ this.signatureHashAlgorithm = "SM3";
+ this.signatureAlgorithm = "SM2_WITH_SM3";
+ this.curve = "sm2p256v1";
+ this.publicKeyFormat = "UNCOMPRESSED_EC_POINT_HEX";
+ this.privateKeyFormat = "SM2_PRIVATE_SCALAR_HEX";
+ this.publicKey = publicKey;
+ this.privateKey = privateKey;
+ }
+
+ public long getRecordId() { return recordId; }
+ public String getKeyId() { return keyId; }
+ public String getName() { return name; }
+ public String getOwner() { return owner; }
+ public String getPurpose() { return purpose; }
+ public long getUserId() { return userId; }
+ public long getSchoolId() { return schoolId; }
+ public long getClassId() { return classId; }
+ public String getAlgorithm() { return algorithm; }
+ public String getSignatureHashAlgorithm() { return signatureHashAlgorithm; }
+ public String getSignatureAlgorithm() { return signatureAlgorithm; }
+ public String getCurve() { return curve; }
+ public String getPublicKeyFormat() { return publicKeyFormat; }
+ public String getPrivateKeyFormat() { return privateKeyFormat; }
+ public String getPublicKey() { return publicKey; }
+ public String getPrivateKey() { return privateKey; }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java
new file mode 100644
index 0000000..6a18245
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java
@@ -0,0 +1,147 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionPrivateKeyCipher;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyAuditEntity;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyAuditMapper;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyEntity;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyMapper;
+import com.yau.digitalrmb.shared.api.ErrorCode;
+import com.yau.digitalrmb.shared.exception.BusinessException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+
+@Service
+public class InstitutionKeyService {
+ public static final String BANK_SECOND_KEY = "BANK_SECOND_SM2_KEY";
+ public static final String CENTRAL_FIRST_KEY = "CENTRAL_FIRST_SM2_KEY";
+
+ @Resource
+ private InstitutionIdentityCryptography cryptography;
+ @Resource
+ private InstitutionPrivateKeyCipher privateKeyCipher;
+ @Resource
+ private InstitutionSm2KeyMapper keyMapper;
+ @Resource
+ private InstitutionSm2KeyAuditMapper auditMapper;
+
+ @Transactional
+ public InstitutionKeyPairResult commercialBankKey(InstitutionKeySubject subject, String operator) {
+ return findOrCreate(subject, BANK_SECOND_KEY, "商业银行第二密钥", "COMMERCIAL_BANK",
+ "SIGN_APPLICATION", operator);
+ }
+
+ @Transactional
+ public InstitutionKeyPairResult centralBankKey(InstitutionKeySubject subject, String operator) {
+ return findOrCreate(subject, CENTRAL_FIRST_KEY, "中央银行第一密钥", "CENTRAL_BANK",
+ "SIGN_IDENTIFIER", operator);
+ }
+
+ @Transactional(readOnly = true)
+ public String signCommercialBank(InstitutionKeySubject subject, String message) {
+ return cryptography.sign(privateKey(subject, BANK_SECOND_KEY), message);
+ }
+
+ @Transactional(readOnly = true)
+ public boolean verifyCommercialBank(InstitutionKeySubject subject, String message, String signature) {
+ return cryptography.verify(required(subject, BANK_SECOND_KEY).getPublicKey(), message, signature);
+ }
+
+ @Transactional(readOnly = true)
+ public String signCentralBank(InstitutionKeySubject subject, String message) {
+ return cryptography.sign(privateKey(subject, CENTRAL_FIRST_KEY), message);
+ }
+
+ @Transactional
+ public void logicalDelete(InstitutionKeySubject subject, String keyId, String operator) {
+ InstitutionSm2KeyEntity key = required(subject, keyId);
+ LambdaUpdateWrapper update = new LambdaUpdateWrapper()
+ .eq(InstitutionSm2KeyEntity::getId, key.getId())
+ .eq(InstitutionSm2KeyEntity::getDeleted, false)
+ .set(InstitutionSm2KeyEntity::getDeleted, true)
+ .set(InstitutionSm2KeyEntity::getUpdatedAt, LocalDateTime.now())
+ .set(InstitutionSm2KeyEntity::getUpdatedBy, operator);
+ if (keyMapper.update(null, update) != 1) {
+ throw new BusinessException(ErrorCode.VALIDATION_ERROR, "密钥已删除或状态已变化");
+ }
+ audit(key, subject, "DELETE", "逻辑删除机构SM2密钥", operator);
+ }
+
+ private InstitutionKeyPairResult findOrCreate(InstitutionKeySubject subject, String keyId, String name,
+ String owner, String purpose, String operator) {
+ InstitutionSm2KeyEntity key = find(subject, keyId);
+ if (key == null) {
+ InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair();
+ key = new InstitutionSm2KeyEntity();
+ key.setKeyId(keyId);
+ key.setKeyName(name);
+ key.setKeyOwner(owner);
+ key.setKeyPurpose(purpose);
+ key.setUserId(subject.getUserId());
+ key.setSchoolId(subject.getSchoolId());
+ key.setClassId(subject.getClassId());
+ key.setPublicKey(pair.getPublicKey());
+ key.setEncryptedPrivateKey(privateKeyCipher.encrypt(pair.getPrivateKey()));
+ LocalDateTime now = LocalDateTime.now();
+ key.setCreatedAt(now);
+ key.setUpdatedAt(now);
+ key.setCreatedBy(operator);
+ key.setUpdatedBy(operator);
+ key.setDeleted(false);
+ keyMapper.insert(key);
+ audit(key, subject, "CREATE", "生成并保存机构SM2密钥对", operator);
+ } else {
+ audit(key, subject, "VIEW_PRIVATE_KEY", "受控查看机构SM2公私钥", operator);
+ }
+ return result(key, subject);
+ }
+
+ private String privateKey(InstitutionKeySubject subject, String keyId) {
+ return privateKeyCipher.decrypt(required(subject, keyId).getEncryptedPrivateKey());
+ }
+
+ private InstitutionSm2KeyEntity required(InstitutionKeySubject subject, String keyId) {
+ InstitutionSm2KeyEntity key = find(subject, keyId);
+ if (key == null) {
+ throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前用户、学校和班级下的密钥不存在,请先获取密钥");
+ }
+ return key;
+ }
+
+ private InstitutionSm2KeyEntity find(InstitutionKeySubject subject, String keyId) {
+ return keyMapper.selectOne(new LambdaQueryWrapper()
+ .eq(InstitutionSm2KeyEntity::getKeyId, keyId)
+ .eq(InstitutionSm2KeyEntity::getUserId, subject.getUserId())
+ .eq(InstitutionSm2KeyEntity::getSchoolId, subject.getSchoolId())
+ .eq(InstitutionSm2KeyEntity::getClassId, subject.getClassId())
+ .eq(InstitutionSm2KeyEntity::getDeleted, false)
+ .orderByDesc(InstitutionSm2KeyEntity::getCreatedAt)
+ .last("LIMIT 1"));
+ }
+
+ private InstitutionKeyPairResult result(InstitutionSm2KeyEntity key, InstitutionKeySubject subject) {
+ return new InstitutionKeyPairResult(key.getId(), key.getKeyId(), key.getKeyName(), key.getKeyOwner(),
+ key.getKeyPurpose(), subject, key.getPublicKey(), privateKeyCipher.decrypt(key.getEncryptedPrivateKey()));
+ }
+
+ private void audit(InstitutionSm2KeyEntity key, InstitutionKeySubject subject, String operation,
+ String detail, String operator) {
+ InstitutionSm2KeyAuditEntity audit = new InstitutionSm2KeyAuditEntity();
+ audit.setKeyRecordId(key.getId());
+ audit.setKeyId(key.getKeyId());
+ audit.setOperation(operation);
+ audit.setUserId(subject.getUserId());
+ audit.setSchoolId(subject.getSchoolId());
+ audit.setClassId(subject.getClassId());
+ audit.setOperationDetail(detail);
+ audit.setCreatedAt(LocalDateTime.now());
+ audit.setCreatedBy(operator);
+ auditMapper.insert(audit);
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeySubject.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeySubject.java
new file mode 100644
index 0000000..c1e6ae6
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeySubject.java
@@ -0,0 +1,38 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+import com.yau.digitalrmb.shared.api.ErrorCode;
+import com.yau.digitalrmb.shared.exception.BusinessException;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+public class InstitutionKeySubject {
+ private final long userId;
+ private final long schoolId;
+ private final long classId;
+
+ public InstitutionKeySubject(long userId, long schoolId, long classId) {
+ this.userId = userId;
+ this.schoolId = schoolId;
+ this.classId = classId;
+ }
+
+ public static InstitutionKeySubject from(Jwt jwt) {
+ return new InstitutionKeySubject(requiredLong(jwt, "userId"), requiredLong(jwt, "schoolId"),
+ requiredLong(jwt, "classId"));
+ }
+
+ private static long requiredLong(Jwt jwt, String claim) {
+ Object value = jwt.getClaims().get(claim);
+ if (value == null) {
+ throw new BusinessException(ErrorCode.UNAUTHORIZED, "Token缺少" + claim);
+ }
+ try {
+ return Long.parseLong(String.valueOf(value));
+ } catch (NumberFormatException exception) {
+ throw new BusinessException(ErrorCode.UNAUTHORIZED, "Token中的" + claim + "格式不正确");
+ }
+ }
+
+ public long getUserId() { return userId; }
+ public long getSchoolId() { return schoolId; }
+ public long getClassId() { return classId; }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionPublicKeyResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionPublicKeyResult.java
new file mode 100644
index 0000000..334c796
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionPublicKeyResult.java
@@ -0,0 +1,67 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+public class InstitutionPublicKeyResult {
+ private final String keyId;
+ private final String name;
+ private final String owner;
+ private final String purpose;
+ private final String algorithm;
+ private final String signatureHashAlgorithm;
+ private final String signatureAlgorithm;
+ private final String curve;
+ private final String publicKeyFormat;
+ private final String publicKey;
+
+ public InstitutionPublicKeyResult(String keyId, String name, String owner, String purpose, String publicKey) {
+ this.keyId = keyId;
+ this.name = name;
+ this.owner = owner;
+ this.purpose = purpose;
+ this.algorithm = "SM2";
+ this.signatureHashAlgorithm = "SM3";
+ this.signatureAlgorithm = "SM2_WITH_SM3";
+ this.curve = "sm2p256v1";
+ this.publicKeyFormat = "UNCOMPRESSED_EC_POINT_HEX";
+ this.publicKey = publicKey;
+ }
+
+ public String getKeyId() {
+ return keyId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public String getPurpose() {
+ return purpose;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public String getSignatureHashAlgorithm() {
+ return signatureHashAlgorithm;
+ }
+
+ public String getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public String getCurve() {
+ return curve;
+ }
+
+ public String getPublicKeyFormat() {
+ return publicKeyFormat;
+ }
+
+ public String getPublicKey() {
+ return publicKey;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionSignatureResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionSignatureResult.java
new file mode 100644
index 0000000..8dde9d2
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionSignatureResult.java
@@ -0,0 +1,56 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+public class InstitutionSignatureResult {
+ private final long applicationId;
+ private final String status;
+ private final String algorithm;
+ private final String signatureHashAlgorithm;
+ private final String signatureAlgorithm;
+ private final String keyId;
+ private final String keyPurpose;
+ private final String signature;
+
+ public InstitutionSignatureResult(long applicationId, String status, String algorithm, String keyId,
+ String keyPurpose, String signature) {
+ this.applicationId = applicationId;
+ this.status = status;
+ this.algorithm = algorithm;
+ this.signatureHashAlgorithm = "SM3";
+ this.signatureAlgorithm = "SM2_WITH_SM3";
+ this.keyId = keyId;
+ this.keyPurpose = keyPurpose;
+ this.signature = signature;
+ }
+
+ public long getApplicationId() {
+ return applicationId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public String getSignatureHashAlgorithm() {
+ return signatureHashAlgorithm;
+ }
+
+ public String getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public String getKeyId() {
+ return keyId;
+ }
+
+ public String getKeyPurpose() {
+ return keyPurpose;
+ }
+
+ public String getSignature() {
+ return signature;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java
new file mode 100644
index 0000000..692f967
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java
@@ -0,0 +1,61 @@
+package com.yau.digitalrmb.institutionidentity.application;
+
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierTrainingErrorEntity;
+import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierTrainingErrorMapper;
+import com.yau.digitalrmb.shared.api.ErrorCode;
+import com.yau.digitalrmb.shared.exception.BusinessException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+
+@Service
+public class InstitutionTrainingErrorRecorder {
+ @Resource
+ private InstitutionIdentifierApplicationMapper applicationMapper;
+
+ @Resource
+ private InstitutionIdentifierTrainingErrorMapper errorMapper;
+
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public int record(long applicationId, String errorType, String operationStep, String providedKeyId,
+ String errorMessage, String operator) {
+ InstitutionIdentifierApplicationEntity application = applicationMapper.selectById(applicationId);
+ if (application == null || Boolean.TRUE.equals(application.getDeleted())) {
+ throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "机构标识申请不存在");
+ }
+
+ LambdaUpdateWrapper increment =
+ new LambdaUpdateWrapper()
+ .eq(InstitutionIdentifierApplicationEntity::getId, applicationId)
+ .eq(InstitutionIdentifierApplicationEntity::getScoringCriteria,
+ application.getScoringCriteria())
+ .eq(InstitutionIdentifierApplicationEntity::getDeleted, false)
+ .setSql("scoring_criteria = scoring_criteria + 1")
+ .set(InstitutionIdentifierApplicationEntity::getUpdatedAt, LocalDateTime.now())
+ .set(InstitutionIdentifierApplicationEntity::getUpdatedBy, operator);
+ if (applicationMapper.update(null, increment) != 1) {
+ throw new BusinessException(ErrorCode.VALIDATION_ERROR, "错误次数已变化,请重新操作");
+ }
+
+ int errorSequence = application.getScoringCriteria() + 1;
+ InstitutionIdentifierTrainingErrorEntity error = new InstitutionIdentifierTrainingErrorEntity();
+ error.setApplicationId(applicationId);
+ error.setTrainingRound(application.getTrainingRound());
+ error.setErrorSequence(errorSequence);
+ error.setErrorType(errorType);
+ error.setOperationStep(operationStep);
+ error.setApplicationStatus(application.getStatus());
+ error.setProvidedKeyId(providedKeyId);
+ error.setErrorMessage(errorMessage);
+ error.setCreatedAt(LocalDateTime.now());
+ error.setCreatedBy(operator);
+ errorMapper.insert(error);
+ return errorSequence;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java
new file mode 100644
index 0000000..8fc9501
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java
@@ -0,0 +1,140 @@
+package com.yau.digitalrmb.institutionidentity.domain;
+
+import java.util.regex.Pattern;
+
+public class InstitutionIdentifierApplication {
+ private static final Pattern BANK_CODE = Pattern.compile("[A-Z0-9]{8,32}");
+
+ public enum Status {
+ PREPARED,
+ DIGESTED,
+ SIGNED,
+ PACKAGED,
+ VERIFIED,
+ VERIFICATION_FAILED,
+ ISSUED,
+ REJECTED
+ }
+
+ private final Long id;
+ private final String bankCode;
+ private final String timestamp;
+ private String digest;
+ private String bankSignature;
+ private Status status;
+ private String verificationKeyId;
+ private Boolean signatureValid;
+ private Boolean digestMatches;
+ private String signingKeyId;
+ private String institutionIdentifier;
+ private String centralBankSignature;
+
+ public InstitutionIdentifierApplication(Long id, String bankCode, String timestamp, String digest,
+ String bankSignature, Status status, String verificationKeyId,
+ Boolean signatureValid, Boolean digestMatches, String signingKeyId,
+ String institutionIdentifier, String centralBankSignature) {
+ validateBase(bankCode, timestamp);
+ this.id = id;
+ this.bankCode = bankCode;
+ this.timestamp = timestamp;
+ this.digest = digest;
+ this.bankSignature = bankSignature;
+ this.status = status;
+ this.verificationKeyId = verificationKeyId;
+ this.signatureValid = signatureValid;
+ this.digestMatches = digestMatches;
+ this.signingKeyId = signingKeyId;
+ this.institutionIdentifier = institutionIdentifier;
+ this.centralBankSignature = centralBankSignature;
+ }
+
+ public static InstitutionIdentifierApplication prepare(String bankCode, String timestamp) {
+ return new InstitutionIdentifierApplication(null, bankCode, timestamp, null, null, Status.PREPARED,
+ null, null, null, null, null, null);
+ }
+
+ public static InstitutionIdentifierApplication restore(Long id, String bankCode, String timestamp, String digest,
+ String bankSignature, Status status, String verificationKeyId, Boolean signatureValid,
+ Boolean digestMatches, String signingKeyId, String institutionIdentifier, String centralBankSignature) {
+ return new InstitutionIdentifierApplication(id, bankCode, timestamp, digest, bankSignature, status,
+ verificationKeyId, signatureValid, digestMatches, signingKeyId, institutionIdentifier,
+ centralBankSignature);
+ }
+
+ public void recordDigest(String digest) {
+ requireStatus(Status.PREPARED, "当前步骤不是摘要计算,不能操作");
+ if (digest == null || !digest.matches("[0-9A-F]{64}")) {
+ throw new IllegalArgumentException("SM3摘要格式不正确");
+ }
+ this.digest = digest;
+ this.status = Status.DIGESTED;
+ }
+
+ public void recordBankSignature(String bankSignature) {
+ requireStatus(Status.DIGESTED, "当前步骤不是商业银行签名,不能操作");
+ if (bankSignature == null || !bankSignature.matches("[0-9A-F]+")) {
+ throw new IllegalArgumentException("银行SM2签名格式不正确");
+ }
+ this.bankSignature = bankSignature;
+ this.status = Status.SIGNED;
+ }
+
+ public void packageApplication() {
+ requireStatus(Status.SIGNED, "当前步骤不是申请组装,不能操作");
+ this.status = Status.PACKAGED;
+ }
+
+ public void recordVerification(String verificationKeyId, boolean signatureValid, boolean digestMatches) {
+ requireStatus(Status.PACKAGED, "当前步骤不是央行验证,不能操作");
+ this.verificationKeyId = verificationKeyId;
+ this.signatureValid = signatureValid;
+ this.digestMatches = digestMatches;
+ this.status = signatureValid && digestMatches ? Status.VERIFIED : Status.VERIFICATION_FAILED;
+ }
+
+ public void issue(String signingKeyId, String institutionIdentifier, String centralBankSignature) {
+ requireStatus(Status.VERIFIED, "申请尚未验证通过,不能进行央行签名确权");
+ if (signingKeyId == null || institutionIdentifier == null || centralBankSignature == null) {
+ throw new IllegalArgumentException("签发信息不能为空");
+ }
+ this.signingKeyId = signingKeyId;
+ this.institutionIdentifier = institutionIdentifier;
+ this.centralBankSignature = centralBankSignature;
+ this.status = Status.ISSUED;
+ }
+
+ public void reject() {
+ if (status == Status.ISSUED || status == Status.REJECTED) {
+ throw new IllegalStateException("申请状态不允许拒绝");
+ }
+ status = Status.REJECTED;
+ }
+
+ private void requireStatus(Status expected, String message) {
+ if (status != expected) {
+ throw new IllegalStateException(message + ",当前状态:" + status.name());
+ }
+ }
+
+ private static void validateBase(String bankCode, String timestamp) {
+ if (bankCode == null || !BANK_CODE.matcher(bankCode).matches()) {
+ throw new IllegalArgumentException("银行代码格式不正确");
+ }
+ if (timestamp == null || !timestamp.matches("\\d{14}")) {
+ throw new IllegalArgumentException("时间戳格式不正确");
+ }
+ }
+
+ public Long getId() { return id; }
+ public String getBankCode() { return bankCode; }
+ public String getTimestamp() { return timestamp; }
+ public String getDigest() { return digest; }
+ public String getBankSignature() { return bankSignature; }
+ public Status getStatus() { return status; }
+ public String getVerificationKeyId() { return verificationKeyId; }
+ public Boolean getSignatureValid() { return signatureValid; }
+ public Boolean getDigestMatches() { return digestMatches; }
+ public String getSigningKeyId() { return signingKeyId; }
+ public String getInstitutionIdentifier() { return institutionIdentifier; }
+ public String getCentralBankSignature() { return centralBankSignature; }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java
new file mode 100644
index 0000000..61a33c8
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java
@@ -0,0 +1,17 @@
+package com.yau.digitalrmb.institutionidentity.domain;
+
+import java.util.Optional;
+
+public interface InstitutionIdentifierRepository {
+ long save(InstitutionIdentifierApplication application, int trainingRound, String operator);
+
+ Optional findById(long applicationId);
+
+ void update(InstitutionIdentifierApplication application,
+ InstitutionIdentifierApplication.Status expectedStatus,
+ String operation,
+ String operationDetail,
+ String operator);
+
+ int nextTrainingRound(String bankCode, String operator);
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java
new file mode 100644
index 0000000..78c0766
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java
@@ -0,0 +1,8 @@
+package com.yau.digitalrmb.institutionidentity.domain;
+
+public interface InstitutionIdentityCryptography {
+ String sm3(String text);
+ InstitutionSm2KeyPair generateSm2KeyPair();
+ String sign(String privateKey, String message);
+ boolean verify(String publicKey, String message, String signature);
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionSm2KeyPair.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionSm2KeyPair.java
new file mode 100644
index 0000000..b413271
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionSm2KeyPair.java
@@ -0,0 +1,14 @@
+package com.yau.digitalrmb.institutionidentity.domain;
+
+public class InstitutionSm2KeyPair {
+ private final String privateKey;
+ private final String publicKey;
+
+ public InstitutionSm2KeyPair(String privateKey, String publicKey) {
+ this.privateKey = privateKey;
+ this.publicKey = publicKey;
+ }
+
+ public String getPrivateKey() { return privateKey; }
+ public String getPublicKey() { return publicKey; }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationEntity.java
new file mode 100644
index 0000000..43fc890
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationEntity.java
@@ -0,0 +1,25 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter @Setter
+@TableName("institution_identifier_application")
+public class InstitutionIdentifierApplicationEntity extends AuditableEntity {
+ @TableField("bank_code") private String bankCode;
+ @TableField("request_timestamp") private String requestTimestamp;
+ private String digest;
+ @TableField("bank_signature") private String bankSignature;
+ private String status;
+ @TableField("verification_key_id") private String verificationKeyId;
+ @TableField("signature_valid") private Boolean signatureValid;
+ @TableField("digest_matches") private Boolean digestMatches;
+ @TableField("signing_key_id") private String signingKeyId;
+ @TableField("institution_identifier") private String institutionIdentifier;
+ @TableField("central_bank_signature") private String centralBankSignature;
+ @TableField("training_round") private Integer trainingRound;
+ @TableField("scoring_criteria") private Integer scoringCriteria;
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationMapper.java
new file mode 100644
index 0000000..46d83af
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierApplicationMapper.java
@@ -0,0 +1,7 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface InstitutionIdentifierApplicationMapper extends BaseMapper { }
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java
new file mode 100644
index 0000000..a2bad64
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java
@@ -0,0 +1,31 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@TableName("institution_identifier_operation_log")
+public class InstitutionIdentifierOperationLogEntity {
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+ @TableField("application_id")
+ private Long applicationId;
+ private String operation;
+ @TableField("from_status")
+ private String fromStatus;
+ @TableField("to_status")
+ private String toStatus;
+ @TableField("operation_detail")
+ private String operationDetail;
+ @TableField("created_at")
+ private LocalDateTime createdAt;
+ @TableField("created_by")
+ private String createdBy;
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogMapper.java
new file mode 100644
index 0000000..baa7dfc
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogMapper.java
@@ -0,0 +1,8 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface InstitutionIdentifierOperationLogMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java
new file mode 100644
index 0000000..d330d6e
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java
@@ -0,0 +1,38 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@TableName("institution_identifier_training_error")
+public class InstitutionIdentifierTrainingErrorEntity {
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+ @TableField("application_id")
+ private Long applicationId;
+ @TableField("training_round")
+ private Integer trainingRound;
+ @TableField("error_sequence")
+ private Integer errorSequence;
+ @TableField("error_type")
+ private String errorType;
+ @TableField("operation_step")
+ private String operationStep;
+ @TableField("application_status")
+ private String applicationStatus;
+ @TableField("provided_key_id")
+ private String providedKeyId;
+ @TableField("error_message")
+ private String errorMessage;
+ @TableField("created_at")
+ private LocalDateTime createdAt;
+ @TableField("created_by")
+ private String createdBy;
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorMapper.java
new file mode 100644
index 0000000..72a22da
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorMapper.java
@@ -0,0 +1,8 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface InstitutionIdentifierTrainingErrorMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionPrivateKeyCipher.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionPrivateKeyCipher.java
new file mode 100644
index 0000000..9e3ae20
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionPrivateKeyCipher.java
@@ -0,0 +1,60 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.yau.digitalrmb.security.config.SecurityProperties;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+@Component
+public class InstitutionPrivateKeyCipher {
+ private static final int IV_LENGTH = 12;
+ private static final int TAG_LENGTH = 128;
+ private final SecretKeySpec encryptionKey;
+
+ public InstitutionPrivateKeyCipher(SecurityProperties properties) {
+ try {
+ byte[] digest = MessageDigest.getInstance("SHA-256")
+ .digest(properties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8));
+ this.encryptionKey = new SecretKeySpec(digest, "AES");
+ } catch (Exception exception) {
+ throw new IllegalStateException("机构私钥加密组件初始化失败", exception);
+ }
+ }
+
+ public String encrypt(String privateKey) {
+ try {
+ byte[] iv = new byte[IV_LENGTH];
+ new SecureRandom().nextBytes(iv);
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new GCMParameterSpec(TAG_LENGTH, iv));
+ byte[] encrypted = cipher.doFinal(privateKey.getBytes(StandardCharsets.US_ASCII));
+ byte[] result = new byte[iv.length + encrypted.length];
+ System.arraycopy(iv, 0, result, 0, iv.length);
+ System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
+ return Base64.getEncoder().encodeToString(result);
+ } catch (Exception exception) {
+ throw new IllegalStateException("机构私钥加密失败", exception);
+ }
+ }
+
+ public String decrypt(String encryptedPrivateKey) {
+ try {
+ byte[] value = Base64.getDecoder().decode(encryptedPrivateKey);
+ byte[] iv = new byte[IV_LENGTH];
+ byte[] encrypted = new byte[value.length - IV_LENGTH];
+ System.arraycopy(value, 0, iv, 0, iv.length);
+ System.arraycopy(value, iv.length, encrypted, 0, encrypted.length);
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new GCMParameterSpec(TAG_LENGTH, iv));
+ return new String(cipher.doFinal(encrypted), StandardCharsets.US_ASCII);
+ } catch (Exception exception) {
+ throw new IllegalStateException("机构私钥解密失败", exception);
+ }
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java
new file mode 100644
index 0000000..644278b
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java
@@ -0,0 +1,35 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@TableName("institution_sm2_key_audit")
+public class InstitutionSm2KeyAuditEntity {
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+ @TableField("key_record_id")
+ private Long keyRecordId;
+ @TableField("key_id")
+ private String keyId;
+ private String operation;
+ @TableField("user_id")
+ private Long userId;
+ @TableField("school_id")
+ private Long schoolId;
+ @TableField("class_id")
+ private Long classId;
+ @TableField("operation_detail")
+ private String operationDetail;
+ @TableField("created_at")
+ private LocalDateTime createdAt;
+ @TableField("created_by")
+ private String createdBy;
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditMapper.java
new file mode 100644
index 0000000..d2f30ed
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditMapper.java
@@ -0,0 +1,8 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface InstitutionSm2KeyAuditMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyEntity.java
new file mode 100644
index 0000000..1caaeda
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyEntity.java
@@ -0,0 +1,31 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@TableName("institution_sm2_key")
+public class InstitutionSm2KeyEntity extends AuditableEntity {
+ @TableField("key_id")
+ private String keyId;
+ @TableField("key_name")
+ private String keyName;
+ @TableField("key_owner")
+ private String keyOwner;
+ @TableField("key_purpose")
+ private String keyPurpose;
+ @TableField("user_id")
+ private Long userId;
+ @TableField("school_id")
+ private Long schoolId;
+ @TableField("class_id")
+ private Long classId;
+ @TableField("public_key")
+ private String publicKey;
+ @TableField("encrypted_private_key")
+ private String encryptedPrivateKey;
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyMapper.java
new file mode 100644
index 0000000..c50e2ea
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyMapper.java
@@ -0,0 +1,8 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface InstitutionSm2KeyMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java
new file mode 100644
index 0000000..6faed36
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java
@@ -0,0 +1,132 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierRepository;
+import com.yau.digitalrmb.shared.api.ErrorCode;
+import com.yau.digitalrmb.shared.exception.BusinessException;
+import org.springframework.stereotype.Repository;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+@Repository
+public class MybatisInstitutionIdentifierRepository implements InstitutionIdentifierRepository {
+ @Resource
+ private InstitutionIdentifierApplicationMapper mapper;
+
+ @Resource
+ private InstitutionIdentifierOperationLogMapper operationLogMapper;
+
+ @Override
+ public long save(InstitutionIdentifierApplication application, int trainingRound, String operator) {
+ InstitutionIdentifierApplicationEntity entity = toEntity(application);
+ entity.setTrainingRound(trainingRound);
+ entity.setScoringCriteria(0);
+ LocalDateTime now = LocalDateTime.now();
+ entity.setCreatedAt(now);
+ entity.setUpdatedAt(now);
+ entity.setCreatedBy(operator);
+ entity.setUpdatedBy(operator);
+ entity.setDeleted(false);
+ mapper.insert(entity);
+ insertLog(entity.getId(), "PREPARE", null, application.getStatus().name(),
+ "创建机构标识申请并生成业务时间戳", operator);
+ return entity.getId();
+ }
+
+ @Override
+ public Optional findById(long applicationId) {
+ InstitutionIdentifierApplicationEntity entity = mapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(InstitutionIdentifierApplicationEntity::getId, applicationId)
+ .eq(InstitutionIdentifierApplicationEntity::getDeleted, false));
+ return Optional.ofNullable(entity).map(this::toDomain);
+ }
+
+ @Override
+ public void update(InstitutionIdentifierApplication application,
+ InstitutionIdentifierApplication.Status expectedStatus,
+ String operation,
+ String operationDetail,
+ String operator) {
+ LambdaUpdateWrapper update =
+ new LambdaUpdateWrapper()
+ .eq(InstitutionIdentifierApplicationEntity::getId, application.getId())
+ .eq(InstitutionIdentifierApplicationEntity::getStatus, expectedStatus.name())
+ .eq(InstitutionIdentifierApplicationEntity::getDeleted, false)
+ .set(InstitutionIdentifierApplicationEntity::getDigest, application.getDigest())
+ .set(InstitutionIdentifierApplicationEntity::getBankSignature, application.getBankSignature())
+ .set(InstitutionIdentifierApplicationEntity::getStatus, application.getStatus().name())
+ .set(InstitutionIdentifierApplicationEntity::getVerificationKeyId,
+ application.getVerificationKeyId())
+ .set(InstitutionIdentifierApplicationEntity::getSignatureValid,
+ application.getSignatureValid())
+ .set(InstitutionIdentifierApplicationEntity::getDigestMatches,
+ application.getDigestMatches())
+ .set(InstitutionIdentifierApplicationEntity::getSigningKeyId, application.getSigningKeyId())
+ .set(InstitutionIdentifierApplicationEntity::getInstitutionIdentifier,
+ application.getInstitutionIdentifier())
+ .set(InstitutionIdentifierApplicationEntity::getCentralBankSignature,
+ application.getCentralBankSignature())
+ .set(InstitutionIdentifierApplicationEntity::getUpdatedAt, LocalDateTime.now())
+ .set(InstitutionIdentifierApplicationEntity::getUpdatedBy, operator);
+ if (mapper.update(null, update) != 1) {
+ throw new BusinessException(ErrorCode.VALIDATION_ERROR, "申请状态已变化,不能重复或越级操作");
+ }
+ insertLog(application.getId(), operation, expectedStatus.name(), application.getStatus().name(),
+ operationDetail, operator);
+ }
+
+ @Override
+ public int nextTrainingRound(String bankCode, String operator) {
+ InstitutionIdentifierApplicationEntity latest = mapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(InstitutionIdentifierApplicationEntity::getBankCode, bankCode)
+ .eq(InstitutionIdentifierApplicationEntity::getCreatedBy, operator)
+ .eq(InstitutionIdentifierApplicationEntity::getDeleted, false)
+ .orderByDesc(InstitutionIdentifierApplicationEntity::getTrainingRound)
+ .last("LIMIT 1"));
+ return latest == null || latest.getTrainingRound() == null ? 1 : latest.getTrainingRound() + 1;
+ }
+
+ private InstitutionIdentifierApplicationEntity toEntity(InstitutionIdentifierApplication application) {
+ InstitutionIdentifierApplicationEntity entity = new InstitutionIdentifierApplicationEntity();
+ entity.setId(application.getId());
+ entity.setBankCode(application.getBankCode());
+ entity.setRequestTimestamp(application.getTimestamp());
+ entity.setDigest(application.getDigest());
+ entity.setBankSignature(application.getBankSignature());
+ entity.setStatus(application.getStatus().name());
+ entity.setVerificationKeyId(application.getVerificationKeyId());
+ entity.setSignatureValid(application.getSignatureValid());
+ entity.setDigestMatches(application.getDigestMatches());
+ entity.setSigningKeyId(application.getSigningKeyId());
+ entity.setInstitutionIdentifier(application.getInstitutionIdentifier());
+ entity.setCentralBankSignature(application.getCentralBankSignature());
+ return entity;
+ }
+
+ private InstitutionIdentifierApplication toDomain(InstitutionIdentifierApplicationEntity entity) {
+ return InstitutionIdentifierApplication.restore(entity.getId(), entity.getBankCode(),
+ entity.getRequestTimestamp(), entity.getDigest(), entity.getBankSignature(),
+ InstitutionIdentifierApplication.Status.valueOf(entity.getStatus()), entity.getVerificationKeyId(),
+ entity.getSignatureValid(), entity.getDigestMatches(), entity.getSigningKeyId(),
+ entity.getInstitutionIdentifier(), entity.getCentralBankSignature());
+ }
+
+ private void insertLog(Long applicationId, String operation, String fromStatus, String toStatus,
+ String operationDetail, String operator) {
+ InstitutionIdentifierOperationLogEntity log = new InstitutionIdentifierOperationLogEntity();
+ log.setApplicationId(applicationId);
+ log.setOperation(operation);
+ log.setFromStatus(fromStatus);
+ log.setToStatus(toStatus);
+ log.setOperationDetail(operationDetail);
+ log.setCreatedAt(LocalDateTime.now());
+ log.setCreatedBy(operator);
+ operationLogMapper.insert(log);
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java
new file mode 100644
index 0000000..a991f9e
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java
@@ -0,0 +1,109 @@
+package com.yau.digitalrmb.institutionidentity.infrastructure;
+
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
+import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair;
+import org.bouncycastle.asn1.gm.GMNamedCurves;
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.digests.SM3Digest;
+import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
+import org.bouncycastle.crypto.params.ECDomainParameters;
+import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.crypto.params.ParametersWithID;
+import org.bouncycastle.crypto.params.ParametersWithRandom;
+import org.bouncycastle.crypto.signers.SM2Signer;
+import org.springframework.stereotype.Component;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+
+@Component
+public class SmInstitutionIdentityCryptography implements InstitutionIdentityCryptography {
+ private static final byte[] USER_ID = "1234567812345678".getBytes(StandardCharsets.US_ASCII);
+ private static final ECDomainParameters DOMAIN;
+
+ static {
+ X9ECParameters curve = GMNamedCurves.getByName("sm2p256v1");
+ DOMAIN = new ECDomainParameters(curve.getCurve(), curve.getG(), curve.getN(), curve.getH());
+ }
+
+ @Override
+ public String sm3(String text) {
+ byte[] input = text.getBytes(StandardCharsets.UTF_8);
+ SM3Digest digest = new SM3Digest();
+ digest.update(input, 0, input.length);
+ byte[] output = new byte[digest.getDigestSize()];
+ digest.doFinal(output, 0);
+ return hex(output);
+ }
+
+ @Override
+ public InstitutionSm2KeyPair generateSm2KeyPair() {
+ ECKeyPairGenerator generator = new ECKeyPairGenerator();
+ generator.init(new ECKeyGenerationParameters(DOMAIN, new SecureRandom()));
+ AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
+ ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
+ ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();
+ return new InstitutionSm2KeyPair(leftPad(privateKey.getD().toString(16).toUpperCase(), 64),
+ hex(publicKey.getQ().getEncoded(false)));
+ }
+
+ @Override
+ public String sign(String privateKey, String message) {
+ ECPrivateKeyParameters key = new ECPrivateKeyParameters(new BigInteger(privateKey, 16), DOMAIN);
+ SM2Signer signer = new SM2Signer();
+ signer.init(true, new ParametersWithID(new ParametersWithRandom(key, new SecureRandom()), USER_ID));
+ byte[] bytes = message.getBytes(StandardCharsets.US_ASCII);
+ signer.update(bytes, 0, bytes.length);
+ try {
+ return hex(signer.generateSignature());
+ } catch (Exception exception) {
+ throw new IllegalStateException("SM2签名计算失败", exception);
+ }
+ }
+
+ @Override
+ public boolean verify(String publicKey, String message, String signature) {
+ try {
+ ECPublicKeyParameters key = new ECPublicKeyParameters(DOMAIN.getCurve().decodePoint(fromHex(publicKey)),
+ DOMAIN);
+ SM2Signer signer = new SM2Signer();
+ signer.init(false, new ParametersWithID(key, USER_ID));
+ byte[] bytes = message.getBytes(StandardCharsets.US_ASCII);
+ signer.update(bytes, 0, bytes.length);
+ return signer.verifySignature(fromHex(signature));
+ } catch (RuntimeException exception) {
+ return false;
+ }
+ }
+
+ private static byte[] fromHex(String value) {
+ if (value == null || (value.length() & 1) == 1) {
+ throw new IllegalArgumentException("非法十六进制");
+ }
+ byte[] bytes = new byte[value.length() / 2];
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = (byte) Integer.parseInt(value.substring(i * 2, i * 2 + 2), 16);
+ }
+ return bytes;
+ }
+
+ private static String leftPad(String value, int length) {
+ StringBuilder result = new StringBuilder(length);
+ for (int i = value.length(); i < length; i++) {
+ result.append('0');
+ }
+ return result.append(value).toString();
+ }
+
+ private static String hex(byte[] bytes) {
+ StringBuilder result = new StringBuilder(bytes.length * 2);
+ for (byte value : bytes) {
+ result.append(String.format("%02X", value));
+ }
+ return result.toString();
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java
new file mode 100644
index 0000000..efd695e
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java
@@ -0,0 +1,28 @@
+package com.yau.digitalrmb.institutionidentity.interfaces.dto;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+public class ConfirmInstitutionIdentifierRequest {
+ @NotNull(message = "申请编号不能为空")
+ private Long applicationId;
+
+ @NotBlank(message = "签名密钥不能为空")
+ private String signingKeyId;
+
+ public Long getApplicationId() {
+ return applicationId;
+ }
+
+ public void setApplicationId(Long applicationId) {
+ this.applicationId = applicationId;
+ }
+
+ public String getSigningKeyId() {
+ return signingKeyId;
+ }
+
+ public void setSigningKeyId(String signingKeyId) {
+ this.signingKeyId = signingKeyId;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/GenerateInstitutionIdentifierRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/GenerateInstitutionIdentifierRequest.java
new file mode 100644
index 0000000..1401ae3
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/GenerateInstitutionIdentifierRequest.java
@@ -0,0 +1,12 @@
+package com.yau.digitalrmb.institutionidentity.interfaces.dto;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+
+public class GenerateInstitutionIdentifierRequest {
+ @NotBlank(message = "银行代码不能为空")
+ @Pattern(regexp = "[A-Za-z0-9]{8,32}", message = "银行代码必须为8至32位字母或数字")
+ private String bankCode;
+ public String getBankCode() { return bankCode; }
+ public void setBankCode(String bankCode) { this.bankCode = bankCode; }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java
new file mode 100644
index 0000000..1a829a1
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java
@@ -0,0 +1,26 @@
+package com.yau.digitalrmb.institutionidentity.interfaces.dto;
+
+import javax.validation.constraints.NotNull;
+
+public class InstitutionIdentifierStepRequest {
+ @NotNull(message = "申请编号不能为空")
+ private Long applicationId;
+
+ private String keyId;
+
+ public Long getApplicationId() {
+ return applicationId;
+ }
+
+ public void setApplicationId(Long applicationId) {
+ this.applicationId = applicationId;
+ }
+
+ public String getKeyId() {
+ return keyId;
+ }
+
+ public void setKeyId(String keyId) {
+ this.keyId = keyId;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java
new file mode 100644
index 0000000..209a473
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java
@@ -0,0 +1,28 @@
+package com.yau.digitalrmb.institutionidentity.interfaces.dto;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+public class VerifyInstitutionApplicationRequest {
+ @NotNull(message = "申请编号不能为空")
+ private Long applicationId;
+
+ @NotBlank(message = "验证密钥不能为空")
+ private String verificationKeyId;
+
+ public Long getApplicationId() {
+ return applicationId;
+ }
+
+ public void setApplicationId(Long applicationId) {
+ this.applicationId = applicationId;
+ }
+
+ public String getVerificationKeyId() {
+ return verificationKeyId;
+ }
+
+ public void setVerificationKeyId(String verificationKeyId) {
+ this.verificationKeyId = verificationKeyId;
+ }
+}
diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java
new file mode 100644
index 0000000..3bb23ad
--- /dev/null
+++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java
@@ -0,0 +1,136 @@
+package com.yau.digitalrmb.institutionidentity.interfaces.rest;
+
+import com.yau.digitalrmb.institutionidentity.application.InstitutionIdentifierStepService;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionApplicationVerificationResult;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionApplicationVerificationService;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionIdentifierConfirmationResult;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionInformationPreparationResult;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionDigestResult;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionSignatureResult;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionApplicationPackageResult;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResult;
+import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
+import com.yau.digitalrmb.institutionidentity.interfaces.dto.GenerateInstitutionIdentifierRequest;
+import com.yau.digitalrmb.institutionidentity.interfaces.dto.InstitutionIdentifierStepRequest;
+import com.yau.digitalrmb.institutionidentity.interfaces.dto.VerifyInstitutionApplicationRequest;
+import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmInstitutionIdentifierRequest;
+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.security.core.Authentication;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.web.bind.annotation.DeleteMapping;
+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")
+@Tag(name = "投放机构标识")
+public class InstitutionIdentifierController {
+ @Resource
+ private InstitutionIdentifierStepService stepService;
+
+ @Resource
+ private InstitutionKeyService keyService;
+
+ @Resource
+ private InstitutionApplicationVerificationService verificationService;
+
+ @PostMapping("/keys/commercial-bank")
+ @Operation(summary = "获取商业银行SM2密钥对",
+ description = "按Token中的userId、schoolId、classId生成或读取商业银行密钥对,私钥加密保存。")
+ public ApiResponse commercialBankKey(
+ @AuthenticationPrincipal Jwt jwt, Authentication authentication) {
+ return ApiResponse.success(keyService.commercialBankKey(InstitutionKeySubject.from(jwt),
+ authentication.getName()), traceId());
+ }
+
+ @DeleteMapping("/keys/commercial-bank")
+ @Operation(summary = "删除商业银行SM2密钥对",
+ description = "仅逻辑删除当前Token中的userId、schoolId、classId所对应的商业银行密钥。")
+ public ApiResponse deleteCommercialBankKey(
+ @AuthenticationPrincipal Jwt jwt, Authentication authentication) {
+ keyService.logicalDelete(InstitutionKeySubject.from(jwt), InstitutionKeyService.BANK_SECOND_KEY,
+ authentication.getName());
+ return ApiResponse.success(null, traceId());
+ }
+
+ @PostMapping("/keys/central-bank")
+ @Operation(summary = "获取中央银行SM2密钥对",
+ description = "按Token中的userId、schoolId、classId生成或读取中央银行密钥对,私钥加密保存。")
+ public ApiResponse centralBankKey(
+ @AuthenticationPrincipal Jwt jwt, Authentication authentication) {
+ return ApiResponse.success(keyService.centralBankKey(InstitutionKeySubject.from(jwt),
+ authentication.getName()), traceId());
+ }
+
+ @DeleteMapping("/keys/central-bank")
+ @Operation(summary = "删除中央银行SM2密钥对",
+ description = "仅逻辑删除当前Token中的userId、schoolId、classId所对应的中央银行密钥。")
+ public ApiResponse deleteCentralBankKey(
+ @AuthenticationPrincipal Jwt jwt, Authentication authentication) {
+ keyService.logicalDelete(InstitutionKeySubject.from(jwt), InstitutionKeyService.CENTRAL_FIRST_KEY,
+ authentication.getName());
+ return ApiResponse.success(null, traceId());
+ }
+
+ @PostMapping("/steps/prepare")
+ @Operation(summary = "创建申请并获取业务信息", description = "创建PREPARED状态申请并保存数据库,返回后续步骤使用的applicationId。")
+ public ApiResponse prepare(
+ @Valid @RequestBody GenerateInstitutionIdentifierRequest request, Authentication authentication) {
+ return ApiResponse.success(stepService.prepare(request.getBankCode(), authentication.getName()), traceId());
+ }
+
+ @PostMapping("/steps/digest")
+ @Operation(summary = "使用SM3计算摘要", description = "仅PREPARED状态可操作,完成后保存摘要并进入DIGESTED状态。")
+ public ApiResponse digest(
+ @Valid @RequestBody InstitutionIdentifierStepRequest request, Authentication authentication) {
+ return ApiResponse.success(stepService.digest(request.getApplicationId(), authentication.getName()), traceId());
+ }
+
+ @PostMapping("/steps/sign")
+ @Operation(summary = "使用商业银行第二私钥进行SM2签名", description = "仅DIGESTED状态可操作,完成后保存签名并进入SIGNED状态。")
+ public ApiResponse sign(
+ @Valid @RequestBody InstitutionIdentifierStepRequest request,
+ @AuthenticationPrincipal Jwt jwt, Authentication authentication) {
+ return ApiResponse.success(stepService.sign(request.getApplicationId(), request.getKeyId(),
+ InstitutionKeySubject.from(jwt), authentication.getName()), traceId());
+ }
+
+ @PostMapping("/steps/package")
+ @Operation(summary = "组装机构标识申请", description = "仅SIGNED状态可操作,完成后进入PACKAGED待央行验证状态。")
+ public ApiResponse packageApplication(
+ @Valid @RequestBody InstitutionIdentifierStepRequest request, Authentication authentication) {
+ return ApiResponse.success(stepService.packageApplication(request.getApplicationId(), authentication.getName()),
+ traceId());
+ }
+
+ @PostMapping("/applications/verification")
+ @Operation(summary = "查看并保存机构标识申请验证过程", description = "仅PACKAGED状态可操作;保存SM2验签和SM3完整性验证结果。")
+ public ApiResponse verifyApplication(
+ @Valid @RequestBody VerifyInstitutionApplicationRequest request,
+ @AuthenticationPrincipal Jwt jwt, Authentication authentication) {
+ return ApiResponse.success(verificationService.verify(request.getApplicationId(),
+ request.getVerificationKeyId(), InstitutionKeySubject.from(jwt), authentication.getName()), traceId());
+ }
+
+ @PostMapping("/applications/confirmation")
+ @Operation(summary = "央行签名确权并组装机构标识", description = "仅VERIFIED状态可操作;保存央行签名和机构标识并进入ISSUED状态。")
+ public ApiResponse confirmIdentifier(
+ @Valid @RequestBody ConfirmInstitutionIdentifierRequest request,
+ @AuthenticationPrincipal Jwt jwt, Authentication authentication) {
+ return ApiResponse.success(verificationService.confirm(request.getApplicationId(), request.getSigningKeyId(),
+ InstitutionKeySubject.from(jwt), authentication.getName()), traceId());
+ }
+
+ private String traceId() { return MDC.get(TraceIdFilter.MDC_KEY); }
+}
diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java b/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java
index 23098d8..e603f4e 100644
--- a/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java
+++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/CasTicketValidator.java
@@ -4,6 +4,7 @@ import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperti
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@@ -21,6 +22,7 @@ import java.io.StringReader;
import java.net.URI;
@Component
+@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
public class CasTicketValidator {
private final PlatformIntegrationProperties.Cas properties;
private final RestTemplate restTemplate;
diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java
index 921b0f7..efbc87c 100644
--- a/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java
+++ b/src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties;
import com.yau.digitalrmb.platformintegration.domain.PlatformActor;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
@@ -17,6 +18,7 @@ import java.time.Instant;
import java.util.Base64;
@Component
+@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
public class PlatformTokenVerifier {
private static final Duration CLOCK_SKEW = Duration.ofSeconds(30);
diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java
index 9a6a282..eb9fd76 100644
--- a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java
+++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/JdbcPlatformIdentityRepository.java
@@ -4,6 +4,7 @@ import com.yau.digitalrmb.platformintegration.application.PlatformIdentityReposi
import com.yau.digitalrmb.platformintegration.domain.PlatformActor;
import com.yau.digitalrmb.platformintegration.domain.PlatformRole;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
@@ -19,6 +20,7 @@ import java.util.ArrayList;
import java.util.Optional;
@Repository
+@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
public class JdbcPlatformIdentityRepository implements PlatformIdentityRepository {
private static final String TEACHER_QUERY =
"SELECT cu.ID, cu.CODE, cu.NAME, t.teacher_id AS profile_id, t.add_time AS signing_time "
diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java
index c7d3459..abd1069 100644
--- a/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java
+++ b/src/main/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfig.java
@@ -4,6 +4,8 @@ import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@@ -11,6 +13,8 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import javax.sql.DataSource;
@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(PlatformIntegrationProperties.class)
+@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
public class PlatformReadOnlyDataSourceConfig {
@Bean(name = "platformReadOnlyDataSource", destroyMethod = "close")
public HikariDataSource platformReadOnlyDataSource(PlatformIntegrationProperties properties) {
diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java
index 9e5a2ae..aac5ff8 100644
--- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java
+++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/CasAuthenticationController.java
@@ -8,6 +8,7 @@ import com.yau.digitalrmb.platformintegration.domain.PlatformActor;
import com.yau.digitalrmb.security.application.LoginExchangeCodeService;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
@RestController
+@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
@RequestMapping("/api/v1/auth/cas")
public class CasAuthenticationController {
private final CasTicketValidator ticketValidator;
diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java
index 60f464e..3879d14 100644
--- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java
+++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java
@@ -5,6 +5,7 @@ import com.yau.digitalrmb.platformintegration.application.PlatformTokenVerifier;
import com.yau.digitalrmb.platformintegration.application.VerifiedPlatformToken;
import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties;
import com.yau.digitalrmb.security.application.LoginExchangeCodeService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
@RestController
+@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
@RequestMapping("/api/v1/auth")
public class PlatformSsoController {
private final PlatformTokenVerifier tokenVerifier;
diff --git a/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java b/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java
index ef35c26..c3da4ff 100644
--- a/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java
+++ b/src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java
@@ -35,17 +35,28 @@ public class JwtTokenService {
}
public Token issueFor(long platformUserId, String account, Set roles) {
+ return issueFor(platformUserId, null, null, account, roles);
+ }
+
+ public Token issueFor(long userId, Long schoolId, Long classId, String account, Set roles) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(properties.getJwt().getAccessTokenTtl());
List authorities = roles.stream().sorted().map(role -> "ROLE_" + role).collect(Collectors.toList());
- JwtClaimsSet claims = JwtClaimsSet.builder()
- .subject(String.valueOf(platformUserId))
+ JwtClaimsSet.Builder claims = JwtClaimsSet.builder()
+ .subject(String.valueOf(userId))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
+ .claim("userId", userId)
.claim("preferred_username", account)
- .claim("roles", authorities)
- .build();
- Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), claims));
+ .claim("roles", authorities);
+ if (schoolId != null) {
+ claims.claim("schoolId", schoolId);
+ }
+ if (classId != null) {
+ claims.claim("classId", classId);
+ }
+ Jwt jwt = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(),
+ claims.build()));
return new Token(jwt.getTokenValue(), properties.getJwt().getAccessTokenTtl().getSeconds());
}
diff --git a/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java b/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java
index f52fb6f..6ed89c3 100644
--- a/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java
+++ b/src/main/java/com/yau/digitalrmb/shared/web/GlobalExceptionHandler.java
@@ -9,6 +9,7 @@ import org.slf4j.MDC;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@@ -26,8 +27,9 @@ public class GlobalExceptionHandler {
}
@ExceptionHandler(BusinessException.class)
- public ApiResponse handleBusiness(BusinessException exception) {
- return ApiResponse.failure(exception.getErrorCode(), exception.getMessage(), traceId());
+ public ResponseEntity> handleBusiness(BusinessException exception) {
+ ApiResponse response = ApiResponse.failure(exception.getErrorCode(), exception.getMessage(), traceId());
+ return ResponseEntity.status(httpStatus(exception.getErrorCode())).body(response);
}
@ExceptionHandler(PlatformTokenException.class)
@@ -43,6 +45,21 @@ public class GlobalExceptionHandler {
return ApiResponse.failure(ErrorCode.INTERNAL_ERROR, "系统内部错误", traceId());
}
+ private static HttpStatus httpStatus(ErrorCode errorCode) {
+ switch (errorCode) {
+ case VALIDATION_ERROR:
+ return HttpStatus.BAD_REQUEST;
+ case UNAUTHORIZED:
+ return HttpStatus.UNAUTHORIZED;
+ case FORBIDDEN:
+ return HttpStatus.FORBIDDEN;
+ case RESOURCE_NOT_FOUND:
+ return HttpStatus.NOT_FOUND;
+ default:
+ return HttpStatus.INTERNAL_SERVER_ERROR;
+ }
+ }
+
private String traceId() {
String traceId = MDC.get(TraceIdFilter.MDC_KEY);
return traceId == null ? UUID.randomUUID().toString() : traceId;
diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml
index 487192c..1bbcd72 100644
--- a/src/main/resources/application-test.yml
+++ b/src/main/resources/application-test.yml
@@ -9,6 +9,7 @@ security:
secret: 0123456789012345678901234567890123456789012345678901234567890123
access-token-ttl: PT30M
platform-integration:
+ enabled: true
datasource:
url: jdbc:h2:mem:platform;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE
username: sa
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 199be9a..e6342f7 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,4 +1,6 @@
spring:
+ profiles:
+ active: dev
application:
name: digital-rmb-backend
datasource:
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index b749a73..4d5165d 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -51,6 +51,39 @@ CREATE TABLE IF NOT EXISTS auth_refresh_token (
revoked_at TIMESTAMP NULL
);
+CREATE TABLE IF NOT EXISTS institution_sm2_key (
+ id BIGINT PRIMARY KEY,
+ key_id VARCHAR(64) NOT NULL,
+ key_name VARCHAR(64) NOT NULL,
+ key_owner VARCHAR(32) NOT NULL,
+ key_purpose VARCHAR(32) NOT NULL,
+ user_id BIGINT NOT NULL,
+ school_id BIGINT NOT NULL,
+ class_id BIGINT NOT NULL,
+ public_key VARCHAR(256) NOT NULL,
+ encrypted_private_key VARCHAR(512) NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+ created_by VARCHAR(64) NOT NULL,
+ updated_by VARCHAR(64) NOT NULL,
+ deleted BOOLEAN NOT NULL DEFAULT FALSE
+);
+
+CREATE TABLE IF NOT EXISTS institution_sm2_key_audit (
+ id BIGINT PRIMARY KEY,
+ key_record_id BIGINT NOT NULL,
+ key_id VARCHAR(64) NOT NULL,
+ operation VARCHAR(32) NOT NULL,
+ user_id BIGINT NOT NULL,
+ school_id BIGINT NOT NULL,
+ class_id BIGINT NOT NULL,
+ operation_detail VARCHAR(512) NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ created_by VARCHAR(64) NOT NULL,
+ CONSTRAINT fk_institution_key_audit_key
+ FOREIGN KEY (key_record_id) REFERENCES institution_sm2_key(id)
+);
+
INSERT IGNORE INTO sys_role (id, name, created_at, updated_at, created_by, updated_by, deleted)
VALUES (1001, 'TEACHER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'SYSTEM', 'SYSTEM', FALSE),
(1002, 'STUDENT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'SYSTEM', 'SYSTEM', FALSE);
@@ -67,3 +100,43 @@ ON DUPLICATE KEY UPDATE account = VALUES(account), display_name = VALUES(display
source_updated_at = CURRENT_TIMESTAMP, synced_at = CURRENT_TIMESTAMP;
INSERT IGNORE INTO sys_user_role (user_id, role_id) VALUES (487, 1002);
+--
+-- CREATE TABLE IF NOT EXISTS institution_identifier_application (
+-- id BIGINT PRIMARY KEY,
+-- bank_code VARCHAR(32) NOT NULL,
+-- request_timestamp CHAR(14) NOT NULL,
+-- digest CHAR(64),
+-- bank_signature VARCHAR(512),
+-- status VARCHAR(32) NOT NULL,
+-- verification_key_id VARCHAR(64),
+-- signature_valid BOOLEAN,
+-- digest_matches BOOLEAN,
+-- signing_key_id VARCHAR(64),
+-- institution_identifier VARCHAR(32),
+-- central_bank_signature VARCHAR(512),
+-- created_at TIMESTAMP NOT NULL,
+-- updated_at TIMESTAMP NOT NULL,
+-- created_by VARCHAR(64) NOT NULL,
+-- updated_by VARCHAR(64) NOT NULL,
+-- deleted BOOLEAN NOT NULL DEFAULT FALSE,
+-- CONSTRAINT uk_institution_identifier_bank UNIQUE (bank_code)
+-- );
+--
+-- CREATE INDEX IF NOT EXISTS idx_institution_identifier_status
+-- ON institution_identifier_application(status);
+--
+-- CREATE TABLE IF NOT EXISTS institution_identifier_operation_log (
+-- id BIGINT PRIMARY KEY,
+-- application_id BIGINT NOT NULL,
+-- operation VARCHAR(32) NOT NULL,
+-- from_status VARCHAR(32),
+-- to_status VARCHAR(32) NOT NULL,
+-- operation_detail VARCHAR(1024) NOT NULL,
+-- created_at TIMESTAMP NOT NULL,
+-- created_by VARCHAR(64) NOT NULL,
+-- CONSTRAINT fk_identifier_operation_application
+-- FOREIGN KEY (application_id) REFERENCES institution_identifier_application(id)
+-- );
+--
+-- CREATE INDEX IF NOT EXISTS idx_identifier_operation_application
+-- ON institution_identifier_operation_log(application_id);
diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java
index c30412a..6d0f3f3 100644
--- a/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java
+++ b/src/test/java/com/yau/digitalrmb/platformintegration/infrastructure/PlatformReadOnlyDataSourceConfigTest.java
@@ -15,6 +15,7 @@ class PlatformReadOnlyDataSourceConfigTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(PropertiesConfiguration.class, PlatformReadOnlyDataSourceConfig.class))
.withPropertyValues(
+ "platform-integration.enabled=true",
"platform-integration.datasource.url=jdbc:h2:mem:platform_config",
"platform-integration.datasource.username=sa",
"platform-integration.datasource.password=test-password",
From 1a65c4d426a87dcf46ee3d1bd1ae2ac34de0ff86 Mon Sep 17 00:00:00 2001
From: "jiazheng.zhao" <2624533894@qq.com>
Date: Tue, 4 Aug 2026 11:24:28 +0800
Subject: [PATCH 2/2] fix: remove merge conflict markers
---
.../interfaces/PlatformSsoController.java | 5 -----
src/main/resources/application-test.yml | 8 --------
2 files changed, 13 deletions(-)
diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java
index 196ed9d..10be3ba 100644
--- a/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java
+++ b/src/main/java/com/yau/digitalrmb/platformintegration/interfaces/PlatformSsoController.java
@@ -4,13 +4,8 @@ import com.yau.digitalrmb.identity.application.LocalSsoAccountService;
import com.yau.digitalrmb.platformintegration.application.PlatformTokenVerifier;
import com.yau.digitalrmb.platformintegration.application.VerifiedPlatformToken;
import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties;
-<<<<<<< HEAD
-import com.yau.digitalrmb.security.application.LoginExchangeCodeService;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-=======
import com.yau.digitalrmb.security.application.JwtTokenService;
import io.swagger.v3.oas.annotations.Operation;
->>>>>>> origin/master
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml
index 2316a2e..36a3a99 100644
--- a/src/main/resources/application-test.yml
+++ b/src/main/resources/application-test.yml
@@ -9,14 +9,6 @@ security:
secret: 0123456789012345678901234567890123456789012345678901234567890123
access-token-ttl: PT30M
platform-integration:
-<<<<<<< HEAD
- enabled: true
- datasource:
- url: jdbc:h2:mem:platform;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE
- username: sa
- password: test-password
-=======
->>>>>>> origin/master
token:
link-secret-key: local-token-sso-test-secret-key-123456
frontend: