Merge remote-tracking branch 'origin/master'

chore/issuance-comments-zh
陈沅 3 weeks ago
commit cd41f337da

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

@ -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<ControlSystemSignatureEntity>()
.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<ControlSystemSignatureEntity>()
.set(ControlSystemSignatureEntity::getSigningKeyId, InstitutionKeyService.CENTRAL_FIRST_KEY)

@ -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<QuotaControlBitEntity>()
.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<QuotaControlBitEntity>()
.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<QuotaControlBitEntity>()
.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<QuotaControlBitEntity>()
.set(QuotaControlBitEntity::getCompleteControlBit, submittedControlBit)

@ -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<StandardCurrencyEntity>()
.eq(StandardCurrencyEntity::getBatchId, current.getId())
.eq(StandardCurrencyEntity::getDeleted, false)

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

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

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

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

@ -0,0 +1,45 @@
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 ExperimentProgressResult {
@Schema(description = "总进度百分比", example = "75.00")
private Double totalProgress;
@Schema(description = "已完成百分比", example = "75.00")
private Double completedRate;
@Schema(description = "进行中百分比", example = "15.00")
private Double inProgressRate;
@Schema(description = "未开始百分比", example = "10.00")
private Double notStartedRate;
@Schema(description = "模块完成情况列表")
private List<ModuleProgressItem> modules;
@Data
@AllArgsConstructor
@Schema(description = "单个模块进度")
public static class ModuleProgressItem {
@Schema(description = "模块编码", example = "1")
private String moduleCode;
@Schema(description = "模块名称", example = "中国人民银行生成数字货币")
private String moduleName;
@Schema(description = "模块状态:COMPLETED-已完成, IN_PROGRESS-进行中, NOT_STARTED-未开始", example = "COMPLETED")
private String status;
@Schema(description = "完成进度百分比", example = "100.00")
private Double progress;
}
}

@ -0,0 +1,29 @@
package com.yau.digitalrmb.progress.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
@AllArgsConstructor
@Schema(description = "实验记录分页结果")
public class ExperimentRecordPageResult {
@Schema(description = "当前页码", example = "1")
private Long current;
@Schema(description = "每页数量", example = "10")
private Long size;
@Schema(description = "总记录数", example = "128")
private Long total;
@Schema(description = "总页数", example = "13")
private Long pages;
@Schema(description = "当前页记录列表")
private List<ExperimentRecordResult> records;
}

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

@ -0,0 +1,34 @@
package com.yau.digitalrmb.walletopening.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "央行开通确认报文输出结果")
public class ActivateConfirmOutputResult {
@Schema(description = "确认报文ID", example = "ACTIVATE_CONFIRM_20260801_001")
private String confirmId;
@Schema(description = "钱包ID", example = "WALLET_A1B2C3D4E5F6...")
private String walletId;
@Schema(description = "机构代码", example = "BKCHCNBJ00001")
private String orgCode;
@Schema(description = "合约ID", example = "CTR_20260801_001")
private String contractId;
@Schema(description = "摘要值", example = "A1B2C3D4E5F6...")
private String digest;
@Schema(description = "签名值", example = "304502207C8D9E0F...")
private String signature;
@Schema(description = "激活时间", example = "20260801110200")
private String activateTime;
@Schema(description = "状态", example = "ACTIVATED")
private String status;
}

@ -0,0 +1,22 @@
package com.yau.digitalrmb.walletopening.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "读取开通确认时间戳结果")
public class ActivateTimestampResult {
@Schema(description = "时间戳", example = "20260801110200")
private String timestamp;
@Schema(description = "钱包标识ID", example = "WALLET_A1B2C3D4E5F6...")
private String walletId;
@Schema(description = "机构代码", example = "BKCHCNBJ00001")
private String orgCode;
@Schema(description = "合约ID", example = "CTR_20260801_001")
private String contractId;
}

@ -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,

@ -0,0 +1,22 @@
package com.yau.digitalrmb.walletopening.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "智能合约限额")
public class ContractLimits {
@Schema(description = "单笔支付限额", example = "50000")
private String singlePayment;
@Schema(description = "日累计支付限额", example = "200000")
private String dailyPayment;
@Schema(description = "年累计支付限额", example = "5000000")
private String annualPayment;
@Schema(description = "余额上限", example = "500000")
private String balanceCeiling;
}

@ -0,0 +1,34 @@
package com.yau.digitalrmb.walletopening.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "智能合约输出结果")
public class ContractOutputResult {
@Schema(description = "合约ID", example = "CTR_20260801_001")
private String contractId;
@Schema(description = "钱包ID", example = "WALLET_A1B2C3D4E5F6...")
private String walletId;
@Schema(description = "钱包类型", example = "个人钱包")
private String walletType;
@Schema(description = "支付限额")
private ContractLimits limits;
@Schema(description = "有效期", example = "20261231")
private String validity;
@Schema(description = "合约哈希", example = "A1B2C3D4E5F6...")
private String contractHash;
@Schema(description = "状态", example = "已生效")
private String status;
@Schema(description = "生效时间", example = "20260801110200")
private String effectiveTime;
}

@ -0,0 +1,25 @@
package com.yau.digitalrmb.walletopening.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "最终确权通知输出结果")
public class FinalConfirmOutputResult {
@Schema(description = "最终确权通知ID", example = "FINAL_CONFIRM_20260801_001")
private String finalConfirmId;
@Schema(description = "钱包ID", example = "WALLET_A1B2C3D4E5F6...")
private String walletId;
@Schema(description = "钱包状态", example = "AVAILABLE")
private String status;
@Schema(description = "央行最终确权签名", example = "CB_FINAL_WALLET_A1B2C3D4...")
private String cbFinalSignature;
@Schema(description = "最终时间", example = "20260801110200")
private String finalTime;
}

@ -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) {

@ -1,78 +0,0 @@
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.CentralWalletActivationPageResult;
import com.yau.digitalrmb.walletopening.application.CentralWalletActivationService;
import com.yau.digitalrmb.walletopening.interfaces.rest.dto.CentralActivationVerifySignRequest;
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;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/central-banks/wallet-opening/step4")
@Tag(name = "数字货币钱包-央行端 - 步骤四:接收开通确认并激活钱包",
description = "央行端接收商业银行开通确认报文→SM2验签(商业银行第二公钥)→SM3重算摘要H2→摘要匹配(H1=H2)→激活钱包")
public class CentralBankWalletActivationController {
@Resource
private CentralWalletActivationService activationService;
@GetMapping("/page")
@Operation(summary = "查询央行端步骤四页面数据", description = "返回接收状态、确认报文、验签结果、摘要校验、激活状态等")
public ApiResponse<CentralWalletActivationPageResult> getPage() {
return ApiResponse.success(activationService.getPage(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/receive")
@Operation(summary = "接收开通确认报文", description = "接收商业银行发送的开通确认报文(ACTIVATE_CONFIRM),前置条件:商业银行已发送确认报文")
public ApiResponse<CentralWalletActivationPageResult> receive() {
return ApiResponse.success(activationService.receive(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/verify-sign")
@Operation(summary = "SM2验证签名", description = "选择商业银行第二公钥进行SM2_Verify(第二公钥, 摘要值, 签名值),公钥错误则错误数+1")
public ApiResponse<CentralWalletActivationPageResult> verifySign(@Valid @RequestBody CentralActivationVerifySignRequest request) {
return ApiResponse.success(activationService.verifySignature(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPublicKey(), request.getAlgorithm()), traceId());
}
@PostMapping("/recalculate-digest")
@Operation(summary = "SM3计算新摘要H2", description = "根据报文字段按 ACTIVATE|钱包标识|机构代码|合约ID|时间戳 格式拼接原文,SM3重算得到H2")
public ApiResponse<CentralWalletActivationPageResult> recalculateDigest(@RequestParam String algorithm) {
return ApiResponse.success(activationService.recalculateDigest(
InstitutionKeySubject.from(AuthContextHolder.get()), algorithm), traceId());
}
@PostMapping("/match-digest")
@Operation(summary = "摘要匹配校验", description = "将H2与报文中的H1(digest)比对,不匹配则错误数+1")
public ApiResponse<CentralWalletActivationPageResult> matchDigest() {
return ApiResponse.success(activationService.matchDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/activate")
@Operation(summary = "激活钱包", description = "校验通过后激活数字货币钱包,标记开通完成")
public ApiResponse<CentralWalletActivationPageResult> activateWallet() {
return ApiResponse.success(activationService.activateWallet(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -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<CentralWalletActivationPageResult> getPage() {
return ApiResponse.success(activationService.getPage(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/verify-sign")
@Operation(summary = "SM2验证签名",
description = "选择商业银行第二公钥进行SM2_Verify(第二公钥, 摘要值, 签名值),公钥错误则错误数+1")
public ApiResponse<CentralWalletActivationPageResult> verifySign(
@Valid @RequestBody CentralActivationVerifySignRequest request) {
description = "选择商业银行第二公钥进行SM2验签,返回通过/未通过,失败则错误数+1并说明原因")
public ApiResponse<String> 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<CentralWalletActivationPageResult> finalSign(
@Valid @RequestBody FinalSignRequest request) {
description = "传算法、摘要值、央行第一私钥,校验通过后返回确权签名值")
public ApiResponse<String> 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<CentralWalletActivationPageResult> updateStatus() {
@Operation(summary = "更新钱包登记状态", description = "将钱包登记状态更新为AVAILABLE,返回成功/失败")
public ApiResponse<String> 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<CentralWalletActivationPageResult> outputConfirm() {
description = "生成最终确权通知,返回finalConfirmId、walletId、status、cbFinalSignature、finalTime")
public ApiResponse<FinalConfirmOutputResult> outputConfirm() {
return ApiResponse.success(activationService.outputFinalConfirm(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/send")
@Operation(summary = "发送最终确权通知", description = "将最终确权通知发送给用户端")
public ApiResponse<CentralWalletActivationPageResult> send() {
public ApiResponse<String> send() {
return ApiResponse.success(activationService.sendFinal(
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.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<SmartContractPageResult> computeContractDigest(
description = "前端传入钱包标识ID和合约参数,校验后返回SM3合约摘要值")
public ApiResponse<String> 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<SmartContractPageResult> outputContract() {
@Operation(summary = "输出智能合约信息JSON",
description = "返回contractId、walletId、walletType、limits、validity、contractHash、status、effectiveTime")
public ApiResponse<ContractOutputResult> outputContract() {
return ApiResponse.success(smartContractService.outputContract(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/read-activate-timestamp")
@Operation(summary = "读取当前时间戳(确认阶段)", description = "生成开通确认所需的当前时间戳,同时带出机构代码")
public ApiResponse<SmartContractPageResult> readActivateTimestamp() {
@Operation(summary = "读取当前时间戳(确认阶段)",
description = "返回时间戳、钱包标识ID、机构代码、合约ID")
public ApiResponse<ActivateTimestampResult> readActivateTimestamp() {
return ApiResponse.success(smartContractService.acquireActivateTimestamp(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/activate-digest")
@Operation(summary = "SM3生成确认摘要", description = "前端传入确认原文,后端直接SM3计算摘要")
public ApiResponse<SmartContractPageResult> computeActivateDigest(
@Operation(summary = "SM3生成确认摘要", description = "前端传入确认原文,返回SM3摘要值")
public ApiResponse<String> 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<SmartContractPageResult> signActivate(@Valid @RequestBody SmartContractSignRequest request) {
@Operation(summary = "SM2签名确认报文", description = "使用商业银行第二私钥对摘要签名,返回签名值")
public ApiResponse<String> 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<SmartContractPageResult> outputConfirm() {
@Operation(summary = "输出央行开通确认报文JSON",
description = "返回confirmId、walletId、orgCode、contractId、digest、signature、activateTime、status")
public ApiResponse<ActivateConfirmOutputResult> outputConfirm() {
return ApiResponse.success(smartContractService.outputConfirm(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/send")
@Operation(summary = "发送开通确认报文", description = "将开通确认报文发送出去,标记步骤四完成")
public ApiResponse<SmartContractPageResult> send() {
public ApiResponse<String> send() {
return ApiResponse.success(smartContractService.send(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}

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

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

@ -1,119 +1,119 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationCreateRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DonationServiceTest {
private static final InstitutionKeySubject SUBJECT = new InstitutionKeySubject("u-1", 1L, 2L);
@Mock private DonationRecordMapper mapper;
@Mock private CorporateWalletApplicationRepository corporateWalletRepository;
@Mock private RemoteWalletApplicationRepository remoteWalletRepository;
@Mock private CorporateWalletKeyService keyService;
@Mock private InstitutionIdentityCryptography cryptography;
@Mock private SalaryScoreService scoreService;
@Spy private ObjectMapper objectMapper = new ObjectMapper();
@InjectMocks private DonationService service;
private final AtomicReference<DonationRecordEntity> latest = new AtomicReference<>();
@BeforeAll
static void initializeMybatisLambdaMetadata() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
DonationRecordEntity.class);
}
@BeforeEach
void setUp() {
CorporateWalletApplication donor = CorporateWalletApplication.create("u-1", 1L, 2L);
donor.fillForm("创新科技公司", "CREDIT-1", "张三", "10000000.00", "技术服务", "20260916090000");
donor.activateWallet("CORP_DONOR", "20260916091000");
RemoteWalletApplication recipient = RemoteWalletApplication.create("u-1", 1L, 2L);
recipient.fillForm("阳光公益基金", "CREDIT-2", "李四", "13900139000", "20260916092000");
recipient.activateWallet("CORP_RECIPIENT", "20260916093000");
when(corporateWalletRepository.findLatest("u-1", 1L, 2L)).thenReturn(Optional.of(donor));
when(remoteWalletRepository.findLatest("u-1", 1L, 2L)).thenReturn(Optional.of(recipient));
when(mapper.selectOne(any())).thenAnswer(invocation -> latest.get());
when(mapper.insert(any(DonationRecordEntity.class))).thenAnswer(invocation -> {
DonationRecordEntity record = invocation.getArgument(0);
record.setId(100L);
latest.set(record);
return 1;
});
when(mapper.updateById(any(DonationRecordEntity.class))).thenAnswer(invocation -> {
latest.set(invocation.getArgument(0));
return 1;
});
when(keyService.getOrCreatePrivateKey(any(), anyString(), anyString())).thenAnswer(invocation ->
"监管机构私钥".equals(invocation.getArgument(1)) ? "REGULATOR_KEY" : "DONOR_KEY");
when(cryptography.sm3(anyString())).thenAnswer(invocation -> "HASH_" + invocation.getArgument(0));
when(cryptography.sign(anyString(), anyString())).thenAnswer(invocation ->
"SIGN_" + invocation.getArgument(1));
}
@Test
void completesPrototypeDonationPublicationAndSupervisionFlow() {
DonationCreateRequest request = new DonationCreateRequest();
request.setDonorName("创新科技公司");
request.setDonorWalletId("CORP_DONOR");
request.setRecipientName("阳光公益基金");
request.setRecipientWalletId("CORP_RECIPIENT");
request.setAmount(new BigDecimal("100000.00"));
request.setProjectName("乡村教育支持计划");
request.setMemo("乡村教育支持计划捐款");
request.setTimestamp("20260916100000");
service.concatenate(SUBJECT, "张三", request);
service.digest(SUBJECT);
service.signDonation(SUBJECT, "DONOR_KEY");
service.outputDonation(SUBJECT);
service.receiptDetails(SUBJECT);
service.extractPublication(SUBJECT);
service.digestPublication(SUBJECT);
service.publish(SUBJECT);
service.outputPublication(SUBJECT);
service.extractSupervision(SUBJECT);
service.digestSupervision(SUBJECT);
service.signSupervision(SUBJECT, "REGULATOR_KEY");
DonationPageResult result = service.outputSupervision(SUBJECT);
assertThat(result.getStatus()).isEqualTo("ARCHIVED");
assertThat(result.getDonationOutputJson()).contains("DONATE_20260916_001", "已提交");
assertThat(result.getPublicationOutputJson()).contains("PUBLISHED", "阳光公益基金");
assertThat(result.getSupervisionOutputJson()).contains("APPROVED", "已备案");
verify(scoreService).markStepCompleted("u-1", 6);
verify(scoreService).markStepCompleted("u-1", 7);
verify(scoreService).markStepCompleted("u-1", 8);
}
}
//package com.yau.digitalrmb.corporatewallet.application;
//
//import com.baomidou.mybatisplus.core.MybatisConfiguration;
//import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
//import com.fasterxml.jackson.databind.ObjectMapper;
//import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication;
//import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
//import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
//import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
//import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordEntity;
//import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper;
//import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationCreateRequest;
//import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
//import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
//import org.apache.ibatis.builder.MapperBuilderAssistant;
//import org.junit.jupiter.api.BeforeAll;
//import org.junit.jupiter.api.BeforeEach;
//import org.junit.jupiter.api.Test;
//import org.junit.jupiter.api.extension.ExtendWith;
//import org.mockito.InjectMocks;
//import org.mockito.Mock;
//import org.mockito.Spy;
//import org.mockito.junit.jupiter.MockitoExtension;
//
//import java.math.BigDecimal;
//import java.util.Optional;
//import java.util.concurrent.atomic.AtomicReference;
//
//import static org.assertj.core.api.Assertions.assertThat;
//import static org.mockito.ArgumentMatchers.any;
//import static org.mockito.ArgumentMatchers.anyString;
//import static org.mockito.Mockito.verify;
//import static org.mockito.Mockito.when;
//
//@ExtendWith(MockitoExtension.class)
//class DonationServiceTest {
// private static final InstitutionKeySubject SUBJECT = new InstitutionKeySubject("u-1", 1L, 2L);
//
// @Mock private DonationRecordMapper mapper;
// @Mock private CorporateWalletApplicationRepository corporateWalletRepository;
// @Mock private RemoteWalletApplicationRepository remoteWalletRepository;
// @Mock private CorporateWalletKeyService keyService;
// @Mock private InstitutionIdentityCryptography cryptography;
// @Mock private SalaryScoreService scoreService;
// @Spy private ObjectMapper objectMapper = new ObjectMapper();
// @InjectMocks private DonationService service;
//
// private final AtomicReference<DonationRecordEntity> latest = new AtomicReference<>();
//
// @BeforeAll
// static void initializeMybatisLambdaMetadata() {
// TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
// DonationRecordEntity.class);
// }
//
// @BeforeEach
// void setUp() {
// CorporateWalletApplication donor = CorporateWalletApplication.create("u-1", 1L, 2L);
// donor.fillForm("创新科技公司", "CREDIT-1", "张三", "10000000.00", "技术服务", "20260916090000");
// donor.activateWallet("CORP_DONOR", "20260916091000");
// RemoteWalletApplication recipient = RemoteWalletApplication.create("u-1", 1L, 2L);
// recipient.fillForm("阳光公益基金", "CREDIT-2", "李四", "13900139000", "20260916092000");
// recipient.activateWallet("CORP_RECIPIENT", "20260916093000");
//
// when(corporateWalletRepository.findLatest("u-1", 1L, 2L)).thenReturn(Optional.of(donor));
// when(remoteWalletRepository.findLatest("u-1", 1L, 2L)).thenReturn(Optional.of(recipient));
// when(mapper.selectOne(any())).thenAnswer(invocation -> latest.get());
// when(mapper.insert(any(DonationRecordEntity.class))).thenAnswer(invocation -> {
// DonationRecordEntity record = invocation.getArgument(0);
// record.setId(100L);
// latest.set(record);
// return 1;
// });
// when(mapper.updateById(any(DonationRecordEntity.class))).thenAnswer(invocation -> {
// latest.set(invocation.getArgument(0));
// return 1;
// });
// when(keyService.getOrCreatePrivateKey(any(), anyString(), anyString())).thenAnswer(invocation ->
// "监管机构私钥".equals(invocation.getArgument(1)) ? "REGULATOR_KEY" : "DONOR_KEY");
// when(cryptography.sm3(anyString())).thenAnswer(invocation -> "HASH_" + invocation.getArgument(0));
// when(cryptography.sign(anyString(), anyString())).thenAnswer(invocation ->
// "SIGN_" + invocation.getArgument(1));
// }
//
// @Test
// void completesPrototypeDonationPublicationAndSupervisionFlow() {
// DonationCreateRequest request = new DonationCreateRequest();
// request.setDonorName("创新科技公司");
// request.setDonorWalletId("CORP_DONOR");
// request.setRecipientName("阳光公益基金");
// request.setRecipientWalletId("CORP_RECIPIENT");
// request.setAmount(new BigDecimal("100000.00"));
// request.setProjectName("乡村教育支持计划");
// request.setMemo("乡村教育支持计划捐款");
// request.setTimestamp("20260916100000");
//
// service.concatenate(SUBJECT, "张三", request);
// service.digest(SUBJECT);
// service.signDonation(SUBJECT, "DONOR_KEY");
// service.outputDonation(SUBJECT);
// service.receiptDetails(SUBJECT);
// service.extractPublication(SUBJECT);
// service.digestPublication(SUBJECT);
// service.publish(SUBJECT);
// service.outputPublication(SUBJECT);
// service.extractSupervision(SUBJECT);
// service.digestSupervision(SUBJECT);
// service.signSupervision(SUBJECT, "REGULATOR_KEY");
// DonationPageResult result = service.outputSupervision(SUBJECT);
//
// assertThat(result.getStatus()).isEqualTo("ARCHIVED");
// assertThat(result.getDonationOutputJson()).contains("DONATE_20260916_001", "已提交");
// assertThat(result.getPublicationOutputJson()).contains("PUBLISHED", "阳光公益基金");
// assertThat(result.getSupervisionOutputJson()).contains("APPROVED", "已备案");
// verify(scoreService).markStepCompleted("u-1", 6);
// verify(scoreService).markStepCompleted("u-1", 7);
// verify(scoreService).markStepCompleted("u-1", 8);
// }
//}

Loading…
Cancel
Save