From f92cb83566e1d8aad7f89671b5f53022b75e7725 Mon Sep 17 00:00:00 2001 From: "jiazheng.zhao" <2624533894@qq.com> Date: Wed, 5 Aug 2026 19:32:00 +0800 Subject: [PATCH] =?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 --- README.md | 4 +- pom.xml | 8 +- .../yau/digitalrmb/DigitalRmbApplication.java | 3 - ...ercialBankInstitutionIdentifierResult.java | 29 ++ .../CurrencyGenerationRequestService.java | 3 + ...CurrencyGenerationVerificationService.java | 2 + ...itutionApplicationVerificationService.java | 42 +- .../InstitutionIdentifierFeedbackService.java | 78 ++++ .../InstitutionIdentifierFinalResult.java | 32 ++ .../InstitutionIdentifierStepService.java | 16 +- .../application/InstitutionKeyService.java | 18 + .../InstitutionTrainingErrorRecorder.java | 4 + .../InstitutionTrainingScoreService.java | 117 +++++ ...suedInstitutionIdentifierQueryService.java | 10 +- ...ransactionInformationIdentifierResult.java | 48 ++ ...ansactionInformationIdentifierService.java | 175 +++++++ .../InstitutionIdentifierApplication.java | 8 +- .../InstitutionIdentifierRepository.java | 4 + .../TransactionInformationIdentifier.java | 109 +++++ ...actionInformationIdentifierRepository.java | 24 + ...cyGenerationRequestOperationLogEntity.java | 2 + ...erationVerificationOperationLogEntity.java | 2 + ...stitutionIdentifierOperationLogEntity.java | 3 + ...titutionIdentifierTrainingErrorEntity.java | 3 + .../InstitutionSm2KeyAuditEntity.java | 3 + ...ybatisInstitutionIdentifierRepository.java | 15 + ...actionInformationIdentifierRepository.java | 152 ++++++ .../StuModuleScoreDetailsEntity.java | 44 ++ .../StuModuleScoreDetailsMapper.java | 8 + ...ransactionInformationIdentifierEntity.java | 28 ++ ...ransactionInformationIdentifierMapper.java | 8 + ...formationIdentifierOperationLogEntity.java | 28 ++ ...formationIdentifierOperationLogMapper.java | 9 + .../ConfirmInstitutionIdentifierRequest.java | 9 +- .../dto/InstitutionIdentifierStepRequest.java | 9 +- .../VerifyInstitutionApplicationRequest.java | 8 +- .../CurrencyGenerationRequestController.java | 2 +- .../rest/InstitutionIdentifierController.java | 49 +- ...actionInformationIdentifierController.java | 59 +++ .../config/PlatformIntegrationConfig.java | 11 + .../security/config/SecurityConfig.java | 3 +- .../shared/config/OpenApiConfig.java | 9 +- .../shared/config/SwaggerStartupLogger.java | 4 +- src/main/resources/application-dev.yml | 2 + src/main/resources/application.yml | 9 +- .../institution_identifier_training.sql | 74 +++ ...tution_training_logical_delete_upgrade.sql | 23 + ...saction_information_identifier_upgrade.sql | 49 ++ src/main/resources/schema.sql | 17 + .../InstitutionIdentifierControllerTest.java | 440 ++++++++++++++---- ...onInformationIdentifierControllerTest.java | 266 +++++++++++ .../config/PlatformIntegrationConfigTest.java | 31 ++ .../shared/SwaggerDocumentationTest.java | 43 ++ src/test/resources/schema.sql | 17 + 54 files changed, 2015 insertions(+), 158 deletions(-) create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/CommercialBankInstitutionIdentifierResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierFeedbackService.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierFinalResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingScoreService.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/TransactionInformationIdentifierResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/TransactionInformationIdentifierService.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifier.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifierRepository.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisTransactionInformationIdentifierRepository.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/StuModuleScoreDetailsEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/StuModuleScoreDetailsMapper.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierMapper.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierOperationLogEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierOperationLogMapper.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/TransactionInformationIdentifierController.java create mode 100644 src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationConfig.java create mode 100644 src/main/resources/db/manual/institution_training_logical_delete_upgrade.sql create mode 100644 src/main/resources/db/manual/transaction_information_identifier_upgrade.sql create mode 100644 src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdentifierControllerTest.java create mode 100644 src/test/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationConfigTest.java create mode 100644 src/test/java/com/yau/digitalrmb/shared/SwaggerDocumentationTest.java diff --git a/README.md b/README.md index 23586d6..95bc349 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## 运行环境 - JDK 8(已按 Temurin 8 验证) -- Spring Boot 2.7.18、Spring Security 5.7、MyBatis-Plus 3.5.17、Springdoc 1.8.0 +- Spring Boot 2.7.18、Spring Security 5.7、MyBatis-Plus 3.5.17、Knife4j 4.4.0 - MySQL 8 ## 登录方式 @@ -47,7 +47,7 @@ Token 单点登录中的平台 `userId` 保存为 `sys_user.zy_user_id`;本地 mvn spring-boot:run -Dspring-boot.run.profiles=local -Dspring-boot.run.arguments=--server.port=8081 ``` -本地 Swagger: +本地 Swagger: 测试环境会执行 `schema.sql`,创建或更新测试账号 `tzs001`。`dev` 配置的 `spring.sql.init.mode` 为 `never`,不会自动修改真实 MySQL;开发库必须已有 diff --git a/pom.xml b/pom.xml index b0e4355..ed31665 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 1.8 1.8 3.5.17 - 1.8.0 + 4.4.0 1.70 @@ -63,9 +63,9 @@ runtime - org.springdoc - springdoc-openapi-ui - ${springdoc.version} + com.github.xiaoymin + knife4j-openapi3-spring-boot-starter + ${knife4j.version} org.projectlombok diff --git a/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java b/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java index e059a89..93d0329 100644 --- a/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java +++ b/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java @@ -2,13 +2,10 @@ package com.yau.digitalrmb; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.scheduling.annotation.EnableScheduling; -import com.yau.digitalrmb.platformintegration.config.PlatformIntegrationProperties; @SpringBootApplication @EnableScheduling -@EnableConfigurationProperties(PlatformIntegrationProperties.class) public class DigitalRmbApplication { public static void main(String[] args) { diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/CommercialBankInstitutionIdentifierResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CommercialBankInstitutionIdentifierResult.java new file mode 100644 index 0000000..1035495 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CommercialBankInstitutionIdentifierResult.java @@ -0,0 +1,29 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication; + +public class CommercialBankInstitutionIdentifierResult { + private final long applicationId; + private final String applicationStatus; + private final int status; + private final String statusText; + private final String institutionIdentifier; + private final String fullInstitutionIdentifier; + + public CommercialBankInstitutionIdentifierResult(InstitutionIdentifierApplication application) { + boolean feedbacked = application.getStatus() == InstitutionIdentifierApplication.Status.FEEDBACKED; + this.applicationId = application.getId(); + this.applicationStatus = application.getStatus().name(); + this.status = feedbacked ? 2 : 1; + this.statusText = feedbacked ? "已确认" : "待确认"; + this.institutionIdentifier = feedbacked ? application.getInstitutionIdentifier() : null; + this.fullInstitutionIdentifier = feedbacked ? application.getDigest() : null; + } + + public long getApplicationId() { return applicationId; } + public String getApplicationStatus() { return applicationStatus; } + public int getStatus() { return status; } + public String getStatusText() { return statusText; } + public String getInstitutionIdentifier() { return institutionIdentifier; } + public String getFullInstitutionIdentifier() { return fullInstitutionIdentifier; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationRequestService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationRequestService.java index 0d9e838..fc96b7a 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationRequestService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationRequestService.java @@ -31,6 +31,8 @@ public class CurrencyGenerationRequestService { private InstitutionIdentityCryptography cryptography; @Resource private InstitutionKeyService keyService; + @Resource + private InstitutionTrainingScoreService scoreService; private final Clock clock = Clock.systemUTC(); @@ -108,6 +110,7 @@ public class CurrencyGenerationRequestService { expected = request.getStatus(); invokeDomain(request::receive); repository.update(request, expected, "RECEIVE", "央行成功接收货币生成请求", operator); + scoreService.markStepCompleted(subject.getUserId(), 2); return new CurrencyRequestResult(request); } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationVerificationService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationVerificationService.java index 64a8b85..00d15e4 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationVerificationService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationVerificationService.java @@ -25,6 +25,7 @@ public class CurrencyGenerationVerificationService { @Resource private CurrencyGenerationRequestService requestService; @Resource private InstitutionIdentityCryptography cryptography; @Resource private InstitutionKeyService keyService; + @Resource private InstitutionTrainingScoreService scoreService; @Transactional public CurrencyGenerationVerificationResult receive(long requestId, InstitutionKeySubject subject, String operator) { @@ -95,6 +96,7 @@ public class CurrencyGenerationVerificationService { CurrencyGenerationVerification.Status expected = value.getStatus(); invoke(value::returnResponse); repository.update(value, expected, "RETURN_RESPONSE", "向商业银行返回确认报文", operator); + scoreService.markStepCompleted(subject.getUserId(), 3); return new CurrencyGenerationVerificationResult(value); } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java index 4c8ec7d..fbd94c8 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionApplicationVerificationService.java @@ -25,15 +25,15 @@ public class InstitutionApplicationVerificationService { private InstitutionTrainingErrorRecorder errorRecorder; @Transactional - public InstitutionApplicationVerificationResult verify(String verificationKeyId, - InstitutionKeySubject subject, String operator) { + public InstitutionApplicationVerificationResult verify(String keyId, InstitutionKeySubject subject, + String operator) { InstitutionIdentifierApplication application = requireCurrent(subject, InstitutionIdentifierApplication.Status.PACKAGED, "当前没有待验证的本轮机构标识数据"); - long applicationId = application.getId(); - if (!InstitutionKeyService.BANK_SECOND_KEY.equals(verificationKeyId)) { - recordAndThrow(application, "WRONG_KEY", "VERIFY", verificationKeyId, - "验证必须选择商业银行第二密钥的公钥", operator); + if (!InstitutionKeyService.BANK_SECOND_KEY.equals(keyId)) { + recordAndThrow(application, "WRONG_PUBLIC_KEY", "DECRYPT_VERIFY", keyId, + "解密验证必须选择商业银行第二公钥", operator); } + long applicationId = application.getId(); requireStatus(application, InstitutionIdentifierApplication.Status.PACKAGED, "当前步骤不是央行验证,不能操作", operator); InstitutionIdentifierApplication.Status expectedStatus = application.getStatus(); @@ -42,36 +42,42 @@ public class InstitutionApplicationVerificationService { 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完整性验证通过" : "申请验证失败", + invokeDomain(() -> application.recordVerification( + InstitutionKeyService.BANK_SECOND_KEY, signatureValid, digestMatches)); + repository.update(application, expectedStatus, "DECRYPT_VERIFY", + signatureValid && digestMatches + ? "使用商业银行第二公钥完成SM2解密验证,身份真实性和数据完整性验证通过" + : "央行解密验证失败", operator); return new InstitutionApplicationVerificationResult(applicationId, application.getStatus().name(), - "SM2_WITH_SM3_VERIFY_AND_SM3_RECALCULATE", verificationKeyId, originalText, + "SM2_WITH_SM3_VERIFY_AND_SM3_RECALCULATE", InstitutionKeyService.BANK_SECOND_KEY, originalText, application.getDigest(), recalculatedDigest, signatureValid, digestMatches); } @Transactional - public InstitutionIdentifierConfirmationResult confirm(String signingKeyId, + public InstitutionIdentifierConfirmationResult confirm(String providedPrivateKey, InstitutionKeySubject subject, String operator) { InstitutionIdentifierApplication application = requireCurrent(subject, InstitutionIdentifierApplication.Status.VERIFIED, "当前没有待确权的本轮机构标识数据"); long applicationId = application.getId(); - 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)); + String centralBankSignature = providedPrivateKey == null + ? keyService.signCentralBank(subject, digest) + : keyService.signCentralBank(subject, providedPrivateKey, digest); + if (centralBankSignature == null) { + recordAndThrow(application, "WRONG_PRIVATE_KEY", "CONFIRM", null, + "签名确权必须使用中央银行第一私钥", operator); + } + invokeDomain(() -> application.issue( + InstitutionKeyService.CENTRAL_FIRST_KEY, identifier, centralBankSignature)); repository.update(application, expectedStatus, "CONFIRM", "中央银行第一私钥完成SM2签名确权并签发机构标识", operator); return new InstitutionIdentifierConfirmationResult(applicationId, application.getStatus().name(), - signingKeyId, centralBankSignature, identifier, digest); + InstitutionKeyService.CENTRAL_FIRST_KEY, centralBankSignature, identifier, digest); } private InstitutionIdentifierApplication requireCurrent(InstitutionKeySubject subject, diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierFeedbackService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierFeedbackService.java new file mode 100644 index 0000000..5ae146b --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierFeedbackService.java @@ -0,0 +1,78 @@ +package com.yau.digitalrmb.institutionidentity.application; + +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.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; + +@Service +public class InstitutionIdentifierFeedbackService { + @Resource + private InstitutionIdentifierRepository repository; + + @Resource + private InstitutionTrainingScoreService scoreService; + + @Transactional(readOnly = true) + public InstitutionIdentifierFinalResult finalIdentifier(InstitutionKeySubject subject) { + InstitutionIdentifierApplication application = requireLatest(subject); + if (application.getStatus() != InstitutionIdentifierApplication.Status.ISSUED + && application.getStatus() != InstitutionIdentifierApplication.Status.FEEDBACKED) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + "央行尚未完成签名确权,不能查看最终机构标识,当前状态:" + + application.getStatus().name()); + } + return new InstitutionIdentifierFinalResult(application); + } + + @Transactional + public InstitutionIdentifierFinalResult feedback(InstitutionKeySubject subject, String operator) { + InstitutionIdentifierApplication application = requireLatest(subject); + if (application.getStatus() != InstitutionIdentifierApplication.Status.ISSUED) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + "当前没有待反馈的最终机构标识,当前状态:" + application.getStatus().name()); + } + InstitutionIdentifierApplication.Status expectedStatus = application.getStatus(); + invokeDomain(application::feedback); + repository.update(application, expectedStatus, "FEEDBACK", + "央行将最终机构标识同步反馈到商业银行端", operator); + scoreService.markStepCompleted(subject.getUserId(), 1); + return new InstitutionIdentifierFinalResult(application); + } + + @Transactional(readOnly = true) + public CommercialBankInstitutionIdentifierResult commercialBankResult(InstitutionKeySubject subject) { + InstitutionIdentifierApplication application = requireLatest(subject); + if (!isVisibleToCommercialBank(application.getStatus())) { + throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, + "商业银行第二私钥签名成功后才会出现待确认数据"); + } + return new CommercialBankInstitutionIdentifierResult(application); + } + + private InstitutionIdentifierApplication requireLatest(InstitutionKeySubject subject) { + return repository.findLatestBySubject(subject.getUserId(), subject.getSchoolId(), subject.getClassId()) + .orElseThrow(() -> new BusinessException( + ErrorCode.RESOURCE_NOT_FOUND, "当前没有机构标识申请数据")); + } + + private static boolean isVisibleToCommercialBank(InstitutionIdentifierApplication.Status status) { + return status == InstitutionIdentifierApplication.Status.SIGNED + || status == InstitutionIdentifierApplication.Status.PACKAGED + || status == InstitutionIdentifierApplication.Status.VERIFIED + || status == InstitutionIdentifierApplication.Status.ISSUED + || status == InstitutionIdentifierApplication.Status.FEEDBACKED; + } + + 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/InstitutionIdentifierFinalResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierFinalResult.java new file mode 100644 index 0000000..62ea21c --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierFinalResult.java @@ -0,0 +1,32 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication; + +public class InstitutionIdentifierFinalResult { + private final long applicationId; + private final String applicationStatus; + private final int feedbackStatus; + private final String feedbackStatusText; + private final String institutionIdentifier; + private final String fullInstitutionIdentifier; + private final String centralBankSignature; + + public InstitutionIdentifierFinalResult(InstitutionIdentifierApplication application) { + boolean feedbacked = application.getStatus() == InstitutionIdentifierApplication.Status.FEEDBACKED; + this.applicationId = application.getId(); + this.applicationStatus = application.getStatus().name(); + this.feedbackStatus = feedbacked ? 2 : 1; + this.feedbackStatusText = feedbacked ? "已反馈" : "待反馈"; + this.institutionIdentifier = application.getInstitutionIdentifier(); + this.fullInstitutionIdentifier = application.getDigest(); + this.centralBankSignature = application.getCentralBankSignature(); + } + + public long getApplicationId() { return applicationId; } + public String getApplicationStatus() { return applicationStatus; } + public int getFeedbackStatus() { return feedbackStatus; } + public String getFeedbackStatusText() { return feedbackStatusText; } + public String getInstitutionIdentifier() { return institutionIdentifier; } + public String getFullInstitutionIdentifier() { return fullInstitutionIdentifier; } + public String getCentralBankSignature() { return centralBankSignature; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java index 339a86a..31266e7 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionIdentifierStepService.java @@ -59,22 +59,22 @@ public class InstitutionIdentifierStepService { } @Transactional - public InstitutionSignatureResult sign(String keyId, InstitutionKeySubject subject, String operator) { + public InstitutionSignatureResult sign(String privateKey, InstitutionKeySubject subject, String operator) { InstitutionIdentifierApplication application = requireCurrent(subject, InstitutionIdentifierApplication.Status.DIGESTED, "当前没有待签名的本轮机构标识数据"); long applicationId = application.getId(); - 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()); + String signature = keyService.signCommercialBank(subject, privateKey, application.getDigest()); + if (signature == null) { + recordAndThrow(application, "WRONG_PRIVATE_KEY", "SIGN", null, + "签名必须选择商业银行第二私钥", operator); + } 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); + return new InstitutionSignatureResult(applicationId, application.getStatus().name(), "SM2", + InstitutionKeyService.BANK_SECOND_KEY, "COMMERCIAL_BANK_SECOND_PRIVATE_KEY", signature); } @Transactional diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java index c7ffb1d..ccf0adb 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionKeyService.java @@ -56,6 +56,15 @@ public class InstitutionKeyService { return cryptography.sign(privateKey(subject, BANK_SECOND_KEY), message); } + @Transactional(readOnly = true) + public String signCommercialBank(InstitutionKeySubject subject, String providedPrivateKey, String message) { + String storedPrivateKey = privateKey(subject, BANK_SECOND_KEY); + if (!storedPrivateKey.equalsIgnoreCase(providedPrivateKey)) { + return null; + } + return cryptography.sign(storedPrivateKey, message); + } + @Transactional(readOnly = true) public boolean verifyCommercialBank(InstitutionKeySubject subject, String message, String signature) { return cryptography.verify(required(subject, BANK_SECOND_KEY).getPublicKey(), message, signature); @@ -66,6 +75,15 @@ public class InstitutionKeyService { return cryptography.sign(privateKey(subject, CENTRAL_FIRST_KEY), message); } + @Transactional(readOnly = true) + public String signCentralBank(InstitutionKeySubject subject, String providedPrivateKey, String message) { + String storedPrivateKey = privateKey(subject, CENTRAL_FIRST_KEY); + if (!storedPrivateKey.equalsIgnoreCase(providedPrivateKey)) { + return null; + } + return cryptography.sign(storedPrivateKey, message); + } + @Transactional public void logicalDelete(InstitutionKeySubject subject, String keyId, String operator) { InstitutionSm2KeyEntity key = required(subject, keyId); diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java index 787f112..bf00d92 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingErrorRecorder.java @@ -22,6 +22,9 @@ public class InstitutionTrainingErrorRecorder { @Resource private InstitutionIdentifierTrainingErrorMapper errorMapper; + @Resource + private InstitutionTrainingScoreService scoreService; + @Transactional(propagation = Propagation.REQUIRES_NEW) public int record(long applicationId, String errorType, String operationStep, String providedKeyId, String errorMessage, String operator) { @@ -56,6 +59,7 @@ public class InstitutionTrainingErrorRecorder { error.setCreatedAt(LocalDateTime.now()); error.setCreatedBy(limit(operator, 64)); errorMapper.insert(error); + scoreService.recordError(application.getUserId(), errorSequence); return errorSequence; } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingScoreService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingScoreService.java new file mode 100644 index 0000000..0d624da --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/InstitutionTrainingScoreService.java @@ -0,0 +1,117 @@ +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.infrastructure.StuModuleScoreDetailsEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.UUID; + +/** + * 将本实验的错误次数、完成进度和项目得分同步到平台统一成绩明细表。 + */ +@Service +public class InstitutionTrainingScoreService { + public static final String MODULE_NAME = "中国人民银行生成数字货币"; + private static final int TRAINING_SERIAL_NUMBER = 3; + private static final double TOTAL_SCORE = 100.0; + private static final int TOTAL_TRAINING_MODULES = 9; + + @Resource + private StuModuleScoreDetailsMapper scoreDetailsMapper; + + @Value("${institution-training.score.wrong-deduct:0.50}") + private BigDecimal wrongDeduct = new BigDecimal("0.50"); + + @Transactional + public void recordError(String userId, int errorCount) { + if (userId == null || userId.trim().isEmpty()) { + return; + } + StuModuleScoreDetailsEntity detail = getOrCreate(userId); + detail.setCompletionStatus(String.valueOf(errorCount)); + detail.setScoreProject(score(errorCount, progress(detail.getSchedule()))); + scoreDetailsMapper.updateById(detail); + } + + @Transactional + public void markStepCompleted(String userId, int completedStep) { + if (userId == null || userId.trim().isEmpty()) { + return; + } + StuModuleScoreDetailsEntity detail = getOrCreate(userId); + int errorCount = errorCount(detail.getCompletionStatus()); + int normalizedStep = Math.max(0, Math.min(TOTAL_TRAINING_MODULES, completedStep)); + double schedule = BigDecimal.valueOf(normalizedStep) + .multiply(BigDecimal.valueOf(TOTAL_SCORE)) + .divide(BigDecimal.valueOf(TOTAL_TRAINING_MODULES), 2, RoundingMode.HALF_UP) + .doubleValue(); + detail.setCompletionStatus(String.valueOf(errorCount)); + detail.setSchedule(schedule); + detail.setScoreProject(score(errorCount, schedule)); + scoreDetailsMapper.updateById(detail); + } + + @Transactional(readOnly = true) + public StuModuleScoreDetailsEntity detail(String userId) { + if (userId == null || userId.trim().isEmpty()) { + return null; + } + return scoreDetailsMapper.selectOne(new LambdaQueryWrapper() + .eq(StuModuleScoreDetailsEntity::getUserId, userId) + .eq(StuModuleScoreDetailsEntity::getMoudule, MODULE_NAME) + .eq(StuModuleScoreDetailsEntity::getSerialNumber, TRAINING_SERIAL_NUMBER) + .last("LIMIT 1")); + } + + private StuModuleScoreDetailsEntity getOrCreate(String userId) { + StuModuleScoreDetailsEntity existing = detail(userId); + if (existing != null) { + return existing; + } + StuModuleScoreDetailsEntity created = new StuModuleScoreDetailsEntity(); + created.setId(UUID.randomUUID().toString()); + created.setMoudule(MODULE_NAME); + created.setLearningProjects("实验实训"); + created.setAssessmentItems("三步流程错误次数"); + created.setScoringCriteria(wrongDeduct.stripTrailingZeros().toPlainString()); + created.setCompletionStatus("0"); + created.setScoreProject(0.0); + created.setUserId(userId); + created.setTotalScore(TOTAL_SCORE); + created.setSerialNumber(TRAINING_SERIAL_NUMBER); + created.setSchedule(0.0); + scoreDetailsMapper.insert(created); + return created; + } + + private double score(int errorCount, double schedule) { + BigDecimal base = BigDecimal.valueOf(TOTAL_SCORE) + .subtract(wrongDeduct.multiply(BigDecimal.valueOf(Math.max(0, errorCount)))) + .max(BigDecimal.ZERO); + return base.multiply(BigDecimal.valueOf(schedule)) + .divide(BigDecimal.valueOf(TOTAL_SCORE), 2, RoundingMode.HALF_UP) + .doubleValue(); + } + + private double progress(Double schedule) { + return schedule == null ? 0.0 : schedule; + } + + private int errorCount(String completionStatus) { + if (completionStatus == null || completionStatus.trim().isEmpty()) { + return 0; + } + try { + return Math.max(0, Integer.parseInt(completionStatus)); + } catch (NumberFormatException exception) { + return 0; + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/IssuedInstitutionIdentifierQueryService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/IssuedInstitutionIdentifierQueryService.java index 61ddb55..94226be 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/IssuedInstitutionIdentifierQueryService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/IssuedInstitutionIdentifierQueryService.java @@ -16,11 +16,13 @@ public class IssuedInstitutionIdentifierQueryService { @Transactional(readOnly = true) public IssuedInstitutionIdentifierResult requireIssued(long applicationId, InstitutionKeySubject subject) { - InstitutionIdentifierApplication application = repository.findById(applicationId).orElseThrow(() -> - new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "机构标识申请不存在")); - if (application.getStatus() != InstitutionIdentifierApplication.Status.ISSUED) { + InstitutionIdentifierApplication application = repository.findByIdAndSubject(applicationId, + subject.getUserId(), subject.getSchoolId(), subject.getClassId()).orElseThrow(() -> + new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前实训主体下的机构标识申请不存在")); + if (application.getStatus() != InstitutionIdentifierApplication.Status.FEEDBACKED) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, - "任务一尚未签发机构标识,不能发起货币生成请求,当前状态:" + application.getStatus().name()); + "机构标识尚未反馈到商业银行端,不能发起货币生成请求,当前状态:" + + application.getStatus().name()); } return new IssuedInstitutionIdentifierResult(applicationId, application.getInstitutionIdentifier(), application.getDigest()); diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/TransactionInformationIdentifierResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/TransactionInformationIdentifierResult.java new file mode 100644 index 0000000..82d8282 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/TransactionInformationIdentifierResult.java @@ -0,0 +1,48 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier; + +import java.math.BigDecimal; + +public class TransactionInformationIdentifierResult { + private final long id; + private final long verificationId; + private final long requestId; + private final long identifierApplicationId; + private final BigDecimal amount; + private final String institutionIdentifier; + private final String transactionTimestamp; + private final String originalText; + private final String digestAlgorithm; + private final String digest; + private final String transactionIdentifier; + private final String status; + + public TransactionInformationIdentifierResult(TransactionInformationIdentifier value) { + this.id = value.getId(); + this.verificationId = value.getVerificationId(); + this.requestId = value.getRequestId(); + this.identifierApplicationId = value.getIdentifierApplicationId(); + this.amount = value.getAmount(); + this.institutionIdentifier = value.getInstitutionIdentifier(); + this.transactionTimestamp = value.getTransactionTimestamp(); + this.originalText = value.getOriginalText(); + this.digestAlgorithm = value.getDigest() == null ? null : "SM3"; + this.digest = value.getDigest(); + this.transactionIdentifier = value.getTransactionIdentifier(); + this.status = value.getStatus().name(); + } + + public long getId() { return id; } + public long getVerificationId() { return verificationId; } + public long getRequestId() { return requestId; } + public long getIdentifierApplicationId() { return identifierApplicationId; } + public BigDecimal getAmount() { return amount; } + public String getInstitutionIdentifier() { return institutionIdentifier; } + public String getTransactionTimestamp() { return transactionTimestamp; } + public String getOriginalText() { return originalText; } + public String getDigestAlgorithm() { return digestAlgorithm; } + public String getDigest() { return digest; } + public String getTransactionIdentifier() { return transactionIdentifier; } + public String getStatus() { return status; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/TransactionInformationIdentifierService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/TransactionInformationIdentifierService.java new file mode 100644 index 0000000..fc92e4e --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/TransactionInformationIdentifierService.java @@ -0,0 +1,175 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier; +import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifierRepository; +import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper; +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 TransactionInformationIdentifierService { + private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + @Resource + private CurrencyGenerationVerificationMapper verificationMapper; + @Resource + private CurrencyGenerationRequestMapper requestMapper; + @Resource + private InstitutionIdentifierApplicationMapper applicationMapper; + @Resource + private TransactionInformationIdentifierRepository repository; + @Resource + private InstitutionIdentityCryptography cryptography; + @Resource + private InstitutionTrainingScoreService scoreService; + + private final Clock clock = Clock.systemUTC(); + + @Transactional + public TransactionInformationIdentifierResult prepare(InstitutionKeySubject subject, String operator) { + CurrencyGenerationVerificationEntity verification = requireCompletedVerification(subject); + CurrencyGenerationRequestEntity request = requireCompletedRequest(verification.getRequestId(), subject); + InstitutionIdentifierApplicationEntity application = requireCompletedIdentifier( + request.getIdentifierApplicationId(), subject); + + TransactionInformationIdentifier existing = repository.findLatestByVerificationAndSubject( + verification.getId(), subject.getUserId(), subject.getSchoolId(), subject.getClassId()).orElse(null); + if (existing != null) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + "当前步骤三结果已生成模块四记录,不能重复准备"); + } + + String timestamp = LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC).format(TIMESTAMP); + TransactionInformationIdentifier value = invokeResult(() -> TransactionInformationIdentifier.prepare( + verification.getId(), request.getId(), application.getId(), request.getAmount(), + application.getInstitutionIdentifier(), timestamp)); + long id = repository.save(value, subject.getUserId(), subject.getSchoolId(), subject.getClassId(), operator); + return new TransactionInformationIdentifierResult(TransactionInformationIdentifier.restore(id, + value.getVerificationId(), value.getRequestId(), value.getIdentifierApplicationId(), + value.getAmount(), value.getInstitutionIdentifier(), value.getTransactionTimestamp(), + value.getOriginalText(), null, null, value.getStatus())); + } + + @Transactional + public TransactionInformationIdentifierResult digest(InstitutionKeySubject subject, String operator) { + TransactionInformationIdentifier value = requireStatus(subject, + TransactionInformationIdentifier.Status.PREPARED, "请先获取前序数据并准备交易信息标识"); + TransactionInformationIdentifier.Status expected = value.getStatus(); + String digest = cryptography.sm3(value.getOriginalText()); + invoke(() -> value.recordDigest(digest)); + repository.update(value, expected, "DIGEST", "使用SM3计算交易信息标识摘要", operator); + return new TransactionInformationIdentifierResult(value); + } + + @Transactional + public TransactionInformationIdentifierResult assemble(InstitutionKeySubject subject, String operator) { + TransactionInformationIdentifier value = requireStatus(subject, + TransactionInformationIdentifier.Status.DIGESTED, "请先使用SM3生成摘要"); + TransactionInformationIdentifier.Status expected = value.getStatus(); + invoke(value::assemble); + repository.update(value, expected, "ASSEMBLE", "将固定前缀TXN_与SM3摘要组装为交易信息标识", operator); + scoreService.markStepCompleted(subject.getUserId(), 4); + return new TransactionInformationIdentifierResult(value); + } + + @Transactional(readOnly = true) + public TransactionInformationIdentifierResult detail(InstitutionKeySubject subject) { + TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(), + subject.getSchoolId(), subject.getClassId()).orElseThrow(() -> + new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前没有交易信息标识记录")); + return new TransactionInformationIdentifierResult(value); + } + + private CurrencyGenerationVerificationEntity requireCompletedVerification(InstitutionKeySubject subject) { + CurrencyGenerationVerificationEntity value = verificationMapper.selectOne( + new LambdaQueryWrapper() + .eq(CurrencyGenerationVerificationEntity::getUserId, subject.getUserId()) + .eq(CurrencyGenerationVerificationEntity::getSchoolId, subject.getSchoolId()) + .eq(CurrencyGenerationVerificationEntity::getClassId, subject.getClassId()) + .eq(CurrencyGenerationVerificationEntity::getStatus, "RESPONSE_RETURNED") + .eq(CurrencyGenerationVerificationEntity::getDeleted, false) + .orderByDesc(CurrencyGenerationVerificationEntity::getCreatedAt) + .last("LIMIT 1")); + if (value == null) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + "步骤三尚未完成确认报文返回,不能进入模块四"); + } + return value; + } + + private CurrencyGenerationRequestEntity requireCompletedRequest(long requestId, InstitutionKeySubject subject) { + CurrencyGenerationRequestEntity value = requestMapper.selectOne( + new LambdaQueryWrapper() + .eq(CurrencyGenerationRequestEntity::getId, requestId) + .eq(CurrencyGenerationRequestEntity::getUserId, subject.getUserId()) + .eq(CurrencyGenerationRequestEntity::getSchoolId, subject.getSchoolId()) + .eq(CurrencyGenerationRequestEntity::getClassId, subject.getClassId()) + .eq(CurrencyGenerationRequestEntity::getStatus, "RECEIVED") + .eq(CurrencyGenerationRequestEntity::getDeleted, false)); + if (value == null) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + "步骤二尚未完成央行接收,不能进入模块四"); + } + return value; + } + + private InstitutionIdentifierApplicationEntity requireCompletedIdentifier(long applicationId, + InstitutionKeySubject subject) { + InstitutionIdentifierApplicationEntity value = applicationMapper.selectOne( + new LambdaQueryWrapper() + .eq(InstitutionIdentifierApplicationEntity::getId, applicationId) + .eq(InstitutionIdentifierApplicationEntity::getUserId, subject.getUserId()) + .eq(InstitutionIdentifierApplicationEntity::getSchoolId, subject.getSchoolId()) + .eq(InstitutionIdentifierApplicationEntity::getClassId, subject.getClassId()) + .eq(InstitutionIdentifierApplicationEntity::getStatus, "FEEDBACKED") + .eq(InstitutionIdentifierApplicationEntity::getDeleted, false)); + if (value == null) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + "步骤一尚未完成机构标识反馈,不能进入模块四"); + } + return value; + } + + private TransactionInformationIdentifier requireStatus(InstitutionKeySubject subject, + TransactionInformationIdentifier.Status status, + String message) { + return repository.findLatestBySubjectAndStatus(subject.getUserId(), subject.getSchoolId(), + subject.getClassId(), status).orElseThrow(() -> + new BusinessException(ErrorCode.VALIDATION_ERROR, message)); + } + + private static void invoke(Runnable operation) { + try { + operation.run(); + } catch (IllegalArgumentException | IllegalStateException exception) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage()); + } + } + + private static T invokeResult(Supplier supplier) { + try { + return supplier.get(); + } catch (IllegalArgumentException | IllegalStateException exception) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage()); + } + } + + private interface Supplier { + T get(); + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java index 8fc9501..1de975f 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java @@ -13,6 +13,7 @@ public class InstitutionIdentifierApplication { VERIFIED, VERIFICATION_FAILED, ISSUED, + FEEDBACKED, REJECTED } @@ -103,8 +104,13 @@ public class InstitutionIdentifierApplication { this.status = Status.ISSUED; } + public void feedback() { + requireStatus(Status.ISSUED, "请先完成央行签名确权"); + this.status = Status.FEEDBACKED; + } + public void reject() { - if (status == Status.ISSUED || status == Status.REJECTED) { + if (status == Status.ISSUED || status == Status.FEEDBACKED || status == Status.REJECTED) { throw new IllegalStateException("申请状态不允许拒绝"); } status = Status.REJECTED; diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java index 28ec90f..3c1f78c 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierRepository.java @@ -13,6 +13,10 @@ public interface InstitutionIdentifierRepository { long schoolId, long classId); + Optional findLatestBySubject(String userId, + long schoolId, + long classId); + Optional findLatestBySubjectAndStatus(String userId, long schoolId, long classId, diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifier.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifier.java new file mode 100644 index 0000000..dafbdde --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifier.java @@ -0,0 +1,109 @@ +package com.yau.digitalrmb.institutionidentity.domain; + +import java.math.BigDecimal; + +/** + * 模块四:根据前序流程落库的金额、机构标识和时间戳生成交易信息标识。 + */ +public class TransactionInformationIdentifier { + public enum Status { + PREPARED, DIGESTED, ASSEMBLED + } + + private final Long id; + private final long verificationId; + private final long requestId; + private final long identifierApplicationId; + private final BigDecimal amount; + private final String institutionIdentifier; + private final String transactionTimestamp; + private final String originalText; + private String digest; + private String transactionIdentifier; + private Status status; + + private TransactionInformationIdentifier(Long id, long verificationId, long requestId, + long identifierApplicationId, BigDecimal amount, + String institutionIdentifier, String transactionTimestamp, + String originalText, String digest, + String transactionIdentifier, Status status) { + this.id = id; + this.verificationId = verificationId; + this.requestId = requestId; + this.identifierApplicationId = identifierApplicationId; + this.amount = amount; + this.institutionIdentifier = institutionIdentifier; + this.transactionTimestamp = transactionTimestamp; + this.originalText = originalText; + this.digest = digest; + this.transactionIdentifier = transactionIdentifier; + this.status = status; + } + + public static TransactionInformationIdentifier prepare(long verificationId, long requestId, + long identifierApplicationId, BigDecimal amount, + String institutionIdentifier, + String transactionTimestamp) { + if (verificationId <= 0 || requestId <= 0 || identifierApplicationId <= 0) { + throw new IllegalArgumentException("前序流程数据不完整,不能生成交易信息标识"); + } + if (amount == null || amount.signum() <= 0) { + throw new IllegalArgumentException("前序流程中的货币金额无效"); + } + if (institutionIdentifier == null || !institutionIdentifier.matches("ORG_[0-9A-F]{12}")) { + throw new IllegalArgumentException("前序流程中的投放机构标识无效"); + } + if (transactionTimestamp == null || !transactionTimestamp.matches("\\d{14}")) { + throw new IllegalArgumentException("交易信息标识时间戳格式不正确"); + } + String originalText = amount.setScale(2).toPlainString() + "|" + institutionIdentifier + + "|" + transactionTimestamp; + return new TransactionInformationIdentifier(null, verificationId, requestId, + identifierApplicationId, amount.setScale(2), institutionIdentifier, + transactionTimestamp, originalText, null, null, Status.PREPARED); + } + + public static TransactionInformationIdentifier restore(Long id, long verificationId, long requestId, + long identifierApplicationId, BigDecimal amount, + String institutionIdentifier, + String transactionTimestamp, String originalText, + String digest, String transactionIdentifier, + Status status) { + return new TransactionInformationIdentifier(id, verificationId, requestId, + identifierApplicationId, amount, institutionIdentifier, transactionTimestamp, + originalText, digest, transactionIdentifier, status); + } + + 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 assemble() { + requireStatus(Status.DIGESTED, "请先使用SM3生成摘要"); + this.transactionIdentifier = "TXN_" + digest; + this.status = Status.ASSEMBLED; + } + + private void requireStatus(Status expected, String message) { + if (status != expected) { + throw new IllegalStateException(message + ",当前状态:" + status.name()); + } + } + + public Long getId() { return id; } + public long getVerificationId() { return verificationId; } + public long getRequestId() { return requestId; } + public long getIdentifierApplicationId() { return identifierApplicationId; } + public BigDecimal getAmount() { return amount; } + public String getInstitutionIdentifier() { return institutionIdentifier; } + public String getTransactionTimestamp() { return transactionTimestamp; } + public String getOriginalText() { return originalText; } + public String getDigest() { return digest; } + public String getTransactionIdentifier() { return transactionIdentifier; } + public Status getStatus() { return status; } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifierRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifierRepository.java new file mode 100644 index 0000000..137a56d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifierRepository.java @@ -0,0 +1,24 @@ +package com.yau.digitalrmb.institutionidentity.domain; + +import java.util.Optional; + +public interface TransactionInformationIdentifierRepository { + long save(TransactionInformationIdentifier value, String userId, long schoolId, long classId, String operator); + + Optional findByIdAndSubject(long id, String userId, + long schoolId, long classId); + + Optional findLatestByVerificationAndSubject(long verificationId, + String userId, + long schoolId, + long classId); + + Optional findLatestBySubject(String userId, long schoolId, long classId); + + Optional findLatestBySubjectAndStatus( + String userId, long schoolId, long classId, TransactionInformationIdentifier.Status status); + + void update(TransactionInformationIdentifier value, + TransactionInformationIdentifier.Status expectedStatus, + String operation, String detail, String operator); +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/CurrencyGenerationRequestOperationLogEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/CurrencyGenerationRequestOperationLogEntity.java index 5ce1986..e5d85df 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/CurrencyGenerationRequestOperationLogEntity.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/CurrencyGenerationRequestOperationLogEntity.java @@ -3,6 +3,7 @@ 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.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; import lombok.Setter; @@ -21,4 +22,5 @@ public class CurrencyGenerationRequestOperationLogEntity { @TableField("operation_detail") private String operationDetail; @TableField("created_at") private LocalDateTime createdAt; @TableField("created_by") private String createdBy; + @TableLogic private Boolean deleted = false; } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/CurrencyGenerationVerificationOperationLogEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/CurrencyGenerationVerificationOperationLogEntity.java index bf4c82b..ea9df4f 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/CurrencyGenerationVerificationOperationLogEntity.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/CurrencyGenerationVerificationOperationLogEntity.java @@ -3,6 +3,7 @@ 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.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; import lombok.Setter; @@ -21,4 +22,5 @@ public class CurrencyGenerationVerificationOperationLogEntity { @TableField("operation_detail") private String operationDetail; @TableField("created_at") private LocalDateTime createdAt; @TableField("created_by") private String createdBy; + @TableLogic private Boolean deleted = false; } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java index a2bad64..05f6dd3 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierOperationLogEntity.java @@ -3,6 +3,7 @@ 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.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; import lombok.Setter; @@ -28,4 +29,6 @@ public class InstitutionIdentifierOperationLogEntity { private LocalDateTime createdAt; @TableField("created_by") private String createdBy; + @TableLogic + private Boolean deleted = false; } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java index d330d6e..c6e302a 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionIdentifierTrainingErrorEntity.java @@ -3,6 +3,7 @@ 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.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; import lombok.Setter; @@ -35,4 +36,6 @@ public class InstitutionIdentifierTrainingErrorEntity { private LocalDateTime createdAt; @TableField("created_by") private String createdBy; + @TableLogic + private Boolean deleted = false; } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java index 636fce0..6a923b4 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/InstitutionSm2KeyAuditEntity.java @@ -3,6 +3,7 @@ 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.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; import lombok.Setter; @@ -32,4 +33,6 @@ public class InstitutionSm2KeyAuditEntity { private LocalDateTime createdAt; @TableField("created_by") private String createdBy; + @TableLogic + private Boolean deleted = false; } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java index ace759d..da0cafc 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisInstitutionIdentifierRepository.java @@ -65,6 +65,21 @@ public class MybatisInstitutionIdentifierRepository implements InstitutionIdenti return Optional.ofNullable(entity).map(this::toDomain); } + @Override + public Optional findLatestBySubject( + String userId, long schoolId, long classId) { + InstitutionIdentifierApplicationEntity entity = mapper.selectOne( + new LambdaQueryWrapper() + .eq(InstitutionIdentifierApplicationEntity::getUserId, userId) + .eq(InstitutionIdentifierApplicationEntity::getSchoolId, schoolId) + .eq(InstitutionIdentifierApplicationEntity::getClassId, classId) + .eq(InstitutionIdentifierApplicationEntity::getDeleted, false) + .orderByDesc(InstitutionIdentifierApplicationEntity::getTrainingRound) + .orderByDesc(InstitutionIdentifierApplicationEntity::getCreatedAt) + .last("LIMIT 1")); + return Optional.ofNullable(entity).map(this::toDomain); + } + @Override public Optional findLatestBySubjectAndStatus( String userId, long schoolId, long classId, InstitutionIdentifierApplication.Status status) { diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisTransactionInformationIdentifierRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisTransactionInformationIdentifierRepository.java new file mode 100644 index 0000000..0bebaeb --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisTransactionInformationIdentifierRepository.java @@ -0,0 +1,152 @@ +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.TransactionInformationIdentifier; +import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifierRepository; +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 MybatisTransactionInformationIdentifierRepository + implements TransactionInformationIdentifierRepository { + @Resource + private TransactionInformationIdentifierMapper mapper; + @Resource + private TransactionInformationIdentifierOperationLogMapper logMapper; + + @Override + public long save(TransactionInformationIdentifier value, String userId, + long schoolId, long classId, String operator) { + TransactionInformationIdentifierEntity entity = toEntity(value); + entity.setUserId(userId); + entity.setSchoolId(schoolId); + entity.setClassId(classId); + entity.setCreatedAt(LocalDateTime.now()); + entity.setUpdatedAt(LocalDateTime.now()); + entity.setCreatedBy(operator); + entity.setUpdatedBy(operator); + entity.setDeleted(false); + mapper.insert(entity); + insertLog(entity.getId(), "PREPARE", null, value.getStatus().name(), + "从已完成的步骤一至步骤三复制金额和机构标识并生成交易时间戳", operator); + return entity.getId(); + } + + @Override + public Optional findByIdAndSubject(long id, String userId, + long schoolId, long classId) { + return Optional.ofNullable(mapper.selectOne(new LambdaQueryWrapper() + .eq(TransactionInformationIdentifierEntity::getId, id) + .eq(TransactionInformationIdentifierEntity::getUserId, userId) + .eq(TransactionInformationIdentifierEntity::getSchoolId, schoolId) + .eq(TransactionInformationIdentifierEntity::getClassId, classId) + .eq(TransactionInformationIdentifierEntity::getDeleted, false))) + .map(this::toDomain); + } + + @Override + public Optional findLatestByVerificationAndSubject( + long verificationId, String userId, long schoolId, long classId) { + return Optional.ofNullable(mapper.selectOne(new LambdaQueryWrapper() + .eq(TransactionInformationIdentifierEntity::getVerificationId, verificationId) + .eq(TransactionInformationIdentifierEntity::getUserId, userId) + .eq(TransactionInformationIdentifierEntity::getSchoolId, schoolId) + .eq(TransactionInformationIdentifierEntity::getClassId, classId) + .eq(TransactionInformationIdentifierEntity::getDeleted, false) + .orderByDesc(TransactionInformationIdentifierEntity::getCreatedAt) + .last("LIMIT 1"))) + .map(this::toDomain); + } + + @Override + public Optional findLatestBySubject(String userId, long schoolId, long classId) { + return Optional.ofNullable(mapper.selectOne(new LambdaQueryWrapper() + .eq(TransactionInformationIdentifierEntity::getUserId, userId) + .eq(TransactionInformationIdentifierEntity::getSchoolId, schoolId) + .eq(TransactionInformationIdentifierEntity::getClassId, classId) + .eq(TransactionInformationIdentifierEntity::getDeleted, false) + .orderByDesc(TransactionInformationIdentifierEntity::getCreatedAt) + .last("LIMIT 1"))) + .map(this::toDomain); + } + + @Override + public Optional findLatestBySubjectAndStatus( + String userId, long schoolId, long classId, TransactionInformationIdentifier.Status status) { + return Optional.ofNullable(mapper.selectOne(new LambdaQueryWrapper() + .eq(TransactionInformationIdentifierEntity::getUserId, userId) + .eq(TransactionInformationIdentifierEntity::getSchoolId, schoolId) + .eq(TransactionInformationIdentifierEntity::getClassId, classId) + .eq(TransactionInformationIdentifierEntity::getStatus, status.name()) + .eq(TransactionInformationIdentifierEntity::getDeleted, false) + .orderByDesc(TransactionInformationIdentifierEntity::getCreatedAt) + .last("LIMIT 1"))) + .map(this::toDomain); + } + + @Override + public void update(TransactionInformationIdentifier value, + TransactionInformationIdentifier.Status expectedStatus, + String operation, String detail, String operator) { + LambdaUpdateWrapper update = + new LambdaUpdateWrapper() + .eq(TransactionInformationIdentifierEntity::getId, value.getId()) + .eq(TransactionInformationIdentifierEntity::getStatus, expectedStatus.name()) + .eq(TransactionInformationIdentifierEntity::getDeleted, false) + .set(TransactionInformationIdentifierEntity::getDigest, value.getDigest()) + .set(TransactionInformationIdentifierEntity::getTransactionIdentifier, + value.getTransactionIdentifier()) + .set(TransactionInformationIdentifierEntity::getStatus, value.getStatus().name()) + .set(TransactionInformationIdentifierEntity::getUpdatedAt, LocalDateTime.now()) + .set(TransactionInformationIdentifierEntity::getUpdatedBy, operator); + if (mapper.update(null, update) != 1) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + "交易信息标识状态已变化,不能重复或越级操作"); + } + insertLog(value.getId(), operation, expectedStatus.name(), value.getStatus().name(), detail, operator); + } + + private TransactionInformationIdentifierEntity toEntity(TransactionInformationIdentifier value) { + TransactionInformationIdentifierEntity entity = new TransactionInformationIdentifierEntity(); + entity.setId(value.getId()); + entity.setVerificationId(value.getVerificationId()); + entity.setRequestId(value.getRequestId()); + entity.setIdentifierApplicationId(value.getIdentifierApplicationId()); + entity.setAmount(value.getAmount()); + entity.setInstitutionIdentifier(value.getInstitutionIdentifier()); + entity.setTransactionTimestamp(value.getTransactionTimestamp()); + entity.setOriginalText(value.getOriginalText()); + entity.setDigest(value.getDigest()); + entity.setTransactionIdentifier(value.getTransactionIdentifier()); + entity.setStatus(value.getStatus().name()); + return entity; + } + + private TransactionInformationIdentifier toDomain(TransactionInformationIdentifierEntity entity) { + return TransactionInformationIdentifier.restore(entity.getId(), entity.getVerificationId(), + entity.getRequestId(), entity.getIdentifierApplicationId(), entity.getAmount(), + entity.getInstitutionIdentifier(), entity.getTransactionTimestamp(), entity.getOriginalText(), + entity.getDigest(), entity.getTransactionIdentifier(), + TransactionInformationIdentifier.Status.valueOf(entity.getStatus())); + } + + private void insertLog(Long id, String operation, String fromStatus, String toStatus, + String detail, String operator) { + TransactionInformationIdentifierOperationLogEntity log = + new TransactionInformationIdentifierOperationLogEntity(); + log.setTransactionIdentifierId(id); + log.setOperation(operation); + log.setFromStatus(fromStatus); + log.setToStatus(toStatus); + log.setOperationDetail(detail); + log.setCreatedAt(LocalDateTime.now()); + log.setCreatedBy(operator); + logMapper.insert(log); + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/StuModuleScoreDetailsEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/StuModuleScoreDetailsEntity.java new file mode 100644 index 0000000..49f6d23 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/StuModuleScoreDetailsEntity.java @@ -0,0 +1,44 @@ +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; + +@Getter +@Setter +@TableName("stu_module_score_details") +public class StuModuleScoreDetailsEntity { + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + private String moudule; + + @TableField("learning_projects") + private String learningProjects; + + @TableField("assessment_items") + private String assessmentItems; + + @TableField("scoring_criteria") + private String scoringCriteria; + + @TableField("completion_status") + private String completionStatus; + + @TableField("score_project") + private Double scoreProject; + + @TableField("user_id") + private String userId; + + @TableField("total_score") + private Double totalScore; + + @TableField("serial_number") + private Integer serialNumber; + + private Double schedule; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/StuModuleScoreDetailsMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/StuModuleScoreDetailsMapper.java new file mode 100644 index 0000000..fa17679 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/StuModuleScoreDetailsMapper.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 StuModuleScoreDetailsMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierEntity.java new file mode 100644 index 0000000..854b475 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierEntity.java @@ -0,0 +1,28 @@ +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; + +import java.math.BigDecimal; + +@Getter +@Setter +@TableName("transaction_information_identifier") +public class TransactionInformationIdentifierEntity extends AuditableEntity { + @TableField("verification_id") private Long verificationId; + @TableField("request_id") private Long requestId; + @TableField("identifier_application_id") private Long identifierApplicationId; + private BigDecimal amount; + @TableField("institution_identifier") private String institutionIdentifier; + @TableField("transaction_timestamp") private String transactionTimestamp; + @TableField("original_text") private String originalText; + private String digest; + @TableField("transaction_identifier") private String transactionIdentifier; + private String status; + @TableField("user_id") private String userId; + @TableField("school_id") private Long schoolId; + @TableField("class_id") private Long classId; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierMapper.java new file mode 100644 index 0000000..d01c8c8 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierMapper.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 TransactionInformationIdentifierMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierOperationLogEntity.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierOperationLogEntity.java new file mode 100644 index 0000000..df46fb6 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierOperationLogEntity.java @@ -0,0 +1,28 @@ +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.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@TableName("transaction_information_identifier_operation_log") +public class TransactionInformationIdentifierOperationLogEntity { + @TableId(type = IdType.ASSIGN_ID) + private Long id; + @TableField("transaction_identifier_id") private Long transactionIdentifierId; + 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; + @TableLogic + private Boolean deleted = false; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierOperationLogMapper.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierOperationLogMapper.java new file mode 100644 index 0000000..3b978c0 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/TransactionInformationIdentifierOperationLogMapper.java @@ -0,0 +1,9 @@ +package com.yau.digitalrmb.institutionidentity.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TransactionInformationIdentifierOperationLogMapper + extends BaseMapper { +} 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 index 84f4f0a..cdbfefc 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/ConfirmInstitutionIdentifierRequest.java @@ -1,11 +1,8 @@ package com.yau.digitalrmb.institutionidentity.interfaces.dto; -import javax.validation.constraints.NotBlank; - public class ConfirmInstitutionIdentifierRequest { - @NotBlank(message = "签名密钥不能为空") - private String signingKeyId; + private String privateKey; - public String getSigningKeyId() { return signingKeyId; } - public void setSigningKeyId(String signingKeyId) { this.signingKeyId = signingKeyId; } + public String getPrivateKey() { return privateKey; } + public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } } 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 index 6363358..efa96af 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/InstitutionIdentifierStepRequest.java @@ -1,11 +1,8 @@ package com.yau.digitalrmb.institutionidentity.interfaces.dto; -import javax.validation.constraints.Size; - public class InstitutionIdentifierStepRequest { - @Size(max = 64, message = "密钥标识长度不能超过64个字符") - private String keyId; + private String privateKey; - public String getKeyId() { return keyId; } - public void setKeyId(String keyId) { this.keyId = keyId; } + public String getPrivateKey() { return privateKey; } + public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } } 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 index c3dc5a7..a007916 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/dto/VerifyInstitutionApplicationRequest.java @@ -3,9 +3,9 @@ package com.yau.digitalrmb.institutionidentity.interfaces.dto; import javax.validation.constraints.NotBlank; public class VerifyInstitutionApplicationRequest { - @NotBlank(message = "验证密钥不能为空") - private String verificationKeyId; + @NotBlank(message = "验证公钥编号不能为空") + private String keyId; - public String getVerificationKeyId() { return verificationKeyId; } - public void setVerificationKeyId(String verificationKeyId) { this.verificationKeyId = verificationKeyId; } + public String getKeyId() { return keyId; } + public void setKeyId(String keyId) { this.keyId = keyId; } } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CurrencyGenerationRequestController.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CurrencyGenerationRequestController.java index c20da13..b106ad4 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CurrencyGenerationRequestController.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CurrencyGenerationRequestController.java @@ -31,7 +31,7 @@ public class CurrencyGenerationRequestController { @PostMapping("/steps/prepare") @Operation(summary = "准备请求信息", - description = "任务一必须已ISSUED;后端自动复制简洁和完整机构标识,前端不能覆盖。") + description = "任务一必须已完成信息反馈;后端自动复制简洁和完整机构标识,前端不能覆盖。") public ApiResponse prepare( @Valid @RequestBody PrepareCurrencyRequest request) { JwtUser jwtUser = AuthContextHolder.get(); 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 index 99a17a4..c315099 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/InstitutionIdentifierController.java @@ -5,6 +5,9 @@ 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.InstitutionIdentifierFeedbackService; +import com.yau.digitalrmb.institutionidentity.application.InstitutionIdentifierFinalResult; +import com.yau.digitalrmb.institutionidentity.application.CommercialBankInstitutionIdentifierResult; import com.yau.digitalrmb.institutionidentity.application.InstitutionInformationPreparationResult; import com.yau.digitalrmb.institutionidentity.application.InstitutionDigestResult; import com.yau.digitalrmb.institutionidentity.application.InstitutionSignatureResult; @@ -13,8 +16,8 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResu 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.institutionidentity.interfaces.dto.VerifyInstitutionApplicationRequest; import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.JwtUser; import com.yau.digitalrmb.shared.api.ApiResponse; @@ -44,6 +47,9 @@ public class InstitutionIdentifierController { @Resource private InstitutionApplicationVerificationService verificationService; + @Resource + private InstitutionIdentifierFeedbackService feedbackService; + @PostMapping("/keys/commercial-bank") @Operation(summary = "获取商业银行SM2密钥对", description = "按Token中的userId、schoolId、classId生成或读取商业银行密钥对,私钥加密保存。") @@ -97,7 +103,7 @@ public class InstitutionIdentifierController { public ApiResponse sign( @Valid @RequestBody InstitutionIdentifierStepRequest request) { JwtUser jwtUser = AuthContextHolder.get(); - return ApiResponse.success(stepService.sign(request.getKeyId(), + return ApiResponse.success(stepService.sign(request.getPrivateKey(), InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId()); } @@ -109,23 +115,50 @@ public class InstitutionIdentifierController { jwtUser.getUsername()), traceId()); } - @PostMapping("/applications/verification") - @Operation(summary = "查看并保存机构标识申请验证过程", description = "仅PACKAGED状态可操作;保存SM2验签和SM3完整性验证结果。") + @PostMapping({"/applications/decryption-verification", "/applications/verification"}) + @Operation(summary = "央行解密验证机构标识申请", + description = "仅PACKAGED状态可操作;必须选择商业银行第二公钥,执行SM2验签并重算SM3摘要,验证身份真实性和数据完整性。旧verification路径保留兼容。") public ApiResponse verifyApplication( @Valid @RequestBody VerifyInstitutionApplicationRequest request) { JwtUser jwtUser = AuthContextHolder.get(); - return ApiResponse.success(verificationService.verify(request.getVerificationKeyId(), + return ApiResponse.success(verificationService.verify(request.getKeyId(), InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId()); } @PostMapping("/applications/confirmation") - @Operation(summary = "央行签名确权并组装机构标识", description = "仅VERIFIED状态可操作;保存央行签名和机构标识并进入ISSUED状态。") + @Operation(summary = "央行签名确权并组装机构标识", + description = "仅VERIFIED状态可操作;privateKey可选,传入时校验央行第一私钥,不传时直接完成确权。") public ApiResponse confirmIdentifier( - @Valid @RequestBody ConfirmInstitutionIdentifierRequest request) { + @Valid @RequestBody(required = false) ConfirmInstitutionIdentifierRequest request) { + JwtUser jwtUser = AuthContextHolder.get(); + return ApiResponse.success(verificationService.confirm(request == null ? null : request.getPrivateKey(), + InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId()); + } + + @GetMapping("/applications/final-identifier") + @Operation(summary = "央行查看最终机构标识", + description = "央行签名确权后即可查看;是否已反馈通过feedbackStatus区分。") + public ApiResponse finalIdentifier() { + return ApiResponse.success(feedbackService.finalIdentifier( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + @PostMapping("/applications/feedback") + @Operation(summary = "央行信息反馈", + description = "将已确权的最终机构标识同步到商业银行端,商业银行状态由1变为2。") + public ApiResponse feedback() { JwtUser jwtUser = AuthContextHolder.get(); - return ApiResponse.success(verificationService.confirm(request.getSigningKeyId(), + return ApiResponse.success(feedbackService.feedback( InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId()); } + @GetMapping("/applications/commercial-bank") + @Operation(summary = "商业银行查看机构标识反馈状态", + description = "商业银行第二私钥签名成功后返回状态1待确认;央行信息反馈后返回状态2及机构标识数据。") + public ApiResponse commercialBankResult() { + return ApiResponse.success(feedbackService.commercialBankResult( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + private String traceId() { return MDC.get(TraceIdFilter.MDC_KEY); } } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/TransactionInformationIdentifierController.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/TransactionInformationIdentifierController.java new file mode 100644 index 0000000..57c7812 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/TransactionInformationIdentifierController.java @@ -0,0 +1,59 @@ +package com.yau.digitalrmb.institutionidentity.interfaces.rest; + +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierResult; +import com.yau.digitalrmb.institutionidentity.application.TransactionInformationIdentifierService; +import com.yau.digitalrmb.security.context.AuthContextHolder; +import com.yau.digitalrmb.security.context.JwtUser; +import com.yau.digitalrmb.shared.api.ApiResponse; +import com.yau.digitalrmb.shared.web.TraceIdFilter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.MDC; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/api/v1/institution-identifiers/transaction-information-identifiers") +@Tag(name = "模块四:生成交易信息标识") +public class TransactionInformationIdentifierController { + @Resource + private TransactionInformationIdentifierService service; + + @PostMapping("/steps/prepare") + @Operation(summary = "获取前序数据并拼接交易信息原文", + description = "步骤一必须FEEDBACKED、步骤二必须RECEIVED、步骤三必须RESPONSE_RETURNED;金额和机构标识由后端自动流转。") + public ApiResponse prepare() { + JwtUser user = AuthContextHolder.get(); + return ok(service.prepare(InstitutionKeySubject.from(user), user.getUsername())); + } + + @PostMapping("/steps/digest") + @Operation(summary = "使用SM3生成交易信息摘要") + public ApiResponse digest() { + JwtUser user = AuthContextHolder.get(); + return ok(service.digest(InstitutionKeySubject.from(user), user.getUsername())); + } + + @PostMapping("/steps/assemble") + @Operation(summary = "组装最终交易信息标识", + description = "将固定前缀TXN_与已保存的SM3摘要拼接,结果写入数据库供后续模块读取。") + public ApiResponse assemble() { + JwtUser user = AuthContextHolder.get(); + return ok(service.assemble(InstitutionKeySubject.from(user), user.getUsername())); + } + + @GetMapping + @Operation(summary = "查询当前用户最新交易信息标识记录") + public ApiResponse detail() { + return ok(service.detail(InstitutionKeySubject.from(AuthContextHolder.get()))); + } + + private ApiResponse ok(TransactionInformationIdentifierResult result) { + return ApiResponse.success(result, MDC.get(TraceIdFilter.MDC_KEY)); + } +} diff --git a/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationConfig.java b/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationConfig.java new file mode 100644 index 0000000..ee72d72 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationConfig.java @@ -0,0 +1,11 @@ +package com.yau.digitalrmb.platformintegration.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true") +@EnableConfigurationProperties(PlatformIntegrationProperties.class) +public class PlatformIntegrationConfig { +} diff --git a/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java b/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java index 4e25788..90fe2dd 100644 --- a/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java +++ b/src/main/java/com/yau/digitalrmb/security/config/SecurityConfig.java @@ -52,7 +52,8 @@ public class SecurityConfig { .authorizeRequests(authorize -> authorize .antMatchers("/actuator/health", "/api/v1/auth/login", "/api/v1/auth/sso", "/api/user/login", - "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") + "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", + "/doc.html", "/webjars/**", "/swagger-resources/**") .permitAll() .anyRequest().authenticated()) .oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwt -> { })) diff --git a/src/main/java/com/yau/digitalrmb/shared/config/OpenApiConfig.java b/src/main/java/com/yau/digitalrmb/shared/config/OpenApiConfig.java index e696c03..b7a5703 100644 --- a/src/main/java/com/yau/digitalrmb/shared/config/OpenApiConfig.java +++ b/src/main/java/com/yau/digitalrmb/shared/config/OpenApiConfig.java @@ -2,15 +2,22 @@ package com.yau.digitalrmb.shared.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenApiConfig { + private static final String AUTHORIZATION = "Authorization"; + @Bean OpenAPI openAPI() { return new OpenAPI().info(new Info().title("数字人民币教学仿真后端").version("0.1.0")) - .schemaRequirement("bearerAuth", new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")); + .addSecurityItem(new SecurityRequirement().addList(AUTHORIZATION)) + .schemaRequirement(AUTHORIZATION, new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name(AUTHORIZATION)); } } diff --git a/src/main/java/com/yau/digitalrmb/shared/config/SwaggerStartupLogger.java b/src/main/java/com/yau/digitalrmb/shared/config/SwaggerStartupLogger.java index 1601353..4598435 100644 --- a/src/main/java/com/yau/digitalrmb/shared/config/SwaggerStartupLogger.java +++ b/src/main/java/com/yau/digitalrmb/shared/config/SwaggerStartupLogger.java @@ -18,13 +18,13 @@ public class SwaggerStartupLogger { @EventListener(ApplicationReadyEvent.class) public void printSwaggerAddresses() { - if (!environment.getProperty("springdoc.swagger-ui.enabled", Boolean.class, false)) { + if (!environment.getProperty("knife4j.enable", Boolean.class, false)) { return; } String port = environment.getProperty("local.server.port", environment.getProperty("server.port", "8080")); String contextPath = environment.getProperty("server.servlet.context-path", ""); String baseUrl = "http://localhost:" + port + contextPath; - LOGGER.info("Swagger UI: {}/swagger-ui/index.html", baseUrl); + LOGGER.info("Knife4j UI: {}/doc.html", baseUrl); LOGGER.info("OpenAPI JSON: {}/v3/api-docs", baseUrl); } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5eb58ac..9e80d3f 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -10,6 +10,8 @@ spring: springdoc: swagger-ui: enabled: true +knife4j: + enable: true platform-integration: enabled: true token: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 05b0d9e..25dbb3f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,10 +25,15 @@ management: springdoc: swagger-ui: enabled: false +knife4j: + enable: false platform-integration: token: link-secret-key: ${DIGITAL_RMB_PLATFORM_LINK_SECRET_KEY} frontend: - callback-url: ${DIGITAL_RMB_FRONTEND_CALLBACK_URL} + callback-url: ${DIGITAL_RMB_FRONTEND_CALLBACK_URL:http://localhost:5173/sso-callback} +institution-training: + score: + wrong-deduct: ${INSTITUTION_TRAINING_WRONG_DEDUCT:0.50} server: - port: 8787 \ No newline at end of file + port: 2318 diff --git a/src/main/resources/db/manual/institution_identifier_training.sql b/src/main/resources/db/manual/institution_identifier_training.sql index cc9b828..028dcf4 100644 --- a/src/main/resources/db/manual/institution_identifier_training.sql +++ b/src/main/resources/db/manual/institution_identifier_training.sql @@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS institution_identifier_operation_log ( operation_detail VARCHAR(1024) NOT NULL, created_at TIMESTAMP NOT NULL, created_by VARCHAR(64) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT fk_identifier_operation_application FOREIGN KEY (application_id) REFERENCES institution_identifier_application(id) ); @@ -58,6 +59,7 @@ CREATE TABLE IF NOT EXISTS institution_identifier_training_error ( error_message VARCHAR(512) NOT NULL, created_at TIMESTAMP NOT NULL, created_by VARCHAR(64) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT uk_identifier_training_error UNIQUE (application_id, error_sequence), CONSTRAINT fk_identifier_error_application FOREIGN KEY (application_id) REFERENCES institution_identifier_application(id) @@ -98,6 +100,7 @@ CREATE TABLE IF NOT EXISTS institution_sm2_key_audit ( operation_detail VARCHAR(512) NOT NULL, created_at TIMESTAMP NOT NULL, created_by VARCHAR(64) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT fk_institution_key_audit_key FOREIGN KEY (key_record_id) REFERENCES institution_sm2_key(id) ); @@ -146,6 +149,7 @@ CREATE TABLE IF NOT EXISTS currency_generation_request_operation_log ( operation_detail VARCHAR(1024) NOT NULL, created_at TIMESTAMP NOT NULL, created_by VARCHAR(64) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT fk_currency_request_operation_request FOREIGN KEY (request_id) REFERENCES currency_generation_request(id) ); @@ -198,9 +202,79 @@ CREATE TABLE IF NOT EXISTS currency_generation_verification_operation_log ( operation_detail VARCHAR(1024) NOT NULL, created_at TIMESTAMP NOT NULL, created_by VARCHAR(64) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT fk_currency_verification_operation FOREIGN KEY (verification_id) REFERENCES currency_generation_verification(id) ); CREATE INDEX IF NOT EXISTS idx_currency_verification_operation ON currency_generation_verification_operation_log(verification_id, created_at); + +CREATE TABLE IF NOT EXISTS stu_module_score_details ( + id VARCHAR(64) NOT NULL, + moudule VARCHAR(100) NOT NULL, + learning_projects VARCHAR(100), + assessment_items VARCHAR(100), + scoring_criteria VARCHAR(100), + completion_status VARCHAR(100), + score_project DOUBLE, + user_id VARCHAR(64) NOT NULL, + total_score DECIMAL(10, 2) NOT NULL DEFAULT 100.00, + serial_number INT, + schedule DOUBLE, + PRIMARY KEY (id), + CONSTRAINT uk_user_moudule_serial UNIQUE (user_id, moudule, serial_number) +); + +CREATE INDEX IF NOT EXISTS idx_user_moudule + ON stu_module_score_details(user_id, moudule); + +CREATE TABLE IF NOT EXISTS transaction_information_identifier ( + id BIGINT PRIMARY KEY, + verification_id BIGINT NOT NULL, + request_id BIGINT NOT NULL, + identifier_application_id BIGINT NOT NULL, + amount DECIMAL(18, 2) NOT NULL, + institution_identifier VARCHAR(32) NOT NULL, + transaction_timestamp CHAR(14) NOT NULL, + original_text VARCHAR(256) NOT NULL, + digest CHAR(64), + transaction_identifier VARCHAR(80), + status VARCHAR(32) NOT NULL, + user_id VARCHAR(36) NOT NULL, + school_id BIGINT NOT NULL, + class_id BIGINT 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, + CONSTRAINT uk_transaction_identifier_verification UNIQUE + (verification_id, user_id, school_id, class_id), + CONSTRAINT fk_transaction_identifier_verification FOREIGN KEY (verification_id) + REFERENCES currency_generation_verification(id), + CONSTRAINT fk_transaction_identifier_request FOREIGN KEY (request_id) + REFERENCES currency_generation_request(id), + CONSTRAINT fk_transaction_identifier_application FOREIGN KEY (identifier_application_id) + REFERENCES institution_identifier_application(id) +); + +CREATE INDEX IF NOT EXISTS idx_transaction_identifier_subject + ON transaction_information_identifier(user_id, school_id, class_id, status, deleted); + +CREATE TABLE IF NOT EXISTS transaction_information_identifier_operation_log ( + id BIGINT PRIMARY KEY, + transaction_identifier_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, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT fk_transaction_identifier_operation FOREIGN KEY (transaction_identifier_id) + REFERENCES transaction_information_identifier(id) +); + +CREATE INDEX IF NOT EXISTS idx_transaction_identifier_operation + ON transaction_information_identifier_operation_log(transaction_identifier_id, created_at); diff --git a/src/main/resources/db/manual/institution_training_logical_delete_upgrade.sql b/src/main/resources/db/manual/institution_training_logical_delete_upgrade.sql new file mode 100644 index 0000000..9e1bcef --- /dev/null +++ b/src/main/resources/db/manual/institution_training_logical_delete_upgrade.sql @@ -0,0 +1,23 @@ +-- 为中国人民银行生成数字货币实验的过程记录表补齐逻辑删除字段。 +-- 已存在的历史记录默认视为未删除。 + +-- 本脚本为一次性升级脚本,请勿重复执行。 +ALTER TABLE institution_identifier_operation_log + ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0, + ADD INDEX idx_identifier_operation_application_deleted (application_id, deleted); + +ALTER TABLE institution_identifier_training_error + ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0, + ADD INDEX idx_identifier_error_application_deleted (application_id, deleted); + +ALTER TABLE institution_sm2_key_audit + ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0, + ADD INDEX idx_institution_key_audit_record_deleted (key_record_id, deleted); + +ALTER TABLE currency_generation_request_operation_log + ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0, + ADD INDEX idx_currency_request_operation_deleted (request_id, deleted); + +ALTER TABLE currency_generation_verification_operation_log + ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0, + ADD INDEX idx_currency_verification_operation_deleted (verification_id, deleted); diff --git a/src/main/resources/db/manual/transaction_information_identifier_upgrade.sql b/src/main/resources/db/manual/transaction_information_identifier_upgrade.sql new file mode 100644 index 0000000..0334424 --- /dev/null +++ b/src/main/resources/db/manual/transaction_information_identifier_upgrade.sql @@ -0,0 +1,49 @@ +-- 模块四“生成交易信息标识”一次性 MySQL 升级脚本。 +-- 适用于已经存在步骤一至步骤三业务表的数据库;执行前请先备份。 + +CREATE TABLE transaction_information_identifier ( + id BIGINT PRIMARY KEY, + verification_id BIGINT NOT NULL, + request_id BIGINT NOT NULL, + identifier_application_id BIGINT NOT NULL, + amount DECIMAL(18, 2) NOT NULL, + institution_identifier VARCHAR(32) NOT NULL, + transaction_timestamp CHAR(14) NOT NULL, + original_text VARCHAR(256) NOT NULL, + digest CHAR(64), + transaction_identifier VARCHAR(80), + status VARCHAR(32) NOT NULL, + user_id VARCHAR(36) NOT NULL, + school_id BIGINT NOT NULL, + class_id BIGINT 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, + CONSTRAINT uk_transaction_identifier_verification UNIQUE + (verification_id, user_id, school_id, class_id), + CONSTRAINT fk_transaction_identifier_verification FOREIGN KEY (verification_id) + REFERENCES currency_generation_verification(id), + CONSTRAINT fk_transaction_identifier_request FOREIGN KEY (request_id) + REFERENCES currency_generation_request(id), + CONSTRAINT fk_transaction_identifier_application FOREIGN KEY (identifier_application_id) + REFERENCES institution_identifier_application(id), + INDEX idx_transaction_identifier_subject + (user_id, school_id, class_id, status, deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块四交易信息标识'; + +CREATE TABLE transaction_information_identifier_operation_log ( + id BIGINT PRIMARY KEY, + transaction_identifier_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, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT fk_transaction_identifier_operation FOREIGN KEY (transaction_identifier_id) + REFERENCES transaction_information_identifier(id), + INDEX idx_transaction_identifier_operation (transaction_identifier_id, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块四交易信息标识操作日志'; diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 4dd495e..de40eec 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -50,6 +50,22 @@ CREATE TABLE IF NOT EXISTS auth_refresh_token ( expires_at TIMESTAMP NOT NULL, revoked_at TIMESTAMP NULL ); +-- +-- CREATE TABLE IF NOT EXISTS stu_module_score_details ( +-- id VARCHAR(64) NOT NULL, +-- moudule VARCHAR(100) NOT NULL, +-- learning_projects VARCHAR(100), +-- assessment_items VARCHAR(100), +-- scoring_criteria VARCHAR(100), +-- completion_status VARCHAR(100), +-- score_project DOUBLE, +-- user_id VARCHAR(64) NOT NULL, +-- total_score DECIMAL(10, 2) NOT NULL DEFAULT 100.00, +-- serial_number INT, +-- schedule DOUBLE, +-- PRIMARY KEY (id), +-- CONSTRAINT uk_user_moudule_serial UNIQUE (user_id, moudule, serial_number) +-- ); CREATE TABLE IF NOT EXISTS institution_sm2_key ( id BIGINT PRIMARY KEY, @@ -80,6 +96,7 @@ CREATE TABLE IF NOT EXISTS institution_sm2_key_audit ( operation_detail VARCHAR(512) NOT NULL, created_at TIMESTAMP NOT NULL, created_by VARCHAR(64) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT fk_institution_key_audit_key FOREIGN KEY (key_record_id) REFERENCES institution_sm2_key(id) ); diff --git a/src/test/java/com/yau/digitalrmb/institutionidentity/InstitutionIdentifierControllerTest.java b/src/test/java/com/yau/digitalrmb/institutionidentity/InstitutionIdentifierControllerTest.java index 1b5a085..c31d316 100644 --- a/src/test/java/com/yau/digitalrmb/institutionidentity/InstitutionIdentifierControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/institutionidentity/InstitutionIdentifierControllerTest.java @@ -12,6 +12,8 @@ import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifi import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionPrivateKeyCipher; import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyEntity; import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper; import com.yau.digitalrmb.security.application.JwtTokenService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -64,6 +66,9 @@ class InstitutionIdentifierControllerTest { @Autowired private InstitutionKeyService keyService; + @Autowired + private StuModuleScoreDetailsMapper scoreDetailsMapper; + @Autowired private JdbcTemplate jdbcTemplate; @@ -73,11 +78,16 @@ class InstitutionIdentifierControllerTest { @BeforeEach void setUp() { - trainingErrorMapper.delete(null); - operationLogMapper.delete(null); - applicationMapper.delete(null); + jdbcTemplate.update("DELETE FROM currency_generation_verification_operation_log"); + jdbcTemplate.update("DELETE FROM currency_generation_verification"); + jdbcTemplate.update("DELETE FROM currency_generation_request_operation_log"); + jdbcTemplate.update("DELETE FROM currency_generation_request"); + jdbcTemplate.update("DELETE FROM institution_identifier_training_error"); + jdbcTemplate.update("DELETE FROM institution_identifier_operation_log"); + jdbcTemplate.update("DELETE FROM institution_identifier_application"); jdbcTemplate.update("DELETE FROM institution_sm2_key_audit"); jdbcTemplate.update("DELETE FROM institution_sm2_key"); + jdbcTemplate.update("DELETE FROM stu_module_score_details"); keyAuthorization = "Bearer " + jwtTokenService.issueFor(USER_ID, 1001L, 2001L, "institution-key-tester", Collections.singleton("STUDENT")).accessToken(); authorization = keyAuthorization; @@ -119,6 +129,109 @@ class InstitutionIdentifierControllerTest { assertThat(privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey())).isEqualTo(returnedPrivateKey); } + @Test + void allInstitutionTrainingTablesHaveLogicalDeleteAndMapperDeleteHidesRows() throws Exception { + String[] tables = { + "institution_identifier_application", + "institution_identifier_operation_log", + "institution_identifier_training_error", + "institution_sm2_key", + "institution_sm2_key_audit", + "currency_generation_request", + "currency_generation_request_operation_log", + "currency_generation_verification", + "currency_generation_verification_operation_log" + }; + for (String table : tables) { + Integer columnCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS " + + "WHERE LOWER(TABLE_NAME) = ? AND LOWER(COLUMN_NAME) = 'deleted'", + Integer.class, table); + assertThat(columnCount).as(table + " must have deleted column").isEqualTo(1); + } + + mvc.perform(post(BASE_PATH + "/steps/digest") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"bankCode\":\"BKCHCNBJ20100\"}")) + .andExpect(status().isOk()); + mvc.perform(post(BASE_PATH + "/steps/sign") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"privateKey\":\"WRONG\"}")) + .andExpect(status().isBadRequest()); + + InstitutionIdentifierTrainingErrorEntity error = trainingErrorMapper.selectList(null).get(0); + assertThat(trainingErrorMapper.deleteById(error.getId())).isEqualTo(1); + assertThat(trainingErrorMapper.selectById(error.getId())).isNull(); + Boolean deleted = jdbcTemplate.queryForObject( + "SELECT deleted FROM institution_identifier_training_error WHERE id = ?", + Boolean.class, error.getId()); + assertThat(deleted).isTrue(); + } + + @Test + void signAcceptsCommercialBankSecondPrivateKeyReturnedByKeyEndpoint() throws Exception { + mvc.perform(post(BASE_PATH + "/steps/digest") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"bankCode\":\"BKCHCNBJ20101\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("DIGESTED")); + + InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream() + .filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId())) + .findFirst().orElseThrow(AssertionError::new); + String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey()); + + mvc.perform(post(BASE_PATH + "/steps/sign") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"privateKey\":\"" + privateKey + "\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("SIGNED")) + .andExpect(jsonPath("$.data.keyId").value(InstitutionKeyService.BANK_SECOND_KEY)) + .andExpect(jsonPath("$.data.signature").isNotEmpty()); + } + + @Test + void signRejectsWrongPrivateKeyWithoutPersistingPrivateKeyValue() throws Exception { + mvc.perform(post(BASE_PATH + "/steps/digest") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"bankCode\":\"BKCHCNBJ20102\"}")) + .andExpect(status().isOk()); + + String malformedPrivateKey = "1173B8C5C558BAE5C2C9EB89FD5C30BCB7DA8B478B"; + mvc.perform(post(BASE_PATH + "/steps/sign") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"privateKey\":\"" + malformedPrivateKey + "\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("VALIDATION_ERROR")) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "签名必须选择商业银行第二私钥"))) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误"))); + + String wrongPrivateKey = "0000000000000000000000000000000000000000000000000000000000000001"; + mvc.perform(post(BASE_PATH + "/steps/sign") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"privateKey\":\"" + wrongPrivateKey + "\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误"))); + + InstitutionIdentifierApplicationEntity application = applicationMapper.selectOne(null); + assertThat(application.getScoringCriteria()).isEqualTo(2); + List errors = trainingErrorMapper.selectList(null); + assertThat(errors).hasSize(2); + assertThat(errors).extracting(InstitutionIdentifierTrainingErrorEntity::getErrorType, + InstitutionIdentifierTrainingErrorEntity::getProvidedKeyId) + .containsOnly( + org.assertj.core.groups.Tuple.tuple("WRONG_PRIVATE_KEY", null), + org.assertj.core.groups.Tuple.tuple("WRONG_PRIVATE_KEY", null)); + } + @Test void logicalDeleteIsScopedToTokenSubjectAndAllowsRegeneration() throws Exception { String firstResponse = mvc.perform(post(BASE_PATH + "/keys/commercial-bank") @@ -127,10 +240,8 @@ class InstitutionIdentifierControllerTest { .andReturn().getResponse().getContentAsString(); Number firstRecordId = JsonPath.read(firstResponse, "$.data.recordId"); - mvc.perform(delete(BASE_PATH + "/keys/commercial-bank") - .header("Authorization", keyAuthorization)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data").doesNotExist()); + keyService.logicalDelete(new InstitutionKeySubject(USER_ID, 1001L, 2001L), + InstitutionKeyService.BANK_SECOND_KEY, "institution-key-tester"); Integer deleted = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM institution_sm2_key WHERE id = ? AND deleted = TRUE", @@ -156,7 +267,7 @@ class InstitutionIdentifierControllerTest { @Test void protectedStepRejectsMissingJwt() throws Exception { - mvc.perform(post(BASE_PATH + "/steps/prepare") + mvc.perform(post(BASE_PATH + "/steps/digest") .contentType(MediaType.APPLICATION_JSON) .content("{\"bankCode\":\"BKCHCNBJ20001\"}")) .andExpect(status().isUnauthorized()) @@ -164,56 +275,36 @@ class InstitutionIdentifierControllerTest { } @Test - void skippedStepReturnsBadRequestAndDoesNotChangePersistedState() throws Exception { - long applicationId = prepare("BKCHCNBJ20002"); - + void signWithoutCurrentDigestedRoundReturnsResourceNotFoundAndDoesNotCreateErrorRecord() throws Exception { mvc.perform(post(BASE_PATH + "/steps/sign") .header("Authorization", authorization) .contentType(MediaType.APPLICATION_JSON) - .content(stepRequest(applicationId, InstitutionKeyService.BANK_SECOND_KEY))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("VALIDATION_ERROR")) - .andExpect(jsonPath("$.message") - .value(org.hamcrest.Matchers.containsString("当前步骤不是商业银行签名,不能操作"))); - - InstitutionIdentifierApplicationEntity application = applicationMapper.selectById(applicationId); - assertThat(application.getStatus()).isEqualTo("PREPARED"); - assertThat(application.getDigest()).isNull(); - assertThat(application.getBankSignature()).isNull(); - assertThat(application.getScoringCriteria()).isEqualTo(1); - - List errors = trainingErrorMapper.selectList(null); - assertThat(errors).filteredOn(error -> error.getApplicationId().equals(applicationId)) - .extracting(InstitutionIdentifierTrainingErrorEntity::getErrorSequence, - InstitutionIdentifierTrainingErrorEntity::getErrorType) - .containsExactly(org.assertj.core.groups.Tuple.tuple(1, "INVALID_STEP")); - - List logs = operationLogMapper.selectList(null); - assertThat(logs).filteredOn(log -> log.getApplicationId().equals(applicationId)) - .extracting(InstitutionIdentifierOperationLogEntity::getOperation) - .containsExactly("PREPARE"); + .content("{\"privateKey\":\"WRONG\"}")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("RESOURCE_NOT_FOUND")) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "当前没有待签名的本轮机构标识数据"))); + + assertThat(applicationMapper.selectList(null)).isEmpty(); + assertThat(trainingErrorMapper.selectList(null)).isEmpty(); + assertThat(operationLogMapper.selectList(null)).isEmpty(); } @Test void wrongKeysIncreaseScoringCriteriaAndRecordErrorSequence() throws Exception { - long applicationId = prepare("BKCHCNBJ20004"); - mvc.perform(post(BASE_PATH + "/steps/digest") - .header("Authorization", authorization) - .contentType(MediaType.APPLICATION_JSON) - .content(stepRequest(applicationId, null))) - .andExpect(status().isOk()); + long applicationId = digest("BKCHCNBJ20004"); mvc.perform(post(BASE_PATH + "/steps/sign") .header("Authorization", authorization) .contentType(MediaType.APPLICATION_JSON) - .content(stepRequest(applicationId, "WRONG_PRIVATE_KEY"))) + .content("{\"privateKey\":\"WRONG_PRIVATE_KEY\"}")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误"))); mvc.perform(post(BASE_PATH + "/steps/sign") .header("Authorization", authorization) .contentType(MediaType.APPLICATION_JSON) - .content(stepRequest(applicationId, "ANOTHER_WRONG_KEY"))) + .content("{\"privateKey\":\"ANOTHER_WRONG_KEY\"}")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误"))); @@ -225,14 +316,23 @@ class InstitutionIdentifierControllerTest { .extracting(InstitutionIdentifierTrainingErrorEntity::getErrorSequence, InstitutionIdentifierTrainingErrorEntity::getProvidedKeyId) .containsExactly( - org.assertj.core.groups.Tuple.tuple(1, "WRONG_PRIVATE_KEY"), - org.assertj.core.groups.Tuple.tuple(2, "ANOTHER_WRONG_KEY")); + org.assertj.core.groups.Tuple.tuple(1, null), + org.assertj.core.groups.Tuple.tuple(2, null)); + + StuModuleScoreDetailsEntity score = onlyScoreDetail(); + assertThat(score.getMoudule()).isEqualTo("中国人民银行生成数字货币"); + assertThat(score.getLearningProjects()).isEqualTo("实验实训"); + assertThat(score.getSerialNumber()).isEqualTo(3); + assertThat(score.getCompletionStatus()).isEqualTo("2"); + assertThat(score.getScoringCriteria()).isEqualTo("0.5"); + assertThat(score.getSchedule()).isZero(); + assertThat(score.getScoreProject()).isZero(); } @Test void prepareSameBankAgainCreatesNewTrainingRoundWithoutOverwritingHistory() throws Exception { - long firstApplicationId = prepare("BKCHCNBJ20005"); - long secondApplicationId = prepare("BKCHCNBJ20005"); + long firstApplicationId = digest("BKCHCNBJ20005"); + long secondApplicationId = digest("BKCHCNBJ20005"); assertThat(secondApplicationId).isNotEqualTo(firstApplicationId); InstitutionIdentifierApplicationEntity first = applicationMapper.selectById(firstApplicationId); @@ -244,88 +344,254 @@ class InstitutionIdentifierControllerTest { } @Test - void completeApiWorkflowUsesApplicationIdAndPersistsIssuedResult() throws Exception { - long applicationId = prepare("BKCHCNBJ20003"); + void completeApiWorkflowUsesCurrentTrainingRoundAndPersistsIssuedResult() throws Exception { + long applicationId = digest("BKCHCNBJ20003"); - mvc.perform(post(BASE_PATH + "/steps/digest") - .header("Authorization", authorization) - .contentType(MediaType.APPLICATION_JSON) - .content(stepRequest(applicationId, null))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.applicationId").value(applicationId)) - .andExpect(jsonPath("$.data.status").value("DIGESTED")) - .andExpect(jsonPath("$.data.algorithm").value("SM3")) - .andExpect(jsonPath("$.data.digest").isNotEmpty()); + mvc.perform(get(BASE_PATH + "/applications/commercial-bank") + .header("Authorization", authorization)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "商业银行第二私钥签名成功后才会出现待确认数据"))); + + InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream() + .filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId())) + .findFirst().orElseThrow(AssertionError::new); + String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey()); mvc.perform(post(BASE_PATH + "/steps/sign") .header("Authorization", authorization) .contentType(MediaType.APPLICATION_JSON) - .content(stepRequest(applicationId, InstitutionKeyService.BANK_SECOND_KEY))) + .content("{\"privateKey\":\"" + privateKey + "\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("SIGNED")) .andExpect(jsonPath("$.data.signatureAlgorithm").value("SM2_WITH_SM3")) .andExpect(jsonPath("$.data.signatureHashAlgorithm").value("SM3")); + mvc.perform(get(BASE_PATH + "/applications/commercial-bank") + .header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value(1)) + .andExpect(jsonPath("$.data.statusText").value("待确认")) + .andExpect(jsonPath("$.data.applicationStatus").value("SIGNED")) + .andExpect(jsonPath("$.data.institutionIdentifier") + .value(org.hamcrest.Matchers.nullValue())); + mvc.perform(post(BASE_PATH + "/steps/package") - .header("Authorization", authorization) - .contentType(MediaType.APPLICATION_JSON) - .content(stepRequest(applicationId, null))) + .header("Authorization", authorization)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("PACKAGED")); - mvc.perform(post(BASE_PATH + "/applications/verification") + mvc.perform(post(BASE_PATH + "/applications/decryption-verification") .header("Authorization", authorization) .contentType(MediaType.APPLICATION_JSON) - .content("{\"applicationId\":" + applicationId + ",\"verificationKeyId\":\"" - + InstitutionKeyService.BANK_SECOND_KEY + "\"}")) + .content("{\"keyId\":\"" + InstitutionKeyService.BANK_SECOND_KEY + "\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("VERIFIED")) + .andExpect(jsonPath("$.data.verificationKeyId") + .value(InstitutionKeyService.BANK_SECOND_KEY)) .andExpect(jsonPath("$.data.signatureValid").value(true)) .andExpect(jsonPath("$.data.digestMatches").value(true)); mvc.perform(post(BASE_PATH + "/applications/confirmation") - .header("Authorization", authorization) - .contentType(MediaType.APPLICATION_JSON) - .content("{\"applicationId\":" + applicationId + ",\"signingKeyId\":\"" - + InstitutionKeyService.CENTRAL_FIRST_KEY + "\"}")) + .header("Authorization", authorization)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("ISSUED")) + .andExpect(jsonPath("$.data.signingKeyId") + .value(InstitutionKeyService.CENTRAL_FIRST_KEY)) .andExpect(jsonPath("$.data.signatureAlgorithm").value("SM2_WITH_SM3")) .andExpect(jsonPath("$.data.institutionIdentifier").value(org.hamcrest.Matchers.startsWith("ORG_"))); - InstitutionIdentifierApplicationEntity issued = applicationMapper.selectById(applicationId); - assertThat(issued.getStatus()).isEqualTo("ISSUED"); - assertThat(issued.getDigest()).matches("[0-9A-F]{64}"); - assertThat(issued.getBankSignature()).isNotBlank(); - assertThat(issued.getCentralBankSignature()).isNotBlank(); - assertThat(issued.getInstitutionIdentifier()).startsWith("ORG_"); + mvc.perform(get(BASE_PATH + "/applications/final-identifier") + .header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.applicationStatus").value("ISSUED")) + .andExpect(jsonPath("$.data.feedbackStatus").value(1)) + .andExpect(jsonPath("$.data.feedbackStatusText").value("待反馈")) + .andExpect(jsonPath("$.data.institutionIdentifier") + .value(org.hamcrest.Matchers.startsWith("ORG_"))) + .andExpect(jsonPath("$.data.fullInstitutionIdentifier") + .value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))); + + mvc.perform(get(BASE_PATH + "/applications/commercial-bank") + .header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value(1)) + .andExpect(jsonPath("$.data.statusText").value("待确认")) + .andExpect(jsonPath("$.data.applicationStatus").value("ISSUED")) + .andExpect(jsonPath("$.data.institutionIdentifier") + .value(org.hamcrest.Matchers.nullValue())); + + mvc.perform(post(BASE_PATH + "/currency-requests/steps/prepare") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"identifierApplicationId\":" + applicationId + + ",\"amount\":1000.00,\"deliveryNodeCode\":\"SYS_DC_001\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "机构标识尚未反馈到商业银行端"))); + + mvc.perform(post(BASE_PATH + "/applications/feedback") + .header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.applicationStatus").value("FEEDBACKED")) + .andExpect(jsonPath("$.data.feedbackStatus").value(2)) + .andExpect(jsonPath("$.data.feedbackStatusText").value("已反馈")); + + mvc.perform(get(BASE_PATH + "/applications/commercial-bank") + .header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value(2)) + .andExpect(jsonPath("$.data.statusText").value("已确认")) + .andExpect(jsonPath("$.data.applicationStatus").value("FEEDBACKED")) + .andExpect(jsonPath("$.data.institutionIdentifier") + .value(org.hamcrest.Matchers.startsWith("ORG_"))) + .andExpect(jsonPath("$.data.fullInstitutionIdentifier") + .value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))); + + mvc.perform(post(BASE_PATH + "/currency-requests/steps/prepare") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"identifierApplicationId\":" + applicationId + + ",\"amount\":1000.00,\"deliveryNodeCode\":\"SYS_DC_001\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("PREPARED")) + .andExpect(jsonPath("$.data.identifierApplicationId").value(applicationId)) + .andExpect(jsonPath("$.data.institutionIdentifier") + .value(org.hamcrest.Matchers.startsWith("ORG_"))) + .andExpect(jsonPath("$.data.fullInstitutionIdentifier") + .value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))); + + InstitutionIdentifierApplicationEntity feedbacked = applicationMapper.selectById(applicationId); + assertThat(feedbacked.getStatus()).isEqualTo("FEEDBACKED"); + assertThat(feedbacked.getDigest()).matches("[0-9A-F]{64}"); + assertThat(feedbacked.getBankSignature()).isNotBlank(); + assertThat(feedbacked.getCentralBankSignature()).isNotBlank(); + assertThat(feedbacked.getInstitutionIdentifier()).startsWith("ORG_"); + + StuModuleScoreDetailsEntity score = onlyScoreDetail(); + assertThat(score.getCompletionStatus()).isEqualTo("0"); + assertThat(score.getSchedule()).isEqualTo(11.11); + assertThat(score.getScoreProject()).isEqualTo(11.11); List logs = operationLogMapper.selectList(null); assertThat(logs).filteredOn(log -> log.getApplicationId().equals(applicationId)) .extracting(InstitutionIdentifierOperationLogEntity::getOperation) - .containsExactly("PREPARE", "DIGEST", "SIGN", "PACKAGE", "VERIFY", "CONFIRM"); + .containsExactly("PREPARE", "DIGEST", "SIGN", "PACKAGE", "DECRYPT_VERIFY", "CONFIRM", "FEEDBACK"); } - private long prepare(String bankCode) throws Exception { - String response = mvc.perform(post(BASE_PATH + "/steps/prepare") + @Test + void centralBankDecryptionVerificationRejectsWrongPublicKeyAndKeepsPackagedStatus() throws Exception { + long applicationId = digest("BKCHCNBJ20104"); + InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream() + .filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId())) + .findFirst().orElseThrow(AssertionError::new); + String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey()); + + mvc.perform(post(BASE_PATH + "/steps/sign") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"privateKey\":\"" + privateKey + "\"}")) + .andExpect(status().isOk()); + mvc.perform(post(BASE_PATH + "/steps/package") + .header("Authorization", authorization)) + .andExpect(status().isOk()); + + mvc.perform(post(BASE_PATH + "/applications/decryption-verification") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"keyId\":\"" + InstitutionKeyService.CENTRAL_FIRST_KEY + "\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("VALIDATION_ERROR")) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "解密验证必须选择商业银行第二公钥"))) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误"))); + + InstitutionIdentifierApplicationEntity packaged = applicationMapper.selectById(applicationId); + assertThat(packaged.getStatus()).isEqualTo("PACKAGED"); + InstitutionIdentifierTrainingErrorEntity error = trainingErrorMapper.selectList(null).get(0); + assertThat(error.getErrorType()).isEqualTo("WRONG_PUBLIC_KEY"); + assertThat(error.getOperationStep()).isEqualTo("DECRYPT_VERIFY"); + assertThat(error.getProvidedKeyId()).isEqualTo(InstitutionKeyService.CENTRAL_FIRST_KEY); + } + + @Test + void confirmationValidatesCentralBankPrivateKeyOnlyWhenProvided() throws Exception { + long applicationId = advanceToVerified("BKCHCNBJ20103"); + + mvc.perform(post(BASE_PATH + "/applications/confirmation") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"privateKey\":\"WRONG\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("VALIDATION_ERROR")) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "签名确权必须使用中央银行第一私钥"))) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误"))); + + InstitutionIdentifierApplicationEntity verified = applicationMapper.selectById(applicationId); + assertThat(verified.getStatus()).isEqualTo("VERIFIED"); + assertThat(verified.getScoringCriteria()).isEqualTo(1); + InstitutionIdentifierTrainingErrorEntity error = trainingErrorMapper.selectList(null).get(0); + assertThat(error.getErrorType()).isEqualTo("WRONG_PRIVATE_KEY"); + assertThat(error.getProvidedKeyId()).isNull(); + + InstitutionSm2KeyEntity centralBankKey = keyMapper.selectList(null).stream() + .filter(key -> InstitutionKeyService.CENTRAL_FIRST_KEY.equals(key.getKeyId())) + .findFirst().orElseThrow(AssertionError::new); + String privateKey = privateKeyCipher.decrypt(centralBankKey.getEncryptedPrivateKey()); + + mvc.perform(post(BASE_PATH + "/applications/confirmation") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"privateKey\":\"" + privateKey + "\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("ISSUED")) + .andExpect(jsonPath("$.data.signingKeyId") + .value(InstitutionKeyService.CENTRAL_FIRST_KEY)); + } + + private long advanceToVerified(String bankCode) throws Exception { + long applicationId = digest(bankCode); + InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream() + .filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId())) + .findFirst().orElseThrow(AssertionError::new); + String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey()); + + mvc.perform(post(BASE_PATH + "/steps/sign") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"privateKey\":\"" + privateKey + "\"}")) + .andExpect(status().isOk()); + mvc.perform(post(BASE_PATH + "/steps/package") + .header("Authorization", authorization)) + .andExpect(status().isOk()); + mvc.perform(post(BASE_PATH + "/applications/decryption-verification") + .header("Authorization", authorization) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"keyId\":\"" + InstitutionKeyService.BANK_SECOND_KEY + "\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("VERIFIED")); + return applicationId; + } + + private StuModuleScoreDetailsEntity onlyScoreDetail() { + List details = scoreDetailsMapper.selectList(null); + assertThat(details).hasSize(1); + return details.get(0); + } + + private long digest(String bankCode) throws Exception { + String response = mvc.perform(post(BASE_PATH + "/steps/digest") .header("Authorization", authorization) .contentType(MediaType.APPLICATION_JSON) .content("{\"bankCode\":\"" + bankCode + "\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.applicationId").isNumber()) - .andExpect(jsonPath("$.data.status").value("PREPARED")) - .andExpect(jsonPath("$.data.bankCode").value(bankCode)) - .andExpect(jsonPath("$.data.trainingRound").isNumber()) - .andExpect(jsonPath("$.data.scoringCriteria").value(0)) + .andExpect(jsonPath("$.data.status").value("DIGESTED")) + .andExpect(jsonPath("$.data.algorithm").value("SM3")) + .andExpect(jsonPath("$.data.digest").isNotEmpty()) .andReturn().getResponse().getContentAsString(); Number applicationId = JsonPath.read(response, "$.data.applicationId"); return applicationId.longValue(); } - - private String stepRequest(long applicationId, String keyId) { - if (keyId == null) { - return "{\"applicationId\":" + applicationId + "}"; - } - return "{\"applicationId\":" + applicationId + ",\"keyId\":\"" + keyId + "\"}"; - } } diff --git a/src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdentifierControllerTest.java b/src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdentifierControllerTest.java new file mode 100644 index 0000000..de0bfaa --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdentifierControllerTest.java @@ -0,0 +1,266 @@ +package com.yau.digitalrmb.institutionidentity; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationRequest; +import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationVerification; +import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierOperationLogEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierOperationLogMapper; +import com.yau.digitalrmb.security.application.JwtTokenService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class TransactionInformationIdentifierControllerTest { + private static final String BASE_PATH = + "/api/v1/institution-identifiers/transaction-information-identifiers"; + private static final String USER_ID = "00000000-0000-0000-0000-000000009004"; + private static final long SCHOOL_ID = 1001L; + private static final long CLASS_ID = 2001L; + + @Autowired private MockMvc mvc; + @Autowired private JwtTokenService jwtTokenService; + @Autowired private JdbcTemplate jdbcTemplate; + @Autowired private InstitutionIdentifierApplicationMapper applicationMapper; + @Autowired private CurrencyGenerationRequestMapper requestMapper; + @Autowired private CurrencyGenerationVerificationMapper verificationMapper; + @Autowired private TransactionInformationIdentifierMapper transactionMapper; + @Autowired private TransactionInformationIdentifierOperationLogMapper transactionLogMapper; + @Autowired private StuModuleScoreDetailsMapper scoreMapper; + + private String authorization; + + @BeforeEach + void setUp() { + jdbcTemplate.update("DELETE FROM transaction_information_identifier_operation_log"); + jdbcTemplate.update("DELETE FROM transaction_information_identifier"); + jdbcTemplate.update("DELETE FROM currency_generation_verification_operation_log"); + jdbcTemplate.update("DELETE FROM currency_generation_verification"); + jdbcTemplate.update("DELETE FROM currency_generation_request_operation_log"); + jdbcTemplate.update("DELETE FROM currency_generation_request"); + jdbcTemplate.update("DELETE FROM institution_identifier_training_error"); + jdbcTemplate.update("DELETE FROM institution_identifier_operation_log"); + jdbcTemplate.update("DELETE FROM institution_identifier_application"); + jdbcTemplate.update("DELETE FROM institution_sm2_key_audit"); + jdbcTemplate.update("DELETE FROM institution_sm2_key"); + jdbcTemplate.update("DELETE FROM stu_module_score_details"); + authorization = "Bearer " + jwtTokenService.issueFor(USER_ID, SCHOOL_ID, CLASS_ID, + "module-four-tester", Collections.singleton("STUDENT")).accessToken(); + } + + @Test + void rejectsModuleFourWhenStepThreeHasNotReturnedResponse() throws Exception { + mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "步骤三尚未完成确认报文返回"))); + assertThat(transactionMapper.selectList(null)).isEmpty(); + } + + @Test + void flowsCompletedPreviousDataAndPersistsEveryModuleFourStep() throws Exception { + PrecedingData preceding = insertCompletedPrecedingData(); + + mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("PREPARED")) + .andExpect(jsonPath("$.data.verificationId").value(preceding.verificationId)) + .andExpect(jsonPath("$.data.requestId").value(preceding.requestId)) + .andExpect(jsonPath("$.data.identifierApplicationId").value(preceding.applicationId)) + .andExpect(jsonPath("$.data.amount").value(1589.85)) + .andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F")) + .andExpect(jsonPath("$.data.transactionTimestamp") + .value(org.hamcrest.Matchers.matchesPattern("\\d{14}"))) + .andExpect(jsonPath("$.data.originalText") + .value(org.hamcrest.Matchers.matchesPattern( + "1589\\.85\\|ORG_3A4B5C6D7E8F\\|\\d{14}"))) + .andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.nullValue())) + .andExpect(jsonPath("$.data.transactionIdentifier") + .value(org.hamcrest.Matchers.nullValue())); + + mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "请先使用SM3生成摘要"))); + + mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("DIGESTED")) + .andExpect(jsonPath("$.data.digestAlgorithm").value("SM3")) + .andExpect(jsonPath("$.data.digest") + .value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))); + + mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("ASSEMBLED")) + .andExpect(jsonPath("$.data.transactionIdentifier") + .value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}"))); + + mvc.perform(get(BASE_PATH).header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("ASSEMBLED")) + .andExpect(jsonPath("$.data.transactionIdentifier") + .value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}"))); + + List records = transactionMapper.selectList(null); + assertThat(records).hasSize(1); + TransactionInformationIdentifierEntity persisted = records.get(0); + assertThat(persisted.getVerificationId()).isEqualTo(preceding.verificationId); + assertThat(persisted.getRequestId()).isEqualTo(preceding.requestId); + assertThat(persisted.getIdentifierApplicationId()).isEqualTo(preceding.applicationId); + assertThat(persisted.getAmount()).isEqualByComparingTo("1589.85"); + assertThat(persisted.getInstitutionIdentifier()).isEqualTo("ORG_3A4B5C6D7E8F"); + assertThat(persisted.getOriginalText()).isEqualTo("1589.85|ORG_3A4B5C6D7E8F|" + + persisted.getTransactionTimestamp()); + assertThat(persisted.getDigest()).matches("[0-9A-F]{64}"); + assertThat(persisted.getTransactionIdentifier()).isEqualTo("TXN_" + persisted.getDigest()); + assertThat(persisted.getStatus()).isEqualTo("ASSEMBLED"); + + assertThat(transactionLogMapper.selectList(null)) + .extracting(TransactionInformationIdentifierOperationLogEntity::getOperation) + .containsExactly("PREPARE", "DIGEST", "ASSEMBLE"); + + StuModuleScoreDetailsEntity score = scoreMapper.selectOne( + new LambdaQueryWrapper() + .eq(StuModuleScoreDetailsEntity::getUserId, USER_ID) + .eq(StuModuleScoreDetailsEntity::getSerialNumber, 3)); + assertThat(score).isNotNull(); + assertThat(score.getSchedule()).isEqualTo(44.44); + assertThat(score.getScoreProject()).isEqualTo(44.44); + } + + @Test + void preventsDuplicatePreparationForSameCompletedVerification() throws Exception { + insertCompletedPrecedingData(); + mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization)) + .andExpect(status().isOk()); + mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( + "不能重复准备"))); + assertThat(transactionMapper.selectList(null)).hasSize(1); + } + + private PrecedingData insertCompletedPrecedingData() { + LocalDateTime now = LocalDateTime.now(); + InstitutionIdentifierApplicationEntity application = new InstitutionIdentifierApplicationEntity(); + application.setBankCode("BKCHCNBJ40001"); + application.setUserId(USER_ID); + application.setSchoolId(SCHOOL_ID); + application.setClassId(CLASS_ID); + application.setRequestTimestamp("20260805180000"); + application.setDigest("3A4B5C6D7E8F0000000000000000000000000000000000000000000000000000"); + application.setBankSignature("3044AABB"); + application.setStatus("FEEDBACKED"); + application.setVerificationKeyId(InstitutionKeyService.BANK_SECOND_KEY); + application.setSignatureValid(true); + application.setDigestMatches(true); + application.setSigningKeyId(InstitutionKeyService.CENTRAL_FIRST_KEY); + application.setInstitutionIdentifier("ORG_3A4B5C6D7E8F"); + application.setCentralBankSignature("3044CCDD"); + application.setTrainingRound(1); + application.setScoringCriteria(0); + application.setCreatedAt(now); + application.setUpdatedAt(now); + application.setCreatedBy("module-four-tester"); + application.setUpdatedBy("module-four-tester"); + application.setDeleted(false); + applicationMapper.insert(application); + + CurrencyGenerationRequestEntity request = new CurrencyGenerationRequestEntity(); + request.setIdentifierApplicationId(application.getId()); + request.setInstitutionIdentifier(application.getInstitutionIdentifier()); + request.setFullInstitutionIdentifier(application.getDigest()); + request.setAmount(new BigDecimal("1589.85")); + request.setDeliveryNodeCode("SYS_DC_001"); + request.setRequestTimestamp("20260805180100"); + request.setRequestOriginalText("ORG_3A4B5C6D7E8F|1589.85|SYS_DC_001|20260805180100"); + request.setRequestDigest("7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F"); + request.setBankSignature("3044AABB"); + request.setSigningKeyId(InstitutionKeyService.BANK_SECOND_KEY); + request.setRequestMessage("{request:true}"); + request.setStatus(CurrencyGenerationRequest.Status.RECEIVED.name()); + request.setUserId(USER_ID); + request.setSchoolId(SCHOOL_ID); + request.setClassId(CLASS_ID); + request.setCreatedAt(now); + request.setUpdatedAt(now); + request.setCreatedBy("module-four-tester"); + request.setUpdatedBy("module-four-tester"); + request.setDeleted(false); + requestMapper.insert(request); + + CurrencyGenerationVerificationEntity verification = new CurrencyGenerationVerificationEntity(); + verification.setRequestId(request.getId()); + verification.setRequestMessage(request.getRequestMessage()); + verification.setRequestOriginalText(request.getRequestOriginalText()); + verification.setRequestDigest(request.getRequestDigest()); + verification.setBankSignature(request.getBankSignature()); + verification.setBankKeyId(request.getSigningKeyId()); + verification.setAmount(request.getAmount()); + verification.setSignatureValid(true); + verification.setRecomputedDigest(request.getRequestDigest()); + verification.setDigestMatches(true); + verification.setTotalQuota(new BigDecimal("500000000.00")); + verification.setUsedQuota(new BigDecimal("495000000.00")); + verification.setRemainingQuota(new BigDecimal("5000000.00")); + verification.setQuotaSufficient(true); + verification.setCentralBankKeyId(InstitutionKeyService.CENTRAL_FIRST_KEY); + verification.setCentralBankSignature("3044CCDD"); + verification.setConfirmationTime("20260805180200"); + verification.setConfirmationMessage("{confirmed:true}"); + verification.setStatus(CurrencyGenerationVerification.Status.RESPONSE_RETURNED.name()); + verification.setUserId(USER_ID); + verification.setSchoolId(SCHOOL_ID); + verification.setClassId(CLASS_ID); + verification.setCreatedAt(now); + verification.setUpdatedAt(now); + verification.setCreatedBy("module-four-tester"); + verification.setUpdatedBy("module-four-tester"); + verification.setDeleted(false); + verificationMapper.insert(verification); + return new PrecedingData(application.getId(), request.getId(), verification.getId()); + } + + private static class PrecedingData { + private final long applicationId; + private final long requestId; + private final long verificationId; + + private PrecedingData(long applicationId, long requestId, long verificationId) { + this.applicationId = applicationId; + this.requestId = requestId; + this.verificationId = verificationId; + } + } +} diff --git a/src/test/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationConfigTest.java b/src/test/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationConfigTest.java new file mode 100644 index 0000000..d7a4548 --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/platformintegration/config/PlatformIntegrationConfigTest.java @@ -0,0 +1,31 @@ +package com.yau.digitalrmb.platformintegration.config; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +class PlatformIntegrationConfigTest { + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner().withUserConfiguration(PlatformIntegrationConfig.class); + + @Test + void doesNotBindOrValidatePropertiesWhenIntegrationIsDisabled() { + contextRunner + .withPropertyValues("platform-integration.enabled=false") + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context).doesNotHaveBean(PlatformIntegrationProperties.class); + }); + } + + @Test + void validatesRequiredPropertiesWhenIntegrationIsEnabled() { + contextRunner + .withPropertyValues("platform-integration.enabled=true") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasMessageContaining("platform-integration"); + }); + } +} diff --git a/src/test/java/com/yau/digitalrmb/shared/SwaggerDocumentationTest.java b/src/test/java/com/yau/digitalrmb/shared/SwaggerDocumentationTest.java new file mode 100644 index 0000000..1bd034f --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/shared/SwaggerDocumentationTest.java @@ -0,0 +1,43 @@ +package com.yau.digitalrmb.shared; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@TestPropertySource(properties = { + "springdoc.swagger-ui.enabled=true", + "knife4j.enable=true" +}) +class SwaggerDocumentationTest { + @Autowired + private MockMvc mvc; + + @Test + void docHtmlAndOpenApiJsonArePubliclyAvailable() throws Exception { + mvc.perform(get("/doc.html")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")) + .andExpect(content().string(org.hamcrest.Matchers.containsString("knife4j"))); + + mvc.perform(get("/v3/api-docs")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.info.title").value("数字人民币教学仿真后端")) + .andExpect(jsonPath("$.components.securitySchemes.Authorization.type").value("apiKey")) + .andExpect(jsonPath("$.components.securitySchemes.Authorization.in").value("header")) + .andExpect(jsonPath("$.components.securitySchemes.Authorization.name").value("Authorization")) + .andExpect(jsonPath("$.security[0].Authorization").isArray()); + } +} diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 4a632da..b5ebd3f 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -48,6 +48,22 @@ CREATE TABLE IF NOT EXISTS auth_refresh_token ( revoked_at TIMESTAMP NULL ); +CREATE TABLE IF NOT EXISTS stu_module_score_details ( + id VARCHAR(64) NOT NULL, + moudule VARCHAR(100) NOT NULL, + learning_projects VARCHAR(100), + assessment_items VARCHAR(100), + scoring_criteria VARCHAR(100), + completion_status VARCHAR(100), + score_project DOUBLE, + user_id VARCHAR(64) NOT NULL, + total_score DECIMAL(10, 2) NOT NULL DEFAULT 100.00, + serial_number INT, + schedule DOUBLE, + PRIMARY KEY (id), + CONSTRAINT uk_user_moudule_serial UNIQUE (user_id, moudule, serial_number) +); + CREATE TABLE IF NOT EXISTS institution_sm2_key ( id BIGINT PRIMARY KEY, key_id VARCHAR(64) NOT NULL, @@ -77,6 +93,7 @@ CREATE TABLE IF NOT EXISTS institution_sm2_key_audit ( operation_detail VARCHAR(512) NOT NULL, created_at TIMESTAMP NOT NULL, created_by VARCHAR(64) NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT fk_institution_key_audit_key FOREIGN KEY (key_record_id) REFERENCES institution_sm2_key(id) );