feat:文件上传的功能开发

chore/issuance-comments-zh
赵佳正 3 weeks ago
parent 72b771b409
commit 04cda82cf0

@ -19,7 +19,7 @@ import java.util.UUID;
*/ */
@Service @Service
public class SalaryScoreService { 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 int MODULE_SERIAL_NUMBER = 6;
private static final double TOTAL_SCORE = 100.0; private static final double TOTAL_SCORE = 100.0;

@ -43,6 +43,10 @@ public class ControlSystemSignatureService {
"当前实训已有步骤七控制系统签名记录,如需重新开始请先调用重新实训接口"); "当前实训已有步骤七控制系统签名记录,如需重新开始请先调用重新实训接口");
} }
String digest = cryptography.sm3(normalizedOriginalText); 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, update(current, current.getStatus(), "DIGESTED", operator,
new LambdaUpdateWrapper<ControlSystemSignatureEntity>() new LambdaUpdateWrapper<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getOriginalText, normalizedOriginalText) .set(ControlSystemSignatureEntity::getOriginalText, normalizedOriginalText)
@ -90,6 +94,9 @@ public class ControlSystemSignatureService {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"中央银行第一私钥不正确,本次实训第" + errorSequence + "次错误"); "中央银行第一私钥不正确,本次实训第" + errorSequence + "次错误");
} }
if (value.getSignature() != null && value.getSignature().equals(signature)) {
return new ControlSystemSignatureResult(value);
}
update(value, value.getStatus(), "SIGNED", operator, update(value, value.getStatus(), "SIGNED", operator,
new LambdaUpdateWrapper<ControlSystemSignatureEntity>() new LambdaUpdateWrapper<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getSigningKeyId, InstitutionKeyService.CENTRAL_FIRST_KEY) .set(ControlSystemSignatureEntity::getSigningKeyId, InstitutionKeyService.CENTRAL_FIRST_KEY)

@ -43,6 +43,9 @@ public class QuotaControlBitService {
String originalText = source.getAmount().setScale(2).toPlainString() + "|" String originalText = source.getAmount().setScale(2).toPlainString() + "|"
+ source.getInstitutionIdentifier() + "|" + source.getTransactionIdentifier(); + source.getInstitutionIdentifier() + "|" + source.getTransactionIdentifier();
String segment = "STATIC_" + cryptography.sm3(originalText); 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, update(current, current.getStatus(), "STATIC_READY", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getAmount, source.getAmount().setScale(2)) .set(QuotaControlBitEntity::getAmount, source.getAmount().setScale(2))
@ -86,6 +89,9 @@ public class QuotaControlBitService {
if (value.getStaticSegment() == null) { if (value.getStaticSegment() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成静态段"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成静态段");
} }
if (value.getDynamicSegment() != null) {
return new QuotaControlDynamicSegmentResult(value.getDynamicSegment());
}
String randomValue = randomValue(); String randomValue = randomValue();
String timestamp = LocalDateTime.now().format(TIMESTAMP); String timestamp = LocalDateTime.now().format(TIMESTAMP);
String segment = "DYN_" + randomValue + "_" + timestamp; String segment = "DYN_" + randomValue + "_" + timestamp;
@ -112,6 +118,9 @@ public class QuotaControlBitService {
if (!source.getId().equals(value.getControlSignatureId()) || source.getControlSignature() == null) { if (!source.getId().equals(value.getControlSignatureId()) || source.getControlSignature() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤七央行控制系统签名数据不完整"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤七央行控制系统签名数据不完整");
} }
if (value.getCentralBankSignatureSegment() != null) {
return new CentralBankSignatureSegmentResult(value.getCentralBankSignatureSegment());
}
update(value, value.getStatus(), "CB_SIGNATURE_READY", operator, update(value, value.getStatus(), "CB_SIGNATURE_READY", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getCentralBankSignatureSegment, .set(QuotaControlBitEntity::getCentralBankSignatureSegment,
@ -134,6 +143,9 @@ public class QuotaControlBitService {
requireResult(value.getCentralBankSignatureSegment(), "请先读取央行签名段"); requireResult(value.getCentralBankSignatureSegment(), "请先读取央行签名段");
String complete = value.getStaticSegment() + "|" + value.getDynamicSegment() + "|" String complete = value.getStaticSegment() + "|" + value.getDynamicSegment() + "|"
+ value.getCentralBankSignatureSegment(); + value.getCentralBankSignatureSegment();
if (value.getCompleteControlBit() != null && value.getCompleteControlBit().equals(complete)) {
return new QuotaControlBitResult(value);
}
update(value, value.getStatus(), "ASSEMBLED", operator, update(value, value.getStatus(), "ASSEMBLED", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getCompleteControlBit, complete) .set(QuotaControlBitEntity::getCompleteControlBit, complete)
@ -166,6 +178,9 @@ public class QuotaControlBitService {
if (signature == null) { if (signature == null) {
throw keyError(subject, "签名必须使用当前实训主体的商业银行第二私钥"); throw keyError(subject, "签名必须使用当前实训主体的商业银行第二私钥");
} }
if (value.getBankSignature() != null && value.getBankSignature().equals(signature)) {
return new QuotaControlBitResult(value);
}
update(value, value.getStatus(), "SIGNED", operator, update(value, value.getStatus(), "SIGNED", operator,
new LambdaUpdateWrapper<QuotaControlBitEntity>() new LambdaUpdateWrapper<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getCompleteControlBit, submittedControlBit) .set(QuotaControlBitEntity::getCompleteControlBit, submittedControlBit)

@ -52,6 +52,9 @@ public class StandardCurrencyService {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"当前实训已有步骤九记录,如需重新开始请先调用重新实训接口"); "当前实训已有步骤九记录,如需重新开始请先调用重新实训接口");
} }
if ("DECOMPOSED".equals(current.getStatus()) && current.getDenominationSummary() != null) {
return result(current);
}
currencyMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyEntity>() currencyMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyEntity>()
.eq(StandardCurrencyEntity::getBatchId, current.getId()) .eq(StandardCurrencyEntity::getBatchId, current.getId())
.eq(StandardCurrencyEntity::getDeleted, false) .eq(StandardCurrencyEntity::getDeleted, false)

@ -99,6 +99,10 @@ public class CurrencyGenerationRequest {
if (digest == null || !digest.matches("[0-9A-F]{64}")) { if (digest == null || !digest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("SM3摘要格式不正确"); 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.identifierApplicationId = replacement.identifierApplicationId;
this.institutionIdentifier = replacement.institutionIdentifier; this.institutionIdentifier = replacement.institutionIdentifier;
this.fullInstitutionIdentifier = replacement.fullInstitutionIdentifier; this.fullInstitutionIdentifier = replacement.fullInstitutionIdentifier;
@ -129,6 +133,10 @@ public class CurrencyGenerationRequest {
if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) { if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) {
throw new IllegalArgumentException("SM2签名信息不正确"); throw new IllegalArgumentException("SM2签名信息不正确");
} }
if (this.bankSignature != null && this.bankSignature.equals(signature)
&& keyId.equals(this.signingKeyId)) {
return;
}
signingKeyId = keyId; signingKeyId = keyId;
bankSignature = signature; bankSignature = signature;
this.submittedPrivateKey = submittedPrivateKey; this.submittedPrivateKey = submittedPrivateKey;

@ -99,6 +99,9 @@ public class CurrencyGenerationVerification {
} }
public void receiveAgain() { public void receiveAgain() {
if (status != null && status.ordinal() >= Status.RECEIVED.ordinal()) {
return;
}
signatureValid = null; signatureValid = null;
recomputedDigest = null; recomputedDigest = null;
digestMatches = null; digestMatches = null;
@ -128,6 +131,9 @@ public class CurrencyGenerationVerification {
if (digest == null || !digest.matches("[0-9A-F]{64}")) { if (digest == null || !digest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("重新计算的摘要一格式不正确"); throw new IllegalArgumentException("重新计算的摘要一格式不正确");
} }
if (this.recomputedDigest != null && this.recomputedDigest.equalsIgnoreCase(digest)) {
return;
}
recomputedDigest = digest; recomputedDigest = digest;
this.userInputOriginalText = userInputOriginalText; this.userInputOriginalText = userInputOriginalText;
digestMatches = null; digestMatches = null;

@ -68,12 +68,17 @@ public class InstitutionIdentifierApplication {
public void replaceDigestInput(String bankCode, String timestamp, String originalText, String digest) { public void replaceDigestInput(String bankCode, String timestamp, String originalText, String digest) {
validateBase(bankCode, timestamp); validateBase(bankCode, timestamp);
this.bankCode = bankCode; String validatedOriginalText = requireOriginalText(bankCode, timestamp, originalText);
this.timestamp = timestamp;
this.originalText = requireOriginalText(bankCode, timestamp, originalText);
if (digest == null || !digest.matches("[0-9A-F]{64}")) { if (digest == null || !digest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("SM3摘要格式不正确"); 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.digest = digest;
this.bankSignature = null; this.bankSignature = null;
this.verificationKeyId = null; this.verificationKeyId = null;
@ -101,6 +106,9 @@ public class InstitutionIdentifierApplication {
if (bankSignature == null || !bankSignature.matches("[0-9A-F]+")) { if (bankSignature == null || !bankSignature.matches("[0-9A-F]+")) {
throw new IllegalArgumentException("银行SM2签名格式不正确"); throw new IllegalArgumentException("银行SM2签名格式不正确");
} }
if (this.bankSignature != null && this.bankSignature.equals(bankSignature)) {
return;
}
this.bankSignature = bankSignature; this.bankSignature = bankSignature;
this.verificationKeyId = null; this.verificationKeyId = null;
this.signatureValid = null; this.signatureValid = null;

@ -135,6 +135,10 @@ public class TransactionInformationIdentifier {
if (inputAmount.compareTo(amount) != 0 || !institutionIdentifier.equals(parts[1])) { if (inputAmount.compareTo(amount) != 0 || !institutionIdentifier.equals(parts[1])) {
throw new IllegalArgumentException("拼接请求原文中的金额或机构标识与前序数据不一致"); throw new IllegalArgumentException("拼接请求原文中的金额或机构标识与前序数据不一致");
} }
if (this.originalText != null && this.originalText.equals(originalText)
&& this.digest != null && this.digest.equalsIgnoreCase(digest)) {
return;
}
this.originalText = originalText; this.originalText = originalText;
this.transactionTimestamp = parts[2]; this.transactionTimestamp = parts[2];
this.digest = digest; this.digest = digest;
@ -198,6 +202,9 @@ public class TransactionInformationIdentifier {
if (quotaDigest == null || !quotaDigest.matches("[0-9A-F]{64}")) { if (quotaDigest == null || !quotaDigest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("额度请求SM3摘要格式不正确"); throw new IllegalArgumentException("额度请求SM3摘要格式不正确");
} }
if (this.quotaDigest != null && this.quotaDigest.equalsIgnoreCase(quotaDigest)) {
return;
}
this.quotaDigest = quotaDigest; this.quotaDigest = quotaDigest;
clearQuotaSignatureAndFollowingResults(); clearQuotaSignatureAndFollowingResults();
this.status = Status.QUOTA_DIGESTED; this.status = Status.QUOTA_DIGESTED;
@ -210,6 +217,10 @@ public class TransactionInformationIdentifier {
if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) { if (keyId == null || signature == null || !signature.matches("[0-9A-F]+")) {
throw new IllegalArgumentException("额度请求SM2签名信息不正确"); throw new IllegalArgumentException("额度请求SM2签名信息不正确");
} }
if (this.quotaSignature != null && this.quotaSignature.equals(signature)
&& keyId.equals(this.quotaSigningKeyId)) {
return;
}
this.quotaSigningKeyId = keyId; this.quotaSigningKeyId = keyId;
this.quotaSignature = signature; this.quotaSignature = signature;
clearQuotaPackageAndVerificationResults(); clearQuotaPackageAndVerificationResults();
@ -263,6 +274,9 @@ public class TransactionInformationIdentifier {
if (recomputedDigest == null || !recomputedDigest.matches("[0-9A-F]{64}")) { if (recomputedDigest == null || !recomputedDigest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("H1格式不正确"); throw new IllegalArgumentException("H1格式不正确");
} }
if (this.quotaRecomputedDigest != null && this.quotaRecomputedDigest.equalsIgnoreCase(recomputedDigest)) {
return;
}
this.quotaRecomputedDigest = recomputedDigest; this.quotaRecomputedDigest = recomputedDigest;
this.quotaVerificationKeyId = null; this.quotaVerificationKeyId = null;
this.quotaSignatureDigest = null; this.quotaSignatureDigest = null;
@ -279,6 +293,9 @@ public class TransactionInformationIdentifier {
|| !signatureDigest.matches("[0-9A-F]{64}")) { || !signatureDigest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("第二公钥验签获取摘要失败"); throw new IllegalArgumentException("第二公钥验签获取摘要失败");
} }
if (this.quotaSignatureDigest != null && this.quotaSignatureDigest.equalsIgnoreCase(signatureDigest)) {
return;
}
this.quotaVerificationKeyId = verificationKeyId; this.quotaVerificationKeyId = verificationKeyId;
this.quotaSignatureDigest = signatureDigest; this.quotaSignatureDigest = signatureDigest;
this.quotaSignatureValid = true; this.quotaSignatureValid = true;

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

@ -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<ExperimentRecordEntity> page = new Page<>(current, size);
IPage<ExperimentRecordEntity> result = recordMapper.selectPage(page,
new LambdaQueryWrapper<ExperimentRecordEntity>()
.eq(ExperimentRecordEntity::getUserId, userId)
.orderByDesc(ExperimentRecordEntity::getStartTime));
List<ExperimentRecordResult> 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<ExperimentRecordEntity>()
.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 "未知模块";
}
}
}

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

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

@ -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<ExperimentProgressResult.ModuleProgressItem> 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<StuModuleScoreDetailsEntity>()
.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<String> 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<StuModuleScoreDetailsEntity>()
.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<ModuleStepStatus> 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<String> 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<ModuleStepStatus> 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<ModuleStepStatus> buildSteps(ModuleConfig config, int completedSteps) {
List<ModuleStepStatus> 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;
}
}
}

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

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

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

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

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

@ -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<Void> 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<Void> 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<ExperimentStatisticsResult> getStatistics() {
return ApiResponse.success(service.getStatistics(), traceId());
}
@GetMapping("/recent-records")
@Operation(summary = "获取实验记录(分页)",
description = "分页返回当前用户的实验记录列表。参数:current-当前页码,size-每页数量。")
public ApiResponse<ExperimentRecordPageResult> 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);
}
}

@ -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<ModuleCompletionResult> 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<ExperimentProgressResult> getExperimentProgress() {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.getExperimentProgress(user.getUserId()), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -156,18 +156,18 @@ public class CentralWalletActivationService {
} }
@Transactional @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() : "")) { if (!"SM2".equalsIgnoreCase(algorithm != null ? algorithm.trim() : "")) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1");
} }
CentralWalletActivation a = getOrCreate(subject); CentralWalletActivation a = getOrCreate(subject);
if (!a.isWalletActivated()) { if (!a.isReceived()) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成步骤四钱包激活"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成步骤四开通确认报文发送");
} }
if (a.getStep5SignVerifyResult() != null && a.getStep5SignVerifyResult().startsWith("通过")) { if (a.getStep5SignVerifyResult() != null && a.getStep5SignVerifyResult().startsWith("通过")) {
return new CentralWalletActivationPageResult(a); return "通过";
} }
String storedPublicKey = institutionKeyService.commercialBankSecondPublicKeyOrNull(subject); String storedPublicKey = institutionKeyService.commercialBankSecondPublicKeyOrNull(subject);
if (storedPublicKey == null) { if (storedPublicKey == null) {
@ -189,11 +189,12 @@ public class CentralWalletActivationService {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "签名验证失败"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "签名验证失败");
} }
activationRepository.update(a); activationRepository.update(a);
return new CentralWalletActivationPageResult(a); return "通过";
} }
@Transactional @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() : "")) { if (!"SM2".equalsIgnoreCase(algorithm != null ? algorithm.trim() : "")) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1");
@ -204,7 +205,11 @@ public class CentralWalletActivationService {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM2签名验证"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM2签名验证");
} }
if (a.getCbFinalSignature() != null) { 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); String storedPrivateKey = institutionKeyService.centralBankFirstPrivateKeyOrNull(subject);
if (storedPrivateKey == null) { if (storedPrivateKey == null) {
@ -219,33 +224,35 @@ public class CentralWalletActivationService {
String cbFinalSignature = "CB_FINAL_WALLET_" + sig; String cbFinalSignature = "CB_FINAL_WALLET_" + sig;
a.finalSign(cbFinalSignature); a.finalSign(cbFinalSignature);
activationRepository.update(a); activationRepository.update(a);
return new CentralWalletActivationPageResult(a); return cbFinalSignature;
} }
@Transactional @Transactional
public CentralWalletActivationPageResult updateWalletRegisterStatus(InstitutionKeySubject subject) { public String updateWalletRegisterStatus(InstitutionKeySubject subject) {
CentralWalletActivation a = getOrCreate(subject); CentralWalletActivation a = getOrCreate(subject);
if (a.getCbFinalSignature() == null) { if (a.getCbFinalSignature() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成央行最终确权签名"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成央行最终确权签名");
} }
if (a.getWalletRegisterStatus() != null) { if (a.getWalletRegisterStatus() != null) {
return new CentralWalletActivationPageResult(a); return "成功";
} }
a.updateWalletRegisterStatus(); a.updateWalletRegisterStatus();
activationRepository.update(a); activationRepository.update(a);
return new CentralWalletActivationPageResult(a); return "成功";
} }
@Transactional @Transactional
public CentralWalletActivationPageResult outputFinalConfirm(InstitutionKeySubject subject) { public FinalConfirmOutputResult outputFinalConfirm(InstitutionKeySubject subject) {
CentralWalletActivation a = getOrCreate(subject); CentralWalletActivation a = getOrCreate(subject);
if (a.getWalletRegisterStatus() == null) { if (a.getWalletRegisterStatus() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先更新钱包登记状态"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先更新钱包登记状态");
} }
if (a.getFinalConfirmJson() != null) { 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 date = LocalDateTime.now().format(DATE_FORMAT);
String finalConfirmId = "FINAL_CONFIRM_" + date + "_001"; String finalConfirmId = "FINAL_CONFIRM_" + date + "_001";
@ -254,11 +261,13 @@ public class CentralWalletActivationService {
a.getCbFinalSignature(), finalTime); a.getCbFinalSignature(), finalTime);
a.outputFinalConfirm(finalConfirmId, json, finalTime); a.outputFinalConfirm(finalConfirmId, json, finalTime);
activationRepository.update(a); activationRepository.update(a);
return new CentralWalletActivationPageResult(a); return new FinalConfirmOutputResult(
finalConfirmId, a.getWalletId(), "AVAILABLE",
a.getCbFinalSignature(), finalTime);
} }
@Transactional @Transactional
public CentralWalletActivationPageResult sendFinal(InstitutionKeySubject subject) { public String sendFinal(InstitutionKeySubject subject) {
CentralWalletActivation a = getOrCreate(subject); CentralWalletActivation a = getOrCreate(subject);
if (a.getFinalConfirmJson() == null) { if (a.getFinalConfirmJson() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
@ -266,7 +275,8 @@ public class CentralWalletActivationService {
} }
a.sendFinal(); a.sendFinal();
activationRepository.update(a); activationRepository.update(a);
return new CentralWalletActivationPageResult(a); scoreService.markStepCompleted(subject.getUserId(), 5);
return "成功";
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -288,10 +298,23 @@ public class CentralWalletActivationService {
} }
private CentralWalletActivation getOrCreate(InstitutionKeySubject subject) { private CentralWalletActivation getOrCreate(InstitutionKeySubject subject) {
return activationRepository CentralWalletActivation a = activationRepository
.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId()) .findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseGet(() -> activationRepository.save( .orElse(null);
CentralWalletActivation.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId()))); 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, private String buildFinalConfirmJson(String finalConfirmId, String walletId,

@ -57,14 +57,14 @@ public class SmartContractService {
} }
@Transactional @Transactional
public SmartContractPageResult computeContractDigest(InstitutionKeySubject subject, ContractDigestRequest request) { public String computeContractDigest(InstitutionKeySubject subject, ContractDigestRequest request) {
if (!"SM3".equalsIgnoreCase(request.getAlgorithm() != null ? request.getAlgorithm().trim() : "")) { if (!"SM3".equalsIgnoreCase(request.getAlgorithm() != null ? request.getAlgorithm().trim() : "")) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1");
} }
SmartContractGeneration c = getOrCreate(subject); SmartContractGeneration c = getOrCreate(subject);
if (c.getContractDigest() != null) { if (c.getContractDigest() != null) {
return new SmartContractPageResult(c); return c.getContractDigest();
} }
verifyStep3RegistrationConfirmed(subject); verifyStep3RegistrationConfirmed(subject);
String storedWalletId = resolveWalletId(subject); String storedWalletId = resolveWalletId(subject);
@ -84,14 +84,20 @@ public class SmartContractService {
String digest = cryptography.sm3(c.getContractPlaintext()); String digest = cryptography.sm3(c.getContractPlaintext());
c.computeContractDigest(digest); c.computeContractDigest(digest);
contractRepository.update(c); contractRepository.update(c);
return new SmartContractPageResult(c); return digest;
} }
@Transactional @Transactional
public SmartContractPageResult outputContract(InstitutionKeySubject subject) { public ContractOutputResult outputContract(InstitutionKeySubject subject) {
SmartContractGeneration c = getOrCreate(subject); SmartContractGeneration c = getOrCreate(subject);
if (c.getContractJson() != null) { 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) { if (c.getContractDigest() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
@ -103,14 +109,21 @@ public class SmartContractService {
String json = buildContractJson(contractId, c); String json = buildContractJson(contractId, c);
c.outputContract(contractId, json, effectiveTime); c.outputContract(contractId, json, effectiveTime);
contractRepository.update(c); 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 @Transactional
public SmartContractPageResult acquireActivateTimestamp(InstitutionKeySubject subject) { public ActivateTimestampResult acquireActivateTimestamp(InstitutionKeySubject subject) {
SmartContractGeneration c = getOrCreate(subject); SmartContractGeneration c = getOrCreate(subject);
if (c.getStatus() != null && c.getStatus().ordinal() >= SmartContractGeneration.Status.ACTIVATE_TIMESTAMP_ACQUIRED.ordinal()) { 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) { if (c.getContractId() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
@ -120,18 +133,18 @@ public class SmartContractService {
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
c.acquireActivateTimestamp(timestamp, orgCode); c.acquireActivateTimestamp(timestamp, orgCode);
contractRepository.update(c); contractRepository.update(c);
return new SmartContractPageResult(c); return new ActivateTimestampResult(timestamp, c.getWalletId(), orgCode, c.getContractId());
} }
@Transactional @Transactional
public SmartContractPageResult computeActivateDigest(InstitutionKeySubject subject, ActivateDigestRequest request) { public String computeActivateDigest(InstitutionKeySubject subject, ActivateDigestRequest request) {
if (!"SM3".equalsIgnoreCase(request.getAlgorithm() != null ? request.getAlgorithm().trim() : "")) { if (!"SM3".equalsIgnoreCase(request.getAlgorithm() != null ? request.getAlgorithm().trim() : "")) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1");
} }
SmartContractGeneration c = getOrCreate(subject); SmartContractGeneration c = getOrCreate(subject);
if (c.getActivateDigest() != null) { if (c.getActivateDigest() != null) {
return new SmartContractPageResult(c); return c.getActivateDigest();
} }
if (c.getContractId() == null) { if (c.getContractId() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
@ -145,18 +158,18 @@ public class SmartContractService {
String digest = cryptography.sm3(c.getActivatePlaintext()); String digest = cryptography.sm3(c.getActivatePlaintext());
c.computeActivateDigest(digest); c.computeActivateDigest(digest);
contractRepository.update(c); contractRepository.update(c);
return new SmartContractPageResult(c); return digest;
} }
@Transactional @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() : "")) { if (!"SM2".equalsIgnoreCase(algorithm != null ? algorithm.trim() : "")) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "算法选择有误,错误数+1");
} }
SmartContractGeneration c = getOrCreate(subject); SmartContractGeneration c = getOrCreate(subject);
if (c.getActivateSignature() != null) { if (c.getActivateSignature() != null) {
return new SmartContractPageResult(c); return c.getActivateSignature();
} }
if (c.getActivateDigest() == null) { if (c.getActivateDigest() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
@ -175,14 +188,16 @@ public class SmartContractService {
String signature = cryptography.sign(storedPrivateKey, c.getActivateDigest()); String signature = cryptography.sign(storedPrivateKey, c.getActivateDigest());
c.signActivate(privateKeyInput.trim(), signature); c.signActivate(privateKeyInput.trim(), signature);
contractRepository.update(c); contractRepository.update(c);
return new SmartContractPageResult(c); return signature;
} }
@Transactional @Transactional
public SmartContractPageResult outputConfirm(InstitutionKeySubject subject) { public ActivateConfirmOutputResult outputConfirm(InstitutionKeySubject subject) {
SmartContractGeneration c = getOrCreate(subject); SmartContractGeneration c = getOrCreate(subject);
if (c.getConfirmJson() != null) { 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) { if (c.getActivateSignature() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
@ -193,14 +208,16 @@ public class SmartContractService {
String json = buildConfirmJson(confirmId, c); String json = buildConfirmJson(confirmId, c);
c.outputConfirm(confirmId, json); c.outputConfirm(confirmId, json);
contractRepository.update(c); 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 @Transactional
public SmartContractPageResult send(InstitutionKeySubject subject) { public String send(InstitutionKeySubject subject) {
SmartContractGeneration c = getOrCreate(subject); SmartContractGeneration c = getOrCreate(subject);
if (c.isSent()) { if (c.isSent()) {
return new SmartContractPageResult(c); return "成功";
} }
if (c.getConfirmJson() == null) { if (c.getConfirmJson() == null) {
scoreService.recordError(subject.getUserId()); scoreService.recordError(subject.getUserId());
@ -209,7 +226,7 @@ public class SmartContractService {
c.markSent(); c.markSent();
contractRepository.update(c); contractRepository.update(c);
scoreService.markStepCompleted(subject.getUserId(), 6); scoreService.markStepCompleted(subject.getUserId(), 6);
return new SmartContractPageResult(c); return "成功";
} }
private String buildContractJson(String contractId, SmartContractGeneration c) { private String buildContractJson(String contractId, SmartContractGeneration c) {

@ -4,14 +4,13 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter; 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.application.CentralWalletActivationService;
import com.yau.digitalrmb.walletopening.interfaces.rest.dto.CentralActivationVerifySignRequest; import com.yau.digitalrmb.walletopening.interfaces.rest.dto.CentralActivationVerifySignRequest;
import com.yau.digitalrmb.walletopening.interfaces.rest.dto.FinalSignRequest; import com.yau.digitalrmb.walletopening.interfaces.rest.dto.FinalSignRequest;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.MDC; import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -23,55 +22,47 @@ import javax.validation.Valid;
@RestController @RestController
@RequestMapping("/api/v1/central-banks/wallet-opening/step5") @RequestMapping("/api/v1/central-banks/wallet-opening/step5")
@Tag(name = "数字货币钱包-央行端 - 步骤五:最终确权并通知用户", @Tag(name = "数字货币钱包-央行端 - 步骤五:最终确权并通知用户",
description = "央行SM2验签(商业银行第二公钥)→最终确权签名(央行第一私钥)→钱包登记状态更新(AVAILABLE)→输出最终确权通知→发送给用户") description = "央行SM2验签 → 最终确权签名 → 更新钱包登记状态 → 输出最终确权通知 → 发送给用户")
public class CentralBankWalletFinalConfirmController { public class CentralBankWalletFinalConfirmController {
@Resource @Resource
private CentralWalletActivationService activationService; private CentralWalletActivationService activationService;
@GetMapping("/page")
@Operation(summary = "查询央行端步骤五页面数据", description = "返回验签结果、确权签名、登记状态、最终通知等")
public ApiResponse<CentralWalletActivationPageResult> getPage() {
return ApiResponse.success(activationService.getPage(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/verify-sign") @PostMapping("/verify-sign")
@Operation(summary = "SM2验证签名", @Operation(summary = "SM2验证签名",
description = "选择商业银行第二公钥进行SM2_Verify(第二公钥, 摘要值, 签名值),公钥错误则错误数+1") description = "选择商业银行第二公钥进行SM2验签,返回通过/未通过,失败则错误数+1并说明原因")
public ApiResponse<CentralWalletActivationPageResult> verifySign( public ApiResponse<String> verifySign(@Valid @RequestBody CentralActivationVerifySignRequest request) {
@Valid @RequestBody CentralActivationVerifySignRequest request) {
return ApiResponse.success(activationService.verifyFinalSignature( return ApiResponse.success(activationService.verifyFinalSignature(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPublicKey(), request.getAlgorithm()), traceId()); InstitutionKeySubject.from(AuthContextHolder.get()), request.getPublicKey(), request.getAlgorithm()), traceId());
} }
@PostMapping("/final-sign") @PostMapping("/final-sign")
@Operation(summary = "央行最终确权签名", @Operation(summary = "央行最终确权签名",
description = "使用央行第一私钥对摘要进行SM2签名,生成cbFinalSignature,私钥错误则错误数+1") description = "传算法、摘要值、央行第一私钥,校验通过后返回确权签名值")
public ApiResponse<CentralWalletActivationPageResult> finalSign( public ApiResponse<String> finalSign(@Valid @RequestBody FinalSignRequest request) {
@Valid @RequestBody FinalSignRequest request) {
return ApiResponse.success(activationService.finalSign( 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") @PostMapping("/update-status")
@Operation(summary = "更新钱包登记状态", description = "将钱包登记状态更新为AVAILABLE") @Operation(summary = "更新钱包登记状态", description = "将钱包登记状态更新为AVAILABLE,返回成功/失败")
public ApiResponse<CentralWalletActivationPageResult> updateStatus() { public ApiResponse<String> updateStatus() {
return ApiResponse.success(activationService.updateWalletRegisterStatus( return ApiResponse.success(activationService.updateWalletRegisterStatus(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
@PostMapping("/output-confirm") @PostMapping("/output-confirm")
@Operation(summary = "输出最终确权通知", @Operation(summary = "输出最终确权通知",
description = "生成包含finalConfirmId、walletId、status=AVAILABLE、cbFinalSignature的最终通知JSON") description = "生成最终确权通知,返回finalConfirmId、walletId、status、cbFinalSignature、finalTime")
public ApiResponse<CentralWalletActivationPageResult> outputConfirm() { public ApiResponse<FinalConfirmOutputResult> outputConfirm() {
return ApiResponse.success(activationService.outputFinalConfirm( return ApiResponse.success(activationService.outputFinalConfirm(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
@PostMapping("/send") @PostMapping("/send")
@Operation(summary = "发送最终确权通知", description = "将最终确权通知发送给用户端") @Operation(summary = "发送最终确权通知", description = "将最终确权通知发送给用户端")
public ApiResponse<CentralWalletActivationPageResult> send() { public ApiResponse<String> send() {
return ApiResponse.success(activationService.sendFinal( return ApiResponse.success(activationService.sendFinal(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }

@ -3,8 +3,10 @@ package com.yau.digitalrmb.walletopening.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter; 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.SmartContractPageResult;
import com.yau.digitalrmb.walletopening.application.SmartContractService; import com.yau.digitalrmb.walletopening.application.SmartContractService;
import com.yau.digitalrmb.walletopening.interfaces.rest.dto.ActivateDigestRequest; import com.yau.digitalrmb.walletopening.interfaces.rest.dto.ActivateDigestRequest;
@ -25,7 +27,7 @@ import javax.validation.Valid;
@RestController @RestController
@RequestMapping("/api/v1/commercial-banks/wallet-opening/step4") @RequestMapping("/api/v1/commercial-banks/wallet-opening/step4")
@Tag(name = "数字货币钱包-商业银行端 - 步骤四:生成合约信息", @Tag(name = "数字货币钱包-商业银行端 - 步骤四:生成合约信息",
description = "商业银行端①生成合约(前端传入钱包标识ID及6个合约参数→校验钱包标识ID→SM3摘要→输出智能合约JSON);②提交开通完成确认(读取时间戳→前端传入确认原文→SM3摘要→SM2签名(第二私钥)→输出确认报文→发送)") description = "①生成合约摘要→输出智能合约;②读取时间戳→生成确认摘要→SM2签名→输出开通确认报文→发送")
public class CommercialBankSmartContractController { public class CommercialBankSmartContractController {
@Resource @Resource
@ -40,52 +42,56 @@ public class CommercialBankSmartContractController {
@PostMapping("/contract-digest") @PostMapping("/contract-digest")
@Operation(summary = "SM3生成合约摘要", @Operation(summary = "SM3生成合约摘要",
description = "前端传入钱包标识ID、钱包类型、单笔/日累计/年累计支付限额、余额上限、有效期、时间戳。后端校验钱包标识ID是否正确(错误则错题数+1),拼接合约原文后SM3计算摘要") description = "前端传入钱包标识ID和合约参数,校验后返回SM3合约摘要值")
public ApiResponse<SmartContractPageResult> computeContractDigest( public ApiResponse<String> computeContractDigest(
@Valid @RequestBody ContractDigestRequest request) { @Valid @RequestBody ContractDigestRequest request) {
return ApiResponse.success(smartContractService.computeContractDigest( return ApiResponse.success(smartContractService.computeContractDigest(
InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId()); InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId());
} }
@PostMapping("/output-contract") @PostMapping("/output-contract")
@Operation(summary = "输出智能合约信息JSON", description = "输出包含contractId、walletId、walletType、limits、validity、contractHash、status、effectiveTime的合约JSON") @Operation(summary = "输出智能合约信息JSON",
public ApiResponse<SmartContractPageResult> outputContract() { description = "返回contractId、walletId、walletType、limits、validity、contractHash、status、effectiveTime")
public ApiResponse<ContractOutputResult> outputContract() {
return ApiResponse.success(smartContractService.outputContract( return ApiResponse.success(smartContractService.outputContract(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
@PostMapping("/read-activate-timestamp") @PostMapping("/read-activate-timestamp")
@Operation(summary = "读取当前时间戳(确认阶段)", description = "生成开通确认所需的当前时间戳,同时带出机构代码") @Operation(summary = "读取当前时间戳(确认阶段)",
public ApiResponse<SmartContractPageResult> readActivateTimestamp() { description = "返回时间戳、钱包标识ID、机构代码、合约ID")
public ApiResponse<ActivateTimestampResult> readActivateTimestamp() {
return ApiResponse.success(smartContractService.acquireActivateTimestamp( return ApiResponse.success(smartContractService.acquireActivateTimestamp(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
@PostMapping("/activate-digest") @PostMapping("/activate-digest")
@Operation(summary = "SM3生成确认摘要", description = "前端传入确认原文,后端直接SM3计算摘要") @Operation(summary = "SM3生成确认摘要", description = "前端传入确认原文,返回SM3摘要值")
public ApiResponse<SmartContractPageResult> computeActivateDigest( public ApiResponse<String> computeActivateDigest(
@Valid @RequestBody ActivateDigestRequest request) { @Valid @RequestBody ActivateDigestRequest request) {
return ApiResponse.success(smartContractService.computeActivateDigest( return ApiResponse.success(smartContractService.computeActivateDigest(
InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId()); InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId());
} }
@PostMapping("/sign-activate") @PostMapping("/sign-activate")
@Operation(summary = "SM2签名确认报文", description = "使用商业银行第二私钥对确认摘要进行签名(签名值=SM2_Sign(第二私钥,摘要值)),私钥错误则错误数+1") @Operation(summary = "SM2签名确认报文", description = "使用商业银行第二私钥对摘要签名,返回签名值")
public ApiResponse<SmartContractPageResult> signActivate(@Valid @RequestBody SmartContractSignRequest request) { public ApiResponse<String> signActivate(@Valid @RequestBody SmartContractSignRequest request) {
return ApiResponse.success(smartContractService.signActivate( 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") @PostMapping("/output-confirm")
@Operation(summary = "输出央行开通确认报文JSON", description = "输出包含confirmId、walletId、orgCode、contractId、digest、signature、activateTime、status的确认报文") @Operation(summary = "输出央行开通确认报文JSON",
public ApiResponse<SmartContractPageResult> outputConfirm() { description = "返回confirmId、walletId、orgCode、contractId、digest、signature、activateTime、status")
public ApiResponse<ActivateConfirmOutputResult> outputConfirm() {
return ApiResponse.success(smartContractService.outputConfirm( return ApiResponse.success(smartContractService.outputConfirm(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
@PostMapping("/send") @PostMapping("/send")
@Operation(summary = "发送开通确认报文", description = "将开通确认报文发送出去,标记步骤四完成") @Operation(summary = "发送开通确认报文", description = "将开通确认报文发送出去,标记步骤四完成")
public ApiResponse<SmartContractPageResult> send() { public ApiResponse<String> send() {
return ApiResponse.success(smartContractService.send( return ApiResponse.success(smartContractService.send(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }

@ -7,10 +7,15 @@ import javax.validation.constraints.NotBlank;
@Data @Data
public class FinalSignRequest { public class FinalSignRequest {
@NotBlank(message = "请输入央行第一私钥")
private String privateKey;
@NotBlank(message = "签名算法不能为空") @NotBlank(message = "签名算法不能为空")
@Schema(description = "选择的签名算法(如:SM2)", example = "SM2", required = true) @Schema(description = "选择的签名算法(如:SM2)", example = "SM2", required = true)
private String algorithm; 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;
} }

@ -1430,3 +1430,39 @@ CREATE TABLE IF NOT EXISTS smart_contract_generation (
updated_by VARCHAR(64) NOT NULL, updated_by VARCHAR(64) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE 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;

Loading…
Cancel
Save