# Conflicts:
#	src/main/java/com/yau/digitalrmb/security/application/JwtTokenService.java
#	src/main/resources/schema.sql
master
chenyuan 4 weeks ago
commit e26c9a14bb

@ -25,6 +25,7 @@
<maven.compiler.target>1.8</maven.compiler.target>
<mybatis-plus.version>3.5.17</mybatis-plus.version>
<springdoc.version>1.8.0</springdoc.version>
<bouncycastle.version>1.70</bouncycastle.version>
</properties>
<dependencies>
@ -78,6 +79,11 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>

@ -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;
}
}

@ -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; }
}

@ -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());
}
}
}

@ -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;
}
}

@ -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; }
}

@ -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);
}
}

@ -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;
}
}

@ -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; }
}

@ -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<InstitutionSm2KeyEntity> update = new LambdaUpdateWrapper<InstitutionSm2KeyEntity>()
.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<InstitutionSm2KeyEntity>()
.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);
}
}

@ -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; }
}

@ -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;
}
}

@ -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;
}
}

@ -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<InstitutionIdentifierApplicationEntity> increment =
new LambdaUpdateWrapper<InstitutionIdentifierApplicationEntity>()
.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;
}
}

@ -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; }
}

@ -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<InstitutionIdentifierApplication> findById(long applicationId);
void update(InstitutionIdentifierApplication application,
InstitutionIdentifierApplication.Status expectedStatus,
String operation,
String operationDetail,
String operator);
int nextTrainingRound(String bankCode, String operator);
}

@ -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);
}

@ -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; }
}

@ -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;
}

@ -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<InstitutionIdentifierApplicationEntity> { }

@ -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;
}

@ -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<InstitutionIdentifierOperationLogEntity> {
}

@ -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;
}

@ -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<InstitutionIdentifierTrainingErrorEntity> {
}

@ -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);
}
}
}

@ -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;
}

@ -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<InstitutionSm2KeyAuditEntity> {
}

@ -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;
}

@ -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<InstitutionSm2KeyEntity> {
}

@ -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<InstitutionIdentifierApplication> findById(long applicationId) {
InstitutionIdentifierApplicationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<InstitutionIdentifierApplicationEntity>()
.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<InstitutionIdentifierApplicationEntity> update =
new LambdaUpdateWrapper<InstitutionIdentifierApplicationEntity>()
.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<InstitutionIdentifierApplicationEntity>()
.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);
}
}

@ -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();
}
}

@ -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;
}
}

@ -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; }
}

@ -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;
}
}

@ -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;
}
}

@ -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<InstitutionKeyPairResult> 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<Void> 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<InstitutionKeyPairResult> 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<Void> 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<InstitutionInformationPreparationResult> 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<InstitutionDigestResult> 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<InstitutionSignatureResult> 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<InstitutionApplicationPackageResult> 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<InstitutionApplicationVerificationResult> 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<InstitutionIdentifierConfirmationResult> 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); }
}

@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
@ -15,6 +16,7 @@ import java.time.Instant;
import java.util.Base64;
@Component
@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
public class PlatformTokenVerifier {
private final PlatformIntegrationProperties.Token properties;
private final ObjectMapper objectMapper;

@ -6,6 +6,7 @@ import com.yau.digitalrmb.platformintegration.application.VerifiedPlatformToken;
import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties;
import com.yau.digitalrmb.security.application.JwtTokenService;
import io.swagger.v3.oas.annotations.Operation;
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.util.UriComponentsBuilder;
import java.util.Collections;
@RestController
@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
@RequestMapping("/api/v1/auth")
public class PlatformSsoController {
private final PlatformTokenVerifier tokenVerifier;

@ -35,18 +35,28 @@ public class JwtTokenService {
}
public Token issueFor(long platformUserId, String account, Set<String> roles) {
return issueFor(platformUserId, null, null, account, roles);
}
public Token issueFor(long userId, Long schoolId, Long classId, String account, Set<String> roles) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(properties.getJwt().getAccessTokenTtl());
List<String> 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", platformUserId)
.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());
}

@ -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<Void> handleBusiness(BusinessException exception) {
return ApiResponse.failure(exception.getErrorCode(), exception.getMessage(), traceId());
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException exception) {
ApiResponse<Void> 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;

@ -61,6 +61,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);
@ -128,3 +161,43 @@ VALUES ('BKCHCNBJ00001', 49950000.00, 50000000.00, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE current_balance = VALUES(current_balance),
warning_threshold = VALUES(warning_threshold),
updated_at = CURRENT_TIMESTAMP;
--
-- 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);

Loading…
Cancel
Save