diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java index eaf82b8..0aea2e1 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java @@ -19,7 +19,7 @@ import java.util.UUID; */ @Service public class SalaryScoreService { - public static final String MODULE_NAME = "企业发放数字人民币工资"; + public static final String MODULE_NAME = "数字人民币创新应用场景仿真"; private static final int MODULE_SERIAL_NUMBER = 6; private static final double TOTAL_SCORE = 100.0; diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/ControlSystemSignatureService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/ControlSystemSignatureService.java index 9dd1d9e..5ff7ad7 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/ControlSystemSignatureService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/ControlSystemSignatureService.java @@ -43,6 +43,10 @@ public class ControlSystemSignatureService { "当前实训已有步骤七控制系统签名记录,如需重新开始请先调用重新实训接口"); } String digest = cryptography.sm3(normalizedOriginalText); + if (current.getDigest() != null && current.getDigest().equalsIgnoreCase(digest) + && current.getOriginalText() != null && current.getOriginalText().equals(normalizedOriginalText)) { + return new ControlSystemDigestResult(digest); + } update(current, current.getStatus(), "DIGESTED", operator, new LambdaUpdateWrapper() .set(ControlSystemSignatureEntity::getOriginalText, normalizedOriginalText) @@ -90,6 +94,9 @@ public class ControlSystemSignatureService { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "中央银行第一私钥不正确,本次实训第" + errorSequence + "次错误"); } + if (value.getSignature() != null && value.getSignature().equals(signature)) { + return new ControlSystemSignatureResult(value); + } update(value, value.getStatus(), "SIGNED", operator, new LambdaUpdateWrapper() .set(ControlSystemSignatureEntity::getSigningKeyId, InstitutionKeyService.CENTRAL_FIRST_KEY) diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/QuotaControlBitService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/QuotaControlBitService.java index 21623e6..b757c7b 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/QuotaControlBitService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/QuotaControlBitService.java @@ -43,6 +43,9 @@ public class QuotaControlBitService { String originalText = source.getAmount().setScale(2).toPlainString() + "|" + source.getInstitutionIdentifier() + "|" + source.getTransactionIdentifier(); String segment = "STATIC_" + cryptography.sm3(originalText); + if (current.getStaticSegment() != null && current.getStaticSegment().equals(segment)) { + return new QuotaControlStaticSegmentResult(segment); + } update(current, current.getStatus(), "STATIC_READY", operator, new LambdaUpdateWrapper() .set(QuotaControlBitEntity::getAmount, source.getAmount().setScale(2)) @@ -86,6 +89,9 @@ public class QuotaControlBitService { if (value.getStaticSegment() == null) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成静态段"); } + if (value.getDynamicSegment() != null) { + return new QuotaControlDynamicSegmentResult(value.getDynamicSegment()); + } String randomValue = randomValue(); String timestamp = LocalDateTime.now().format(TIMESTAMP); String segment = "DYN_" + randomValue + "_" + timestamp; @@ -112,6 +118,9 @@ public class QuotaControlBitService { if (!source.getId().equals(value.getControlSignatureId()) || source.getControlSignature() == null) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤七央行控制系统签名数据不完整"); } + if (value.getCentralBankSignatureSegment() != null) { + return new CentralBankSignatureSegmentResult(value.getCentralBankSignatureSegment()); + } update(value, value.getStatus(), "CB_SIGNATURE_READY", operator, new LambdaUpdateWrapper() .set(QuotaControlBitEntity::getCentralBankSignatureSegment, @@ -134,6 +143,9 @@ public class QuotaControlBitService { requireResult(value.getCentralBankSignatureSegment(), "请先读取央行签名段"); String complete = value.getStaticSegment() + "|" + value.getDynamicSegment() + "|" + value.getCentralBankSignatureSegment(); + if (value.getCompleteControlBit() != null && value.getCompleteControlBit().equals(complete)) { + return new QuotaControlBitResult(value); + } update(value, value.getStatus(), "ASSEMBLED", operator, new LambdaUpdateWrapper() .set(QuotaControlBitEntity::getCompleteControlBit, complete) @@ -166,6 +178,9 @@ public class QuotaControlBitService { if (signature == null) { throw keyError(subject, "签名必须使用当前实训主体的商业银行第二私钥"); } + if (value.getBankSignature() != null && value.getBankSignature().equals(signature)) { + return new QuotaControlBitResult(value); + } update(value, value.getStatus(), "SIGNED", operator, new LambdaUpdateWrapper() .set(QuotaControlBitEntity::getCompleteControlBit, submittedControlBit) diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyService.java index f877665..0888efc 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyService.java @@ -52,6 +52,9 @@ public class StandardCurrencyService { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "当前实训已有步骤九记录,如需重新开始请先调用重新实训接口"); } + if ("DECOMPOSED".equals(current.getStatus()) && current.getDenominationSummary() != null) { + return result(current); + } currencyMapper.update(null, new LambdaUpdateWrapper() .eq(StandardCurrencyEntity::getBatchId, current.getId()) .eq(StandardCurrencyEntity::getDeleted, false) diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationRequest.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationRequest.java index f039085..fc442da 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationRequest.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationRequest.java @@ -99,6 +99,10 @@ public class CurrencyGenerationRequest { if (digest == null || !digest.matches("[0-9A-F]{64}")) { throw new IllegalArgumentException("SM3摘要格式不正确"); } + if (this.requestDigest != null && this.requestDigest.equalsIgnoreCase(digest) + && this.requestOriginalText != null && this.requestOriginalText.equals(replacement.requestOriginalText)) { + return; + } this.identifierApplicationId = replacement.identifierApplicationId; this.institutionIdentifier = replacement.institutionIdentifier; this.fullInstitutionIdentifier = replacement.fullInstitutionIdentifier; @@ -129,6 +133,10 @@ public class CurrencyGenerationRequest { if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) { throw new IllegalArgumentException("SM2签名信息不正确"); } + if (this.bankSignature != null && this.bankSignature.equals(signature) + && keyId.equals(this.signingKeyId)) { + return; + } signingKeyId = keyId; bankSignature = signature; this.submittedPrivateKey = submittedPrivateKey; diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerification.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerification.java index 2e37816..08520d8 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerification.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerification.java @@ -99,6 +99,9 @@ public class CurrencyGenerationVerification { } public void receiveAgain() { + if (status != null && status.ordinal() >= Status.RECEIVED.ordinal()) { + return; + } signatureValid = null; recomputedDigest = null; digestMatches = null; @@ -128,6 +131,9 @@ public class CurrencyGenerationVerification { if (digest == null || !digest.matches("[0-9A-F]{64}")) { throw new IllegalArgumentException("重新计算的摘要一格式不正确"); } + if (this.recomputedDigest != null && this.recomputedDigest.equalsIgnoreCase(digest)) { + return; + } recomputedDigest = digest; this.userInputOriginalText = userInputOriginalText; digestMatches = null; 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 02f3b48..5748543 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentifierApplication.java @@ -68,12 +68,17 @@ public class InstitutionIdentifierApplication { public void replaceDigestInput(String bankCode, String timestamp, String originalText, String digest) { validateBase(bankCode, timestamp); - this.bankCode = bankCode; - this.timestamp = timestamp; - this.originalText = requireOriginalText(bankCode, timestamp, originalText); + String validatedOriginalText = requireOriginalText(bankCode, timestamp, originalText); if (digest == null || !digest.matches("[0-9A-F]{64}")) { throw new IllegalArgumentException("SM3摘要格式不正确"); } + if (this.digest != null && this.digest.equalsIgnoreCase(digest) + && this.originalText != null && this.originalText.equals(validatedOriginalText)) { + return; + } + this.bankCode = bankCode; + this.timestamp = timestamp; + this.originalText = validatedOriginalText; this.digest = digest; this.bankSignature = null; this.verificationKeyId = null; @@ -101,6 +106,9 @@ public class InstitutionIdentifierApplication { if (bankSignature == null || !bankSignature.matches("[0-9A-F]+")) { throw new IllegalArgumentException("银行SM2签名格式不正确"); } + if (this.bankSignature != null && this.bankSignature.equals(bankSignature)) { + return; + } this.bankSignature = bankSignature; this.verificationKeyId = null; this.signatureValid = null; diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifier.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifier.java index 1b25948..ae028cc 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifier.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/TransactionInformationIdentifier.java @@ -135,6 +135,10 @@ public class TransactionInformationIdentifier { if (inputAmount.compareTo(amount) != 0 || !institutionIdentifier.equals(parts[1])) { throw new IllegalArgumentException("拼接请求原文中的金额或机构标识与前序数据不一致"); } + if (this.originalText != null && this.originalText.equals(originalText) + && this.digest != null && this.digest.equalsIgnoreCase(digest)) { + return; + } this.originalText = originalText; this.transactionTimestamp = parts[2]; this.digest = digest; @@ -198,6 +202,9 @@ public class TransactionInformationIdentifier { if (quotaDigest == null || !quotaDigest.matches("[0-9A-F]{64}")) { throw new IllegalArgumentException("额度请求SM3摘要格式不正确"); } + if (this.quotaDigest != null && this.quotaDigest.equalsIgnoreCase(quotaDigest)) { + return; + } this.quotaDigest = quotaDigest; clearQuotaSignatureAndFollowingResults(); this.status = Status.QUOTA_DIGESTED; @@ -210,6 +217,10 @@ public class TransactionInformationIdentifier { if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) { throw new IllegalArgumentException("额度请求SM2签名信息不正确"); } + if (this.quotaSignature != null && this.quotaSignature.equals(signature) + && keyId.equals(this.quotaSigningKeyId)) { + return; + } this.quotaSigningKeyId = keyId; this.quotaSignature = signature; clearQuotaPackageAndVerificationResults(); @@ -263,6 +274,9 @@ public class TransactionInformationIdentifier { if (recomputedDigest == null || !recomputedDigest.matches("[0-9A-F]{64}")) { throw new IllegalArgumentException("H1格式不正确"); } + if (this.quotaRecomputedDigest != null && this.quotaRecomputedDigest.equalsIgnoreCase(recomputedDigest)) { + return; + } this.quotaRecomputedDigest = recomputedDigest; this.quotaVerificationKeyId = null; this.quotaSignatureDigest = null; @@ -279,6 +293,9 @@ public class TransactionInformationIdentifier { || !signatureDigest.matches("[0-9A-F]{64}")) { throw new IllegalArgumentException("第二公钥验签获取摘要失败"); } + if (this.quotaSignatureDigest != null && this.quotaSignatureDigest.equalsIgnoreCase(signatureDigest)) { + return; + } this.quotaVerificationKeyId = verificationKeyId; this.quotaSignatureDigest = signatureDigest; this.quotaSignatureValid = true; diff --git a/src/main/java/com/yau/digitalrmb/progress/application/ExperimentRecordResult.java b/src/main/java/com/yau/digitalrmb/progress/application/ExperimentRecordResult.java new file mode 100644 index 0000000..f58b823 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/application/ExperimentRecordResult.java @@ -0,0 +1,36 @@ +package com.yau.digitalrmb.progress.application; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@Schema(description = "实验记录") +public class ExperimentRecordResult { + @Schema(description = "记录ID", example = "1") + private Long id; + + @Schema(description = "用户名", example = "张三") + private String userName; + + @Schema(description = "实验序号", example = "1") + private Integer experimentNo; + + @Schema(description = "模块编码", example = "1") + private String moduleCode; + + @Schema(description = "模块名称", example = "中国人民银行生成数字货币") + private String moduleName; + + @Schema(description = "实验状态:IN_PROGRESS-进行中, COMPLETED-已完成", example = "COMPLETED") + private String status; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "完成时间") + private LocalDateTime completeTime; +} diff --git a/src/main/java/com/yau/digitalrmb/progress/application/ExperimentService.java b/src/main/java/com/yau/digitalrmb/progress/application/ExperimentService.java new file mode 100644 index 0000000..de0b550 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/application/ExperimentService.java @@ -0,0 +1,179 @@ +package com.yau.digitalrmb.progress.application; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yau.digitalrmb.progress.infrastructure.ExperimentRecordEntity; +import com.yau.digitalrmb.progress.infrastructure.ExperimentRecordMapper; +import com.yau.digitalrmb.progress.infrastructure.ExperimentStatisticsEntity; +import com.yau.digitalrmb.progress.infrastructure.ExperimentStatisticsMapper; +import com.yau.digitalrmb.security.context.JwtUser; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ExperimentService { + + @Resource + private ExperimentRecordMapper recordMapper; + + @Resource + private ExperimentStatisticsMapper statisticsMapper; + + @Transactional + public void startExperiment(JwtUser user, String moduleCode) { + String moduleName = resolveModuleName(moduleCode); + + ExperimentRecordEntity lastRecord = findLatestRecord(user.getUserId(), moduleCode); + + if (lastRecord != null && "COMPLETED".equals(lastRecord.getStatus())) { + createRecord(user, moduleCode, moduleName, lastRecord.getExperimentNo() + 1); + incrementTotal(); + incrementInProgress(); + } else if (lastRecord != null && "IN_PROGRESS".equals(lastRecord.getStatus())) { + createRecord(user, moduleCode, moduleName, lastRecord.getExperimentNo() + 1); + incrementTotal(); + } else { + createRecord(user, moduleCode, moduleName, 1); + incrementTotal(); + incrementInProgress(); + } + } + + @Transactional + public void completeExperiment(String userId, String moduleCode) { + ExperimentRecordEntity record = findLatestRecord(userId, moduleCode); + if (record == null || !"IN_PROGRESS".equals(record.getStatus())) { + return; + } + + record.setStatus("COMPLETED"); + record.setCompleteTime(LocalDateTime.now()); + recordMapper.updateById(record); + + incrementCompleted(); + decrementInProgress(); + } + + @Transactional(readOnly = true) + public ExperimentStatisticsResult getStatistics() { + ExperimentStatisticsEntity stats = statisticsMapper.selectById(1L); + if (stats == null) { + return new ExperimentStatisticsResult(0, 0, 0, 0, 0.0); + } + double rate = stats.getTotalExperiments() > 0 + ? Math.round(stats.getCompletedExperiments() * 10000.0 / stats.getTotalExperiments()) / 100.0 + : 0.0; + return new ExperimentStatisticsResult( + stats.getTotalExperiments(), + stats.getCompletedExperiments(), + stats.getInProgressExperiments(), + stats.getNotStartedExperiments(), + rate + ); + } + + @Transactional(readOnly = true) + public ExperimentRecordPageResult getRecentRecords(String userId, int current, int size) { + Page page = new Page<>(current, size); + IPage result = recordMapper.selectPage(page, + new LambdaQueryWrapper() + .eq(ExperimentRecordEntity::getUserId, userId) + .orderByDesc(ExperimentRecordEntity::getStartTime)); + List records = result.getRecords().stream() + .map(r -> new ExperimentRecordResult( + r.getId(), r.getUserName(), r.getExperimentNo(), + r.getModuleCode(), r.getModuleName(), r.getStatus(), + r.getStartTime(), r.getCompleteTime())) + .collect(Collectors.toList()); + return new ExperimentRecordPageResult( + result.getCurrent(), + result.getSize(), + result.getTotal(), + result.getPages(), + records + ); + } + + private ExperimentRecordEntity findLatestRecord(String userId, String moduleCode) { + return recordMapper.selectOne( + new LambdaQueryWrapper() + .eq(ExperimentRecordEntity::getUserId, userId) + .eq(ExperimentRecordEntity::getModuleCode, moduleCode) + .orderByDesc(ExperimentRecordEntity::getExperimentNo) + .last("LIMIT 1")); + } + + private void createRecord(JwtUser user, String moduleCode, String moduleName, int experimentNo) { + ExperimentRecordEntity record = new ExperimentRecordEntity(); + record.setUserId(user.getUserId()); + record.setUserName(user.getUserName()); + record.setSchoolId(user.getSchoolId()); + record.setSchoolName(user.getSchoolName()); + record.setClassId(user.getClassId()); + record.setClassName(user.getClassName()); + record.setExperimentNo(experimentNo); + record.setModuleCode(moduleCode); + record.setModuleName(moduleName); + record.setStatus("IN_PROGRESS"); + record.setStartTime(LocalDateTime.now()); + record.setCreatedAt(LocalDateTime.now()); + record.setUpdatedAt(LocalDateTime.now()); + recordMapper.insert(record); + } + + private ExperimentStatisticsEntity getOrCreateStats() { + ExperimentStatisticsEntity stats = statisticsMapper.selectById(1L); + if (stats == null) { + stats = new ExperimentStatisticsEntity(); + stats.setId(1L); + stats.setTotalExperiments(0); + stats.setCompletedExperiments(0); + stats.setInProgressExperiments(0); + stats.setNotStartedExperiments(0); + statisticsMapper.insert(stats); + } + return stats; + } + + private void incrementTotal() { + ExperimentStatisticsEntity stats = getOrCreateStats(); + stats.setTotalExperiments(stats.getTotalExperiments() + 1); + statisticsMapper.updateById(stats); + } + + private void incrementInProgress() { + ExperimentStatisticsEntity stats = getOrCreateStats(); + stats.setInProgressExperiments(stats.getInProgressExperiments() + 1); + statisticsMapper.updateById(stats); + } + + private void incrementCompleted() { + ExperimentStatisticsEntity stats = getOrCreateStats(); + stats.setCompletedExperiments(stats.getCompletedExperiments() + 1); + statisticsMapper.updateById(stats); + } + + private void decrementInProgress() { + ExperimentStatisticsEntity stats = getOrCreateStats(); + stats.setInProgressExperiments(Math.max(0, stats.getInProgressExperiments() - 1)); + statisticsMapper.updateById(stats); + } + + private String resolveModuleName(String moduleCode) { + switch (moduleCode) { + case "1": return "中国人民银行生成数字货币"; + case "2": return "中国人民银行发行数字货币"; + case "3": return "开通数字货币钱包"; + case "4": return "用户兑换数字货币"; + case "5": return "用户支付数字货币"; + case "6": return "数字人民币创新应用场景仿真"; + default: return "未知模块"; + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/progress/application/ExperimentStatisticsResult.java b/src/main/java/com/yau/digitalrmb/progress/application/ExperimentStatisticsResult.java new file mode 100644 index 0000000..481c157 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/application/ExperimentStatisticsResult.java @@ -0,0 +1,25 @@ +package com.yau.digitalrmb.progress.application; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@Schema(description = "实验统计结果") +public class ExperimentStatisticsResult { + @Schema(description = "实验总次数", example = "128") + private Integer totalExperiments; + + @Schema(description = "完成实验次数", example = "96") + private Integer completedExperiments; + + @Schema(description = "进行中实验次数", example = "12") + private Integer inProgressExperiments; + + @Schema(description = "未开始实验次数", example = "20") + private Integer notStartedExperiments; + + @Schema(description = "完成率(百分比)", example = "75.00") + private Double completionRate; +} diff --git a/src/main/java/com/yau/digitalrmb/progress/application/ModuleCompletionResult.java b/src/main/java/com/yau/digitalrmb/progress/application/ModuleCompletionResult.java new file mode 100644 index 0000000..9682e9f --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/application/ModuleCompletionResult.java @@ -0,0 +1,36 @@ +package com.yau.digitalrmb.progress.application; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +@Schema(description = "模块完成情况") +public class ModuleCompletionResult { + @Schema(description = "模块编码(1-6)", example = "1") + private String moduleCode; + + @Schema(description = "模块名称", example = "中国人民银行生成数字货币") + private String moduleName; + + @Schema(description = "总步骤数", example = "9") + private Integer totalSteps; + + @Schema(description = "已完成步骤数", example = "3") + private Integer completedSteps; + + @Schema(description = "完成进度百分比", example = "33.33") + private Double schedule; + + @Schema(description = "错误次数", example = "2") + private Integer errorCount; + + @Schema(description = "得分", example = "48.00") + private Double score; + + @Schema(description = "各步骤完成情况") + private List steps; +} diff --git a/src/main/java/com/yau/digitalrmb/progress/application/ModuleCompletionService.java b/src/main/java/com/yau/digitalrmb/progress/application/ModuleCompletionService.java new file mode 100644 index 0000000..02ad9e1 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/application/ModuleCompletionService.java @@ -0,0 +1,335 @@ +package com.yau.digitalrmb.progress.application; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.jdbc.core.JdbcTemplate; +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.ArrayList; +import java.util.List; + +@Service +public class ModuleCompletionService { + + @Resource + private StuModuleScoreDetailsMapper scoreDetailsMapper; + + @Resource + private JdbcTemplate jdbcTemplate; + + @Transactional(readOnly = true) + public ModuleCompletionResult getModuleCompletion(String userId, String moduleCode) { + ModuleConfig config = resolveModuleConfig(moduleCode); + + if (config.usesScoreTable) { + return getFromScoreTable(userId, moduleCode, config); + } + return getFromTrainingTaskTable(userId, moduleCode, config); + } + + @Transactional(readOnly = true) + public ExperimentProgressResult getExperimentProgress(String userId) { + List modules = new ArrayList<>(); + int completedCount = 0; + int inProgressCount = 0; + + for (String moduleCode : new String[]{"1", "2", "3", "4", "5", "6"}) { + ModuleConfig config = resolveModuleConfig(moduleCode); + double schedule; + if (config.usesScoreTable) { + StuModuleScoreDetailsEntity detail = scoreDetailsMapper.selectOne( + new LambdaQueryWrapper() + .eq(StuModuleScoreDetailsEntity::getUserId, userId) + .eq(StuModuleScoreDetailsEntity::getMoudule, config.moduleName) + .eq(StuModuleScoreDetailsEntity::getSerialNumber, config.serialNumber) + .last("LIMIT 1")); + schedule = detail != null && detail.getSchedule() != null ? detail.getSchedule() : 0.0; + } else { + List completedStepCodes = jdbcTemplate.queryForList( + "SELECT step_code FROM training_task_step_progress WHERE user_id = ? AND module_code = ?", + String.class, userId, config.trainingModuleCode); + int completedSteps = completedStepCodes.size(); + schedule = config.totalSteps > 0 + ? BigDecimal.valueOf(completedSteps) + .multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(config.totalSteps), 2, RoundingMode.HALF_UP) + .doubleValue() + : 0.0; + } + + schedule = BigDecimal.valueOf(schedule).setScale(2, RoundingMode.HALF_UP).doubleValue(); + String status; + if (schedule >= 100.0) { + status = "COMPLETED"; + completedCount++; + } else if (schedule > 0.0) { + status = "IN_PROGRESS"; + inProgressCount++; + } else { + status = "NOT_STARTED"; + } + modules.add(new ExperimentProgressResult.ModuleProgressItem( + moduleCode, config.moduleName, status, schedule)); + } + + int totalModules = 6; + int notStartedCount = totalModules - completedCount - inProgressCount; + + double completedRate = BigDecimal.valueOf(completedCount) + .multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(totalModules), 2, RoundingMode.HALF_UP).doubleValue(); + double inProgressRate = BigDecimal.valueOf(inProgressCount) + .multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(totalModules), 2, RoundingMode.HALF_UP).doubleValue(); + double notStartedRate = BigDecimal.valueOf(notStartedCount) + .multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(totalModules), 2, RoundingMode.HALF_UP).doubleValue(); + double totalProgress = BigDecimal.valueOf(completedRate + inProgressRate * 0.5) + .setScale(2, RoundingMode.HALF_UP).doubleValue(); + + return new ExperimentProgressResult( + totalProgress, completedRate, inProgressRate, notStartedRate, modules); + } + + private ModuleCompletionResult getFromScoreTable(String userId, String moduleCode, ModuleConfig config) { + StuModuleScoreDetailsEntity detail = scoreDetailsMapper.selectOne( + new LambdaQueryWrapper() + .eq(StuModuleScoreDetailsEntity::getUserId, userId) + .eq(StuModuleScoreDetailsEntity::getMoudule, config.moduleName) + .eq(StuModuleScoreDetailsEntity::getSerialNumber, config.serialNumber) + .last("LIMIT 1")); + + double schedule = 0.0; + int errorCount = 0; + double score = 0.0; + + if (detail != null) { + schedule = detail.getSchedule() != null ? detail.getSchedule() : 0.0; + errorCount = parseErrorCount(detail.getCompletionStatus()); + score = detail.getScoreProject() != null ? detail.getScoreProject() : 0.0; + } + + int completedSteps = calculateCompletedSteps(schedule, config.totalSteps); + List steps = buildSteps(config, completedSteps); + + return new ModuleCompletionResult( + moduleCode, + config.moduleName, + config.totalSteps, + completedSteps, + BigDecimal.valueOf(schedule).setScale(2, RoundingMode.HALF_UP).doubleValue(), + errorCount, + BigDecimal.valueOf(score).setScale(2, RoundingMode.HALF_UP).doubleValue(), + steps + ); + } + + private ModuleCompletionResult getFromTrainingTaskTable(String userId, String moduleCode, ModuleConfig config) { + List completedStepCodes = jdbcTemplate.queryForList( + "SELECT step_code FROM training_task_step_progress WHERE user_id = ? AND module_code = ?", + String.class, userId, config.trainingModuleCode); + + int completedSteps = completedStepCodes.size(); + double schedule = config.totalSteps > 0 + ? BigDecimal.valueOf(completedSteps) + .multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(config.totalSteps), 2, RoundingMode.HALF_UP) + .doubleValue() + : 0.0; + + List steps = buildSteps(config, completedSteps); + + return new ModuleCompletionResult( + moduleCode, + config.moduleName, + config.totalSteps, + completedSteps, + schedule, + 0, + 0.0, + steps + ); + } + + private ModuleConfig resolveModuleConfig(String moduleCode) { + switch (moduleCode) { + case "1": + return new ModuleConfig("中国人民银行生成数字货币", 3, 9, true, null, + new String[]{ + "生成投放机构标识", + "生成发起货币生成请求", + "验证货币生成请求", + "生成交易信息标识", + "生成额度控制位请求", + "验证发送额度控制位请求", + "生成额度控制系统签名信息", + "生成额度控制位和数字签名", + "生成数字人民币" + }, + new String[]{ + "商业银行使用数字证书生成投放机构标识", + "商业银行发起货币生成请求", + "中央银行验证货币生成请求", + "根据已认证请求生成交易信息标识", + "商业银行发起额度控制位请求", + "中央银行验证并发送额度控制位请求", + "额度控制系统生成签名信息", + "生成最终额度控制位和数字签名", + "完成全流程后生成数字人民币" + }); + case "2": + return new ModuleConfig("中国人民银行发行数字货币", 0, 7, false, "PBOC_CURRENCY_ISSUANCE", + new String[]{ + "查看货币需求", + "生成发行申请计划", + "发行计划数字签名", + "发送标准报文", + "中央银行验证与业务核查", + "准备金通知与ACS扣减", + "发行并确权数字货币" + }, + new String[]{ + "查看货币需求情况", + "生成发行申请计划", + "对发行计划进行数字签名", + "发送标准报文", + "中央银行验证与业务核查", + "准备金通知与ACS扣减", + "发行并确权数字货币" + }); + case "3": + return new ModuleConfig("开通数字货币钱包", 4, 5, true, null, + new String[]{ + "商业银行钱包开立", + "钱包标识生成", + "钱包注册", + "智能合约签订", + "央行钱包激活" + }, + new String[]{ + "商业银行发起钱包开立申请", + "生成钱包标识和钱包ID", + "完成钱包注册并生成证书", + "签订智能合约并激活", + "央行最终确认并激活钱包" + }); + case "4": + return new ModuleConfig("用户兑换数字货币", 0, 5, false, "CURRENCY_EXCHANGE", + new String[]{ + "确认银行卡、钱包和商业银行库存", + "生成取币请求报文", + "钱包私钥签名", + "商业银行验签并扣款", + "央行确认币串权属" + }, + new String[]{ + "确认银行卡、钱包和商业银行库存", + "生成取币请求报文", + "钱包私钥签名", + "商业银行验签并扣款", + "央行确认币串权属" + }); + case "5": + return new ModuleConfig("用户支付数字货币", 0, 6, false, "CURRENCY_PAYMENT", + new String[]{ + "初始化付款方和收款方钱包", + "签名支付请求", + "付款行预处理", + "中央银行结算", + "收款行入账", + "生成支付回执与到账通知" + }, + new String[]{ + "初始化付款方和收款方钱包", + "签名支付请求", + "付款行预处理", + "中央银行结算", + "收款行入账", + "生成支付回执与到账通知" + }); + case "6": + return new ModuleConfig("数字人民币创新应用场景仿真", 6, 8, true, null, + new String[]{ + "工资发放批次创建", + "工资发放明细录入", + "工资发放审核", + "工资发放复核", + "工资发放确认", + "捐赠企业发起捐赠", + "受赠机构接收捐款与公示", + "第三方监管监督与审核" + }, + new String[]{ + "创建工资发放批次", + "录入工资发放明细", + "审核工资发放", + "复核工资发放", + "确认工资发放", + "捐赠企业发起捐赠流程", + "受赠机构接收捐款并公示", + "第三方监管机构监督与审核" + }); + default: + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "未知的模块编码:" + moduleCode + ",请传入1-6的数字"); + } + } + + private int calculateCompletedSteps(double schedule, int totalSteps) { + if (schedule <= 0 || totalSteps <= 0) { + return 0; + } + double completed = schedule / 100.0 * totalSteps; + return Math.min(totalSteps, (int) Math.round(completed)); + } + + private List buildSteps(ModuleConfig config, int completedSteps) { + List result = new ArrayList<>(config.stepNames.length); + for (int i = 0; i < config.stepNames.length; i++) { + String stepCode = String.format("%02d", i + 1); + int status = i < completedSteps ? 1 : 0; + String desc = i < config.stepDescriptions.length ? config.stepDescriptions[i] : ""; + result.add(new ModuleStepStatus(stepCode, config.stepNames[i], desc, status)); + } + return result; + } + + private int parseErrorCount(String completionStatus) { + if (completionStatus == null || completionStatus.trim().isEmpty()) { + return 0; + } + try { + return Math.max(0, Integer.parseInt(completionStatus.trim())); + } catch (NumberFormatException e) { + return 0; + } + } + + private static class ModuleConfig { + final String moduleName; + final int serialNumber; + final int totalSteps; + final boolean usesScoreTable; + final String trainingModuleCode; + final String[] stepNames; + final String[] stepDescriptions; + + ModuleConfig(String moduleName, int serialNumber, int totalSteps, + boolean usesScoreTable, String trainingModuleCode, + String[] stepNames, String[] stepDescriptions) { + this.moduleName = moduleName; + this.serialNumber = serialNumber; + this.totalSteps = totalSteps; + this.usesScoreTable = usesScoreTable; + this.trainingModuleCode = trainingModuleCode; + this.stepNames = stepNames; + this.stepDescriptions = stepDescriptions; + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/progress/application/ModuleStepStatus.java b/src/main/java/com/yau/digitalrmb/progress/application/ModuleStepStatus.java new file mode 100644 index 0000000..97da2ed --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/application/ModuleStepStatus.java @@ -0,0 +1,22 @@ +package com.yau.digitalrmb.progress.application; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@Schema(description = "模块步骤完成情况") +public class ModuleStepStatus { + @Schema(description = "步骤编号", example = "01") + private String stepCode; + + @Schema(description = "步骤名称", example = "生成投放机构标识") + private String stepName; + + @Schema(description = "步骤描述", example = "商业银行使用数字证书生成投放机构标识") + private String stepDescription; + + @Schema(description = "完成状态:0=未完成,1=已完成", example = "1") + private Integer status; +} diff --git a/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentRecordEntity.java b/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentRecordEntity.java new file mode 100644 index 0000000..f525b6f --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentRecordEntity.java @@ -0,0 +1,31 @@ +package com.yau.digitalrmb.progress.infrastructure; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("experiment_record") +public class ExperimentRecordEntity { + + @TableId(type = IdType.AUTO) + private Long id; + + private String userId; + private String userName; + private String schoolId; + private String schoolName; + private String classId; + private String className; + private Integer experimentNo; + private String moduleCode; + private String moduleName; + private String status; + private LocalDateTime startTime; + private LocalDateTime completeTime; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentRecordMapper.java b/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentRecordMapper.java new file mode 100644 index 0000000..e349b06 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentRecordMapper.java @@ -0,0 +1,8 @@ +package com.yau.digitalrmb.progress.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ExperimentRecordMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentStatisticsEntity.java b/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentStatisticsEntity.java new file mode 100644 index 0000000..7609720 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentStatisticsEntity.java @@ -0,0 +1,22 @@ +package com.yau.digitalrmb.progress.infrastructure; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("experiment_statistics") +public class ExperimentStatisticsEntity { + + @TableId(type = IdType.AUTO) + private Long id; + + private Integer totalExperiments; + private Integer completedExperiments; + private Integer inProgressExperiments; + private Integer notStartedExperiments; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentStatisticsMapper.java b/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentStatisticsMapper.java new file mode 100644 index 0000000..f228617 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/infrastructure/ExperimentStatisticsMapper.java @@ -0,0 +1,8 @@ +package com.yau.digitalrmb.progress.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ExperimentStatisticsMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/progress/interfaces/rest/ExperimentController.java b/src/main/java/com/yau/digitalrmb/progress/interfaces/rest/ExperimentController.java new file mode 100644 index 0000000..7330202 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/interfaces/rest/ExperimentController.java @@ -0,0 +1,68 @@ +package com.yau.digitalrmb.progress.interfaces.rest; + +import com.yau.digitalrmb.progress.application.ExperimentRecordPageResult; +import com.yau.digitalrmb.progress.application.ExperimentService; +import com.yau.digitalrmb.progress.application.ExperimentStatisticsResult; +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.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/api/v1/experiments") +@Tag(name = "公共模块 - 实验管理", description = "实验统计、实验记录管理。") +public class ExperimentController { + + @Resource + private ExperimentService service; + + @PostMapping("/{moduleCode}/start") + @Operation(summary = "开始实验(实验总次数+1)", + description = "前端每次开始实验时调用,实验总次数+1。如果是重新实训:上次已完成则进行中+1,上次未完成则进行中不变。") + public ApiResponse startExperiment(@PathVariable String moduleCode) { + JwtUser user = AuthContextHolder.get(); + service.startExperiment(user, moduleCode.trim()); + return ApiResponse.success(null, traceId()); + } + + @PostMapping("/{moduleCode}/complete") + @Operation(summary = "完成实验(完成+1,进行中-1)", + description = "模块最后一个接口执行完成后调用。完成实验+1,进行中-1,实验记录状态改为已完成。") + public ApiResponse completeExperiment(@PathVariable String moduleCode) { + JwtUser user = AuthContextHolder.get(); + service.completeExperiment(user.getUserId(), moduleCode.trim()); + return ApiResponse.success(null, traceId()); + } + + @GetMapping("/statistics") + @Operation(summary = "获取实验统计数据", + description = "返回系统首页所需的实验总次数、完成实验、进行中实验、未开始实验、完成率。") + public ApiResponse getStatistics() { + return ApiResponse.success(service.getStatistics(), traceId()); + } + + @GetMapping("/recent-records") + @Operation(summary = "获取实验记录(分页)", + description = "分页返回当前用户的实验记录列表。参数:current-当前页码,size-每页数量。") + public ApiResponse getRecentRecords( + @RequestParam(value = "current", defaultValue = "1") int current, + @RequestParam(value = "size", defaultValue = "10") int size) { + JwtUser user = AuthContextHolder.get(); + return ApiResponse.success(service.getRecentRecords(user.getUserId(), current, size), traceId()); + } + + private String traceId() { + return MDC.get(TraceIdFilter.MDC_KEY); + } +} diff --git a/src/main/java/com/yau/digitalrmb/progress/interfaces/rest/ModuleCompletionController.java b/src/main/java/com/yau/digitalrmb/progress/interfaces/rest/ModuleCompletionController.java new file mode 100644 index 0000000..d3dd222 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/progress/interfaces/rest/ModuleCompletionController.java @@ -0,0 +1,47 @@ +package com.yau.digitalrmb.progress.interfaces.rest; + +import com.yau.digitalrmb.progress.application.ExperimentProgressResult; +import com.yau.digitalrmb.progress.application.ModuleCompletionResult; +import com.yau.digitalrmb.progress.application.ModuleCompletionService; +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.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/api/v1/module-completion") +@Tag(name = "公共模块 - 模块完成情况", description = "根据模块编码查询每个步骤的完成状态、进度、错误次数和得分。") +public class ModuleCompletionController { + + @Resource + private ModuleCompletionService service; + + @GetMapping("/{moduleCode}") + @Operation(summary = "查询单个模块完成情况", + description = "前端传入模块编码(1-6),返回该模块下每个步骤的完成状态。") + public ApiResponse getModuleCompletion(@PathVariable String moduleCode) { + JwtUser user = AuthContextHolder.get(); + return ApiResponse.success(service.getModuleCompletion(user.getUserId(), moduleCode.trim()), traceId()); + } + + @GetMapping("/progress") + @Operation(summary = "查询实验进度(6个模块整体完成度)", + description = "统计全部6个模块的完成情况,返回总进度、已完成/进行中/未开始的百分比及每个模块的状态。") + public ApiResponse getExperimentProgress() { + JwtUser user = AuthContextHolder.get(); + return ApiResponse.success(service.getExperimentProgress(user.getUserId()), traceId()); + } + + private String traceId() { + return MDC.get(TraceIdFilter.MDC_KEY); + } +} diff --git a/src/main/java/com/yau/digitalrmb/walletopening/application/CentralWalletActivationService.java b/src/main/java/com/yau/digitalrmb/walletopening/application/CentralWalletActivationService.java index 9668863..14d1df4 100644 --- a/src/main/java/com/yau/digitalrmb/walletopening/application/CentralWalletActivationService.java +++ b/src/main/java/com/yau/digitalrmb/walletopening/application/CentralWalletActivationService.java @@ -156,18 +156,18 @@ public class CentralWalletActivationService { } @Transactional - public CentralWalletActivationPageResult verifyFinalSignature(InstitutionKeySubject subject, String publicKeyInput, String algorithm) { + public String verifyFinalSignature(InstitutionKeySubject subject, String publicKeyInput, String algorithm) { if (!"SM2".equalsIgnoreCase(algorithm != null ? algorithm.trim() : "")) { scoreService.recordError(subject.getUserId()); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); } CentralWalletActivation a = getOrCreate(subject); - if (!a.isWalletActivated()) { + if (!a.isReceived()) { scoreService.recordError(subject.getUserId()); - throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成步骤四钱包激活"); + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成步骤四开通确认报文发送"); } if (a.getStep5SignVerifyResult() != null && a.getStep5SignVerifyResult().startsWith("通过")) { - return new CentralWalletActivationPageResult(a); + return "通过"; } String storedPublicKey = institutionKeyService.commercialBankSecondPublicKeyOrNull(subject); if (storedPublicKey == null) { @@ -189,11 +189,12 @@ public class CentralWalletActivationService { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "签名验证失败"); } activationRepository.update(a); - return new CentralWalletActivationPageResult(a); + return "通过"; } @Transactional - public CentralWalletActivationPageResult finalSign(InstitutionKeySubject subject, String privateKeyInput, String algorithm) { + public String finalSign(InstitutionKeySubject subject, String algorithm, + String digestValue, String privateKeyInput) { if (!"SM2".equalsIgnoreCase(algorithm != null ? algorithm.trim() : "")) { scoreService.recordError(subject.getUserId()); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); @@ -204,7 +205,11 @@ public class CentralWalletActivationService { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM2签名验证"); } if (a.getCbFinalSignature() != null) { - return new CentralWalletActivationPageResult(a); + return a.getCbFinalSignature(); + } + if (!a.getDigest().equalsIgnoreCase(digestValue != null ? digestValue.trim() : "")) { + scoreService.recordError(subject.getUserId()); + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "摘要值与开通确认报文不一致"); } String storedPrivateKey = institutionKeyService.centralBankFirstPrivateKeyOrNull(subject); if (storedPrivateKey == null) { @@ -219,33 +224,35 @@ public class CentralWalletActivationService { String cbFinalSignature = "CB_FINAL_WALLET_" + sig; a.finalSign(cbFinalSignature); activationRepository.update(a); - return new CentralWalletActivationPageResult(a); + return cbFinalSignature; } @Transactional - public CentralWalletActivationPageResult updateWalletRegisterStatus(InstitutionKeySubject subject) { + public String updateWalletRegisterStatus(InstitutionKeySubject subject) { CentralWalletActivation a = getOrCreate(subject); if (a.getCbFinalSignature() == null) { scoreService.recordError(subject.getUserId()); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成央行最终确权签名"); } if (a.getWalletRegisterStatus() != null) { - return new CentralWalletActivationPageResult(a); + return "成功"; } a.updateWalletRegisterStatus(); activationRepository.update(a); - return new CentralWalletActivationPageResult(a); + return "成功"; } @Transactional - public CentralWalletActivationPageResult outputFinalConfirm(InstitutionKeySubject subject) { + public FinalConfirmOutputResult outputFinalConfirm(InstitutionKeySubject subject) { CentralWalletActivation a = getOrCreate(subject); if (a.getWalletRegisterStatus() == null) { scoreService.recordError(subject.getUserId()); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先更新钱包登记状态"); } if (a.getFinalConfirmJson() != null) { - return new CentralWalletActivationPageResult(a); + return new FinalConfirmOutputResult( + a.getFinalConfirmId(), a.getWalletId(), "AVAILABLE", + a.getCbFinalSignature(), a.getFinalTime()); } String date = LocalDateTime.now().format(DATE_FORMAT); String finalConfirmId = "FINAL_CONFIRM_" + date + "_001"; @@ -254,11 +261,13 @@ public class CentralWalletActivationService { a.getCbFinalSignature(), finalTime); a.outputFinalConfirm(finalConfirmId, json, finalTime); activationRepository.update(a); - return new CentralWalletActivationPageResult(a); + return new FinalConfirmOutputResult( + finalConfirmId, a.getWalletId(), "AVAILABLE", + a.getCbFinalSignature(), finalTime); } @Transactional - public CentralWalletActivationPageResult sendFinal(InstitutionKeySubject subject) { + public String sendFinal(InstitutionKeySubject subject) { CentralWalletActivation a = getOrCreate(subject); if (a.getFinalConfirmJson() == null) { scoreService.recordError(subject.getUserId()); @@ -266,7 +275,8 @@ public class CentralWalletActivationService { } a.sendFinal(); activationRepository.update(a); - return new CentralWalletActivationPageResult(a); + scoreService.markStepCompleted(subject.getUserId(), 5); + return "成功"; } @Transactional(readOnly = true) @@ -288,10 +298,23 @@ public class CentralWalletActivationService { } private CentralWalletActivation getOrCreate(InstitutionKeySubject subject) { - return activationRepository + CentralWalletActivation a = activationRepository .findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId()) - .orElseGet(() -> activationRepository.save( - CentralWalletActivation.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId()))); + .orElse(null); + if (a == null) { + a = activationRepository.save( + CentralWalletActivation.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId())); + } + if (!a.isReceived()) { + SmartContractGeneration c = contractRepository + .findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId()).orElse(null); + if (c != null && c.isSent()) { + a.receive(c.getConfirmId(), c.getWalletId(), c.getOrgCode(), c.getContractId(), + c.getActivateDigest(), c.getActivateSignature(), c.getActivateTimestamp(), c.getConfirmJson()); + activationRepository.update(a); + } + } + return a; } private String buildFinalConfirmJson(String finalConfirmId, String walletId, diff --git a/src/main/java/com/yau/digitalrmb/walletopening/application/SmartContractService.java b/src/main/java/com/yau/digitalrmb/walletopening/application/SmartContractService.java index 1e9b01b..4288f26 100644 --- a/src/main/java/com/yau/digitalrmb/walletopening/application/SmartContractService.java +++ b/src/main/java/com/yau/digitalrmb/walletopening/application/SmartContractService.java @@ -57,14 +57,14 @@ public class SmartContractService { } @Transactional - public SmartContractPageResult computeContractDigest(InstitutionKeySubject subject, ContractDigestRequest request) { + public String computeContractDigest(InstitutionKeySubject subject, ContractDigestRequest request) { if (!"SM3".equalsIgnoreCase(request.getAlgorithm() != null ? request.getAlgorithm().trim() : "")) { scoreService.recordError(subject.getUserId()); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); } SmartContractGeneration c = getOrCreate(subject); if (c.getContractDigest() != null) { - return new SmartContractPageResult(c); + return c.getContractDigest(); } verifyStep3RegistrationConfirmed(subject); String storedWalletId = resolveWalletId(subject); @@ -84,14 +84,20 @@ public class SmartContractService { String digest = cryptography.sm3(c.getContractPlaintext()); c.computeContractDigest(digest); contractRepository.update(c); - return new SmartContractPageResult(c); + return digest; } @Transactional - public SmartContractPageResult outputContract(InstitutionKeySubject subject) { + public ContractOutputResult outputContract(InstitutionKeySubject subject) { SmartContractGeneration c = getOrCreate(subject); if (c.getContractJson() != null) { - return new SmartContractPageResult(c); + ContractLimits limits = new ContractLimits( + c.getSinglePaymentLimit(), c.getDailyPaymentLimit(), + c.getAnnualPaymentLimit(), c.getBalanceCeiling()); + return new ContractOutputResult( + c.getContractId(), c.getWalletId(), c.getWalletType(), + limits, c.getValidity(), c.getContractDigest(), + "已生效", c.getContractEffectiveTime()); } if (c.getContractDigest() == null) { scoreService.recordError(subject.getUserId()); @@ -103,14 +109,21 @@ public class SmartContractService { String json = buildContractJson(contractId, c); c.outputContract(contractId, json, effectiveTime); contractRepository.update(c); - return new SmartContractPageResult(c); + ContractLimits limits = new ContractLimits( + c.getSinglePaymentLimit(), c.getDailyPaymentLimit(), + c.getAnnualPaymentLimit(), c.getBalanceCeiling()); + return new ContractOutputResult( + contractId, c.getWalletId(), c.getWalletType(), + limits, c.getValidity(), c.getContractDigest(), + "已生效", effectiveTime); } @Transactional - public SmartContractPageResult acquireActivateTimestamp(InstitutionKeySubject subject) { + public ActivateTimestampResult acquireActivateTimestamp(InstitutionKeySubject subject) { SmartContractGeneration c = getOrCreate(subject); if (c.getStatus() != null && c.getStatus().ordinal() >= SmartContractGeneration.Status.ACTIVATE_TIMESTAMP_ACQUIRED.ordinal()) { - return new SmartContractPageResult(c); + return new ActivateTimestampResult( + c.getActivateTimestamp(), c.getWalletId(), c.getOrgCode(), c.getContractId()); } if (c.getContractId() == null) { scoreService.recordError(subject.getUserId()); @@ -120,18 +133,18 @@ public class SmartContractService { String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); c.acquireActivateTimestamp(timestamp, orgCode); contractRepository.update(c); - return new SmartContractPageResult(c); + return new ActivateTimestampResult(timestamp, c.getWalletId(), orgCode, c.getContractId()); } @Transactional - public SmartContractPageResult computeActivateDigest(InstitutionKeySubject subject, ActivateDigestRequest request) { + public String computeActivateDigest(InstitutionKeySubject subject, ActivateDigestRequest request) { if (!"SM3".equalsIgnoreCase(request.getAlgorithm() != null ? request.getAlgorithm().trim() : "")) { scoreService.recordError(subject.getUserId()); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); } SmartContractGeneration c = getOrCreate(subject); if (c.getActivateDigest() != null) { - return new SmartContractPageResult(c); + return c.getActivateDigest(); } if (c.getContractId() == null) { scoreService.recordError(subject.getUserId()); @@ -145,18 +158,18 @@ public class SmartContractService { String digest = cryptography.sm3(c.getActivatePlaintext()); c.computeActivateDigest(digest); contractRepository.update(c); - return new SmartContractPageResult(c); + return digest; } @Transactional - public SmartContractPageResult signActivate(InstitutionKeySubject subject, String privateKeyInput, String algorithm) { + public String signActivate(InstitutionKeySubject subject, String privateKeyInput, String algorithm) { if (!"SM2".equalsIgnoreCase(algorithm != null ? algorithm.trim() : "")) { scoreService.recordError(subject.getUserId()); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); } SmartContractGeneration c = getOrCreate(subject); if (c.getActivateSignature() != null) { - return new SmartContractPageResult(c); + return c.getActivateSignature(); } if (c.getActivateDigest() == null) { scoreService.recordError(subject.getUserId()); @@ -175,14 +188,16 @@ public class SmartContractService { String signature = cryptography.sign(storedPrivateKey, c.getActivateDigest()); c.signActivate(privateKeyInput.trim(), signature); contractRepository.update(c); - return new SmartContractPageResult(c); + return signature; } @Transactional - public SmartContractPageResult outputConfirm(InstitutionKeySubject subject) { + public ActivateConfirmOutputResult outputConfirm(InstitutionKeySubject subject) { SmartContractGeneration c = getOrCreate(subject); if (c.getConfirmJson() != null) { - return new SmartContractPageResult(c); + return new ActivateConfirmOutputResult( + c.getConfirmId(), c.getWalletId(), c.getOrgCode(), c.getContractId(), + c.getActivateDigest(), c.getActivateSignature(), c.getActivateTimestamp(), "ACTIVATED"); } if (c.getActivateSignature() == null) { scoreService.recordError(subject.getUserId()); @@ -193,14 +208,16 @@ public class SmartContractService { String json = buildConfirmJson(confirmId, c); c.outputConfirm(confirmId, json); contractRepository.update(c); - return new SmartContractPageResult(c); + return new ActivateConfirmOutputResult( + confirmId, c.getWalletId(), c.getOrgCode(), c.getContractId(), + c.getActivateDigest(), c.getActivateSignature(), c.getActivateTimestamp(), "ACTIVATED"); } @Transactional - public SmartContractPageResult send(InstitutionKeySubject subject) { + public String send(InstitutionKeySubject subject) { SmartContractGeneration c = getOrCreate(subject); if (c.isSent()) { - return new SmartContractPageResult(c); + return "成功"; } if (c.getConfirmJson() == null) { scoreService.recordError(subject.getUserId()); @@ -209,7 +226,7 @@ public class SmartContractService { c.markSent(); contractRepository.update(c); scoreService.markStepCompleted(subject.getUserId(), 6); - return new SmartContractPageResult(c); + return "成功"; } private String buildContractJson(String contractId, SmartContractGeneration c) { diff --git a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/CentralBankWalletFinalConfirmController.java b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/CentralBankWalletFinalConfirmController.java index 38dd85f..1d8ca9f 100644 --- a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/CentralBankWalletFinalConfirmController.java +++ b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/CentralBankWalletFinalConfirmController.java @@ -4,14 +4,13 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.web.TraceIdFilter; -import com.yau.digitalrmb.walletopening.application.CentralWalletActivationPageResult; +import com.yau.digitalrmb.walletopening.application.FinalConfirmOutputResult; import com.yau.digitalrmb.walletopening.application.CentralWalletActivationService; import com.yau.digitalrmb.walletopening.interfaces.rest.dto.CentralActivationVerifySignRequest; import com.yau.digitalrmb.walletopening.interfaces.rest.dto.FinalSignRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.slf4j.MDC; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,55 +22,47 @@ import javax.validation.Valid; @RestController @RequestMapping("/api/v1/central-banks/wallet-opening/step5") @Tag(name = "数字货币钱包-央行端 - 步骤五:最终确权并通知用户", - description = "央行SM2验签(商业银行第二公钥)→最终确权签名(央行第一私钥)→钱包登记状态更新(AVAILABLE)→输出最终确权通知→发送给用户") + description = "央行SM2验签 → 最终确权签名 → 更新钱包登记状态 → 输出最终确权通知 → 发送给用户") public class CentralBankWalletFinalConfirmController { @Resource private CentralWalletActivationService activationService; - @GetMapping("/page") - @Operation(summary = "查询央行端步骤五页面数据", description = "返回验签结果、确权签名、登记状态、最终通知等") - public ApiResponse getPage() { - return ApiResponse.success(activationService.getPage( - InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); - } - @PostMapping("/verify-sign") @Operation(summary = "SM2验证签名", - description = "选择商业银行第二公钥进行SM2_Verify(第二公钥, 摘要值, 签名值),公钥错误则错误数+1") - public ApiResponse verifySign( - @Valid @RequestBody CentralActivationVerifySignRequest request) { + description = "选择商业银行第二公钥进行SM2验签,返回通过/未通过,失败则错误数+1并说明原因") + public ApiResponse verifySign(@Valid @RequestBody CentralActivationVerifySignRequest request) { return ApiResponse.success(activationService.verifyFinalSignature( InstitutionKeySubject.from(AuthContextHolder.get()), request.getPublicKey(), request.getAlgorithm()), traceId()); } @PostMapping("/final-sign") @Operation(summary = "央行最终确权签名", - description = "使用央行第一私钥对摘要进行SM2签名,生成cbFinalSignature,私钥错误则错误数+1") - public ApiResponse finalSign( - @Valid @RequestBody FinalSignRequest request) { + description = "传算法、摘要值、央行第一私钥,校验通过后返回确权签名值") + public ApiResponse finalSign(@Valid @RequestBody FinalSignRequest request) { return ApiResponse.success(activationService.finalSign( - InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey(), request.getAlgorithm()), traceId()); + InstitutionKeySubject.from(AuthContextHolder.get()), + request.getAlgorithm(), request.getDigestValue(), request.getPrivateKey()), traceId()); } @PostMapping("/update-status") - @Operation(summary = "更新钱包登记状态", description = "将钱包登记状态更新为AVAILABLE") - public ApiResponse updateStatus() { + @Operation(summary = "更新钱包登记状态", description = "将钱包登记状态更新为AVAILABLE,返回成功/失败") + public ApiResponse updateStatus() { return ApiResponse.success(activationService.updateWalletRegisterStatus( InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); } @PostMapping("/output-confirm") @Operation(summary = "输出最终确权通知", - description = "生成包含finalConfirmId、walletId、status=AVAILABLE、cbFinalSignature的最终通知JSON") - public ApiResponse outputConfirm() { + description = "生成最终确权通知,返回finalConfirmId、walletId、status、cbFinalSignature、finalTime") + public ApiResponse outputConfirm() { return ApiResponse.success(activationService.outputFinalConfirm( InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); } @PostMapping("/send") @Operation(summary = "发送最终确权通知", description = "将最终确权通知发送给用户端") - public ApiResponse send() { + public ApiResponse send() { return ApiResponse.success(activationService.sendFinal( InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); } diff --git a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/CommercialBankSmartContractController.java b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/CommercialBankSmartContractController.java index f2f5f58..270f935 100644 --- a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/CommercialBankSmartContractController.java +++ b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/CommercialBankSmartContractController.java @@ -3,8 +3,10 @@ package com.yau.digitalrmb.walletopening.interfaces.rest; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.shared.api.ApiResponse; - import com.yau.digitalrmb.shared.web.TraceIdFilter; +import com.yau.digitalrmb.walletopening.application.ActivateConfirmOutputResult; +import com.yau.digitalrmb.walletopening.application.ActivateTimestampResult; +import com.yau.digitalrmb.walletopening.application.ContractOutputResult; import com.yau.digitalrmb.walletopening.application.SmartContractPageResult; import com.yau.digitalrmb.walletopening.application.SmartContractService; import com.yau.digitalrmb.walletopening.interfaces.rest.dto.ActivateDigestRequest; @@ -25,7 +27,7 @@ import javax.validation.Valid; @RestController @RequestMapping("/api/v1/commercial-banks/wallet-opening/step4") @Tag(name = "数字货币钱包-商业银行端 - 步骤四:生成合约信息", - description = "商业银行端①生成合约(前端传入钱包标识ID及6个合约参数→校验钱包标识ID→SM3摘要→输出智能合约JSON);②提交开通完成确认(读取时间戳→前端传入确认原文→SM3摘要→SM2签名(第二私钥)→输出确认报文→发送)") + description = "①生成合约摘要→输出智能合约;②读取时间戳→生成确认摘要→SM2签名→输出开通确认报文→发送") public class CommercialBankSmartContractController { @Resource @@ -40,52 +42,56 @@ public class CommercialBankSmartContractController { @PostMapping("/contract-digest") @Operation(summary = "SM3生成合约摘要", - description = "前端传入钱包标识ID、钱包类型、单笔/日累计/年累计支付限额、余额上限、有效期、时间戳。后端校验钱包标识ID是否正确(错误则错题数+1),拼接合约原文后SM3计算摘要") - public ApiResponse computeContractDigest( + description = "前端传入钱包标识ID和合约参数,校验后返回SM3合约摘要值") + public ApiResponse computeContractDigest( @Valid @RequestBody ContractDigestRequest request) { return ApiResponse.success(smartContractService.computeContractDigest( InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId()); } @PostMapping("/output-contract") - @Operation(summary = "输出智能合约信息JSON", description = "输出包含contractId、walletId、walletType、limits、validity、contractHash、status、effectiveTime的合约JSON") - public ApiResponse outputContract() { + @Operation(summary = "输出智能合约信息JSON", + description = "返回contractId、walletId、walletType、limits、validity、contractHash、status、effectiveTime") + public ApiResponse outputContract() { return ApiResponse.success(smartContractService.outputContract( InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); } @PostMapping("/read-activate-timestamp") - @Operation(summary = "读取当前时间戳(确认阶段)", description = "生成开通确认所需的当前时间戳,同时带出机构代码") - public ApiResponse readActivateTimestamp() { + @Operation(summary = "读取当前时间戳(确认阶段)", + description = "返回时间戳、钱包标识ID、机构代码、合约ID") + public ApiResponse readActivateTimestamp() { return ApiResponse.success(smartContractService.acquireActivateTimestamp( InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); } @PostMapping("/activate-digest") - @Operation(summary = "SM3生成确认摘要", description = "前端传入确认原文,后端直接SM3计算摘要") - public ApiResponse computeActivateDigest( + @Operation(summary = "SM3生成确认摘要", description = "前端传入确认原文,返回SM3摘要值") + public ApiResponse computeActivateDigest( @Valid @RequestBody ActivateDigestRequest request) { return ApiResponse.success(smartContractService.computeActivateDigest( InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId()); } @PostMapping("/sign-activate") - @Operation(summary = "SM2签名确认报文", description = "使用商业银行第二私钥对确认摘要进行签名(签名值=SM2_Sign(第二私钥,摘要值)),私钥错误则错误数+1") - public ApiResponse signActivate(@Valid @RequestBody SmartContractSignRequest request) { + @Operation(summary = "SM2签名确认报文", description = "使用商业银行第二私钥对摘要签名,返回签名值") + public ApiResponse signActivate(@Valid @RequestBody SmartContractSignRequest request) { return ApiResponse.success(smartContractService.signActivate( - InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey(), request.getAlgorithm()), traceId()); + InstitutionKeySubject.from(AuthContextHolder.get()), + request.getPrivateKey(), request.getAlgorithm()), traceId()); } @PostMapping("/output-confirm") - @Operation(summary = "输出央行开通确认报文JSON", description = "输出包含confirmId、walletId、orgCode、contractId、digest、signature、activateTime、status的确认报文") - public ApiResponse outputConfirm() { + @Operation(summary = "输出央行开通确认报文JSON", + description = "返回confirmId、walletId、orgCode、contractId、digest、signature、activateTime、status") + public ApiResponse outputConfirm() { return ApiResponse.success(smartContractService.outputConfirm( InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); } @PostMapping("/send") @Operation(summary = "发送开通确认报文", description = "将开通确认报文发送出去,标记步骤四完成") - public ApiResponse send() { + public ApiResponse send() { return ApiResponse.success(smartContractService.send( InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); } diff --git a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/dto/FinalSignRequest.java b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/dto/FinalSignRequest.java index 6ad023a..93db7c3 100644 --- a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/dto/FinalSignRequest.java +++ b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/dto/FinalSignRequest.java @@ -7,10 +7,15 @@ import javax.validation.constraints.NotBlank; @Data public class FinalSignRequest { - @NotBlank(message = "请输入央行第一私钥") - private String privateKey; - @NotBlank(message = "签名算法不能为空") @Schema(description = "选择的签名算法(如:SM2)", example = "SM2", required = true) private String algorithm; + + @NotBlank(message = "摘要值不能为空") + @Schema(description = "开通确认报文的摘要值", example = "A1B2C3D4E5F6...", required = true) + private String digestValue; + + @NotBlank(message = "请输入央行第一私钥") + @Schema(description = "央行第一私钥", example = "A1B2C3D4E5F6...", required = true) + private String privateKey; } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index bcaf69a..3999b39 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1430,3 +1430,39 @@ CREATE TABLE IF NOT EXISTS smart_contract_generation ( updated_by VARCHAR(64) NOT NULL, deleted BOOLEAN NOT NULL DEFAULT FALSE ); + +-- 实验记录表:记录每次实验会话 +CREATE TABLE IF NOT EXISTS experiment_record ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + user_id VARCHAR(36) NOT NULL COMMENT '用户ID', + user_name VARCHAR(100) NULL COMMENT '用户名', + school_id VARCHAR(36) NULL COMMENT '学校ID', + school_name VARCHAR(100) NULL COMMENT '学校名称', + class_id VARCHAR(36) NULL COMMENT '班级ID', + class_name VARCHAR(100) NULL COMMENT '班级名称', + experiment_no INT NOT NULL COMMENT '实验序号(第几次实验)', + module_code VARCHAR(10) NOT NULL COMMENT '模块编码(1-6)', + module_name VARCHAR(100) NOT NULL COMMENT '模块名称', + status VARCHAR(20) NOT NULL DEFAULT 'IN_PROGRESS' COMMENT '实验状态:IN_PROGRESS-进行中, COMPLETED-已完成', + start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间', + complete_time TIMESTAMP NULL COMMENT '完成时间', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_experiment_record_user (user_id), + INDEX idx_experiment_record_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实验记录表'; + +-- 实验统计表:系统首页总数据 +CREATE TABLE IF NOT EXISTS experiment_statistics ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + total_experiments INT NOT NULL DEFAULT 0 COMMENT '实验总次数', + completed_experiments INT NOT NULL DEFAULT 0 COMMENT '完成实验次数', + in_progress_experiments INT NOT NULL DEFAULT 0 COMMENT '进行中实验次数', + not_started_experiments INT NOT NULL DEFAULT 0 COMMENT '未开始实验次数', + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实验统计表'; + +-- 初始化统计记录 +INSERT INTO experiment_statistics (id, total_experiments, completed_experiments, in_progress_experiments, not_started_experiments) +VALUES (1, 0, 0, 0, 0) +ON DUPLICATE KEY UPDATE id = id;