diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateWalletKeyService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateWalletKeyService.java index 7f29d5c..59b85ab 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateWalletKeyService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateWalletKeyService.java @@ -142,6 +142,44 @@ public class CorporateWalletKeyService { keyInfoMapper.insert(entity); } + /** + * 按需获取私钥:若数据库中已存在则直接返回,否则自动生成SM2密钥对并存入数据库。 + * 适用于:企业法人私钥、数币操作员私钥、数币复核员私钥、监管机构私钥等。 + */ + public String getOrCreatePrivateKey(InstitutionKeySubject subject, String keyType, String holder) { + CorporateWalletKeyInfoEntity existing = find(subject, keyType); + if (existing != null) { + return existing.getKeyValue(); + } + InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair(); + saveKeyInfo(subject, keyType, holder, pair.getPrivateKey(), true, "NEW", holder); + saveKeyInfo(subject, keyType.replace("私钥", "公钥"), holder, pair.getPublicKey(), false, "NEW", holder); + return pair.getPrivateKey(); + } + + /** + * 按需获取公钥:若数据库中已存在则直接返回,否则基于已有私钥生成公钥并存入。 + */ + public String getOrCreatePublicKey(InstitutionKeySubject subject, String keyType, String holder) { + CorporateWalletKeyInfoEntity existing = find(subject, keyType); + if (existing != null) { + return existing.getKeyValue(); + } + // 尝试从对应私钥推导 + String privateKeyType = keyType.replace("公钥", "私钥"); + CorporateWalletKeyInfoEntity privateEntity = find(subject, privateKeyType); + if (privateEntity != null) { + String publicKey = cryptography.derivePublicKey(privateEntity.getKeyValue()); + saveKeyInfo(subject, keyType, holder, publicKey, false, "NEW", holder); + return publicKey; + } + // 私钥也不存在,生成新密钥对 + InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair(); + saveKeyInfo(subject, privateKeyType, holder, pair.getPrivateKey(), true, "NEW", holder); + saveKeyInfo(subject, keyType, holder, pair.getPublicKey(), false, "NEW", holder); + return pair.getPublicKey(); + } + private CorporateWalletKeyResult buildResult(String keyType, String holder, String keyValue, boolean isPrivateKey, String source) { return new CorporateWalletKeyResult(keyType, holder, "SM2", keyValue, isPrivateKey, source); diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateWalletService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateWalletService.java index 6827156..0f4a096 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateWalletService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateWalletService.java @@ -170,6 +170,9 @@ public class CorporateWalletService { subject.getSchoolId(), subject.getClassId()) .orElse(null); if (remoteActivated != null) { + // 【企业信息一致性校验】比对临柜开立和远程开立的企业信息是否一致 + validateEnterpriseInfoConsistency(app, remoteActivated); + // 复用远程开立的钱包ID,不再生成新钱包 app.activateWallet(remoteActivated.getWalletId(), remoteActivated.getActivateTime()); repository.update(app); @@ -178,7 +181,8 @@ public class CorporateWalletService { app.getWalletType(), "ACTIVATED", remoteActivated.getActivateTime(), "远程开立"); } - String walletId = "CORP_" + app.getDigestValue(); + // 钱包ID基于统一社会信用代码生成,确保同一企业无论通过哪个渠道开立,钱包ID都一致 + String walletId = "CORP_" + cryptography.sm3(app.getCreditCode()); String activateTime = app.getVerifyTime() != null ? app.getVerifyTime() : LocalDateTime.now().format(TIMESTAMP_FORMAT); @@ -217,4 +221,38 @@ public class CorporateWalletService { + "\"applyTime\":\"" + applyTime + "\"," + "\"status\":\"已提交\"}"; } + + /** + * 校验临柜开立和远程开立的企业信息一致性 + * @param corpApp 临柜开立申请 + * @param remoteApp 远程开立申请 + */ + private void validateEnterpriseInfoConsistency(CorporateWalletApplication corpApp, + RemoteWalletApplication remoteApp) { + java.util.List errors = new java.util.ArrayList<>(); + + // 1. 校验统一社会信用代码(最关键) + if (!corpApp.getCreditCode().equals(remoteApp.getCreditCode())) { + errors.add("统一社会信用代码不一致:临柜开立为【" + corpApp.getCreditCode() + "】," + + "远程开立为【" + remoteApp.getCreditCode() + "】"); + } + + // 2. 校验企业名称 + if (!corpApp.getCorpName().equals(remoteApp.getCorpName())) { + errors.add("企业名称不一致:临柜开立为【" + corpApp.getCorpName() + "】," + + "远程开立为【" + remoteApp.getCorpName() + "】"); + } + + // 3. 校验法定代表人 + if (!corpApp.getLegalPerson().equals(remoteApp.getLegalPerson())) { + errors.add("法定代表人不一致:临柜开立为【" + corpApp.getLegalPerson() + "】," + + "远程开立为【" + remoteApp.getLegalPerson() + "】"); + } + + // 4. 如果有错误,抛出异常 + if (!errors.isEmpty()) { + String errorMsg = "企业信息不一致,无法提交:\n" + String.join("\n", errors); + throw new BusinessException(ErrorCode.VALIDATION_ERROR, errorMsg); + } + } } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/RemoteSignatureResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/RemoteSignatureResult.java index 5a77f55..8703f67 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/RemoteSignatureResult.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/RemoteSignatureResult.java @@ -6,15 +6,12 @@ import lombok.Getter; @Getter @Schema(description = "远程开立-SM2签名结果") public class RemoteSignatureResult { - @Schema(description = "签名私钥(十六进制)") - private final String privateKey; @Schema(description = "签名算法", example = "SM2") private final String algorithm; @Schema(description = "签名值(十六进制)") private final String signature; - public RemoteSignatureResult(String privateKey, String algorithm, String signature) { - this.privateKey = privateKey; + public RemoteSignatureResult(String algorithm, String signature) { this.algorithm = algorithm; this.signature = signature; } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/RemoteWalletService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/RemoteWalletService.java index 34b034e..c7f5887 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/RemoteWalletService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/RemoteWalletService.java @@ -1,13 +1,17 @@ package com.yau.digitalrmb.corporatewallet.application; +import com.yau.digitalrmb.corporatewallet.application.CorporateWalletKeyService; 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.CorporateWalletKeyInfoEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletKeyInfoMapper; import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteWalletApplicationRequest; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; -import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair; import com.yau.digitalrmb.institutionidentity.infrastructure.SmInstitutionIdentityCryptography; +import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.exception.BusinessException; import org.springframework.stereotype.Service; @@ -28,6 +32,16 @@ public class RemoteWalletService { private CorporateWalletApplicationRepository corporateRepository; @Resource private SmInstitutionIdentityCryptography cryptography; + @Resource + private CorporateWalletKeyInfoMapper keyInfoMapper; + @Resource + private SalaryScoreService scoreService; + @Resource + private CorporateWalletKeyService keyService; + @Resource + private InstitutionKeyService institutionKeyService; + + private static final String LEGAL_PERSON_KEY_TYPE = "企业法人私钥"; // ==================== 步骤一:企业法人在线填写信息 ==================== @@ -122,23 +136,81 @@ public class RemoteWalletService { } @Transactional - public RemoteSignatureResult computeSignature(InstitutionKeySubject subject) { + public RemoteSignatureResult computeSignature(InstitutionKeySubject subject, String providedPrivateKey) { RemoteWalletApplication app = findRequired(subject); if (app.getSigningDigestValue() == null) { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM3摘要运算"); } if (app.getSignature() != null) { - return new RemoteSignatureResult(app.getSignPrivateKey(), "SM2", app.getSignature()); + return new RemoteSignatureResult("SM2", app.getSignature()); } - InstitutionSm2KeyPair keyPair = cryptography.generateSm2KeyPair(); - String privateKey = keyPair.getPrivateKey(); - String signature = cryptography.sign(privateKey, app.getSigningExtractMessage()); + // 验证前端传入的私钥不能为空 + if (providedPrivateKey == null || providedPrivateKey.trim().isEmpty()) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "企业法人私钥不能为空"); + } + String trimmedPrivateKey = providedPrivateKey.trim(); + + // 检查数据库中是否已存在企业法人私钥 + String legalPersonName = app.getLegalPerson() != null ? app.getLegalPerson() : "企业法人"; + CorporateWalletKeyInfoEntity existingKey = keyInfoMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(CorporateWalletKeyInfoEntity::getUserId, subject.getUserId()) + .eq(CorporateWalletKeyInfoEntity::getSchoolId, subject.getSchoolId()) + .eq(CorporateWalletKeyInfoEntity::getClassId, subject.getClassId()) + .eq(CorporateWalletKeyInfoEntity::getKeyType, LEGAL_PERSON_KEY_TYPE) + .eq(CorporateWalletKeyInfoEntity::getIsPrivateKey, true) + .eq(CorporateWalletKeyInfoEntity::getDeleted, false) + .orderByDesc(CorporateWalletKeyInfoEntity::getCreatedAt) + .last("LIMIT 1")); + + if (existingKey != null) { + // 【场景1】数据库中已有密钥,验证前端传入的私钥是否正确 + if (!existingKey.getKeyValue().equalsIgnoreCase(trimmedPrivateKey)) { + scoreService.recordError(subject.getUserId()); + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "企业法人私钥不正确,错误次数+1"); + } + // 使用数据库中已有的私钥进行签名 + String signature = cryptography.sign(existingKey.getKeyValue(), app.getSigningExtractMessage()); + app.setSignPrivateKey(existingKey.getKeyValue()); + app.setSignature(signature); + repository.update(app); + return new RemoteSignatureResult("SM2", signature); + } else { + // 【场景2】数据库中没有密钥,说明用户没有先调用 /keys 接口获取密钥 + // 这种情况视为错误,记录错误次数并提示用户 + scoreService.recordError(subject.getUserId()); + this.getAllKeys( + InstitutionKeySubject.from(AuthContextHolder.get()), AuthContextHolder.get().getUsername()); + + throw new BusinessException(ErrorCode.VALIDATION_ERROR, + "企业法人私钥有误,错误次数+1"); + } - app.setSignPrivateKey(privateKey); - app.setSignature(signature); - repository.update(app); - return new RemoteSignatureResult(privateKey, "SM2", signature); + } + + /** + * 保存密钥信息到 corporate_wallet_key_info 表 + */ + private void saveKeyInfo(InstitutionKeySubject subject, String keyType, String holder, + String keyValue, boolean isPrivateKey, String source) { + CorporateWalletKeyInfoEntity entity = new CorporateWalletKeyInfoEntity(); + entity.setUserId(subject.getUserId()); + entity.setSchoolId(subject.getSchoolId()); + entity.setClassId(subject.getClassId()); + entity.setKeyType(keyType); + entity.setHolder(holder); + entity.setKeyAlgorithm("SM2"); + entity.setKeyValue(keyValue); + entity.setIsPrivateKey(isPrivateKey); + entity.setSource(source); + java.time.LocalDateTime now = java.time.LocalDateTime.now(); + entity.setCreatedAt(now); + entity.setUpdatedAt(now); + entity.setCreatedBy(holder); + entity.setUpdatedBy(holder); + entity.setDeleted(false); + keyInfoMapper.insert(entity); } @Transactional @@ -221,6 +293,9 @@ public class RemoteWalletService { subject.getSchoolId(), subject.getClassId()) .orElse(null); if (corpActivated != null) { + // 【企业信息一致性校验】比对远程开立和临柜开立的企业信息是否一致 + validateEnterpriseInfoConsistency(app, corpActivated); + // 复用临柜开立的钱包ID,不再生成新钱包 app.activateWallet(corpActivated.getWalletId(), corpActivated.getActivateTime()); repository.update(app); @@ -229,7 +304,8 @@ public class RemoteWalletService { app.getWalletType(), "ACTIVATED", corpActivated.getActivateTime(), "已验证", "临柜开立"); } - String walletId = "CORP_" + app.getWalletDigestValue(); + // 钱包ID基于统一社会信用代码生成,确保同一企业无论通过哪个渠道开立,钱包ID都一致 + String walletId = "CORP_" + cryptography.sm3(app.getCreditCode()); String activateTime = app.getSignTime() != null ? app.getSignTime() : LocalDateTime.now().format(TIMESTAMP_FORMAT); @@ -260,4 +336,108 @@ public class RemoteWalletService { return "REMOTE_OPEN|" + corpName + "|" + creditCode + "|" + legalPerson + "|" + phone + "|" + applyTime; } + + /** + * 获取远程开立所需的全部密钥信息: + * 央行第一密钥、商业银行第二密钥复用已有数据; + * 企业法人、数币操作员、数币复核员、监管机构密钥按需生成(若不存在则新增,已存在则复用)。 + */ + @Transactional + public java.util.List getAllKeys(InstitutionKeySubject subject, String operator) { + java.util.List results = new java.util.ArrayList<>(); + + // 1. 央行第一密钥(复用 institution_sm2_key) + com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResult centralKey = + institutionKeyService.centralBankKey(subject, operator); + results.add(buildKeyResult("央行第一私钥", "中国人民银行", + centralKey.getPrivateKey(), true, "REUSED")); + results.add(buildKeyResult("央行第一公钥", "中国人民银行", + centralKey.getPublicKey(), false, "REUSED")); + + // 2. 商业银行第二密钥(复用 institution_sm2_key) + com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResult bankKey = + institutionKeyService.commercialBankKey(subject, operator); + results.add(buildKeyResult("商业银行第二私钥", "中国银行", + bankKey.getPrivateKey(), true, "REUSED")); + results.add(buildKeyResult("商业银行第二公钥", "中国银行", + bankKey.getPublicKey(), false, "REUSED")); + + // 3. 企业法人密钥(按需生成) + RemoteWalletApplication app = findOrCreate(subject); + String legalPersonName = app.getLegalPerson() != null ? app.getLegalPerson() : "企业法人"; + String[] legalPersonPair = getOrCreateKeyPair(subject, "企业法人私钥", "企业法人公钥", + legalPersonName); + results.add(buildKeyResult("企业法人私钥", legalPersonName, legalPersonPair[0], true, "NEW")); + results.add(buildKeyResult("企业法人公钥", legalPersonName, legalPersonPair[1], false, "NEW")); + + // 4. 数币操作员密钥(按需生成) + String[] operatorPair = getOrCreateKeyPair(subject, "数币操作员私钥", "数币操作员公钥", + "王五"); + results.add(buildKeyResult("数币操作员私钥", "王五", operatorPair[0], true, "NEW")); + results.add(buildKeyResult("数币操作员公钥", "王五", operatorPair[1], false, "NEW")); + + // 5. 数币复核员密钥(按需生成) + String[] reviewerPair = getOrCreateKeyPair(subject, "数币复核员私钥", "数币复核员公钥", + "赵六"); + results.add(buildKeyResult("数币复核员私钥", "赵六", reviewerPair[0], true, "NEW")); + results.add(buildKeyResult("数币复核员公钥", "赵六", reviewerPair[1], false, "NEW")); + + // 6. 监管机构密钥(按需生成) + String[] regulatorPair = getOrCreateKeyPair(subject, "监管机构私钥", "监管机构公钥", + "民政部公益监管中心"); + results.add(buildKeyResult("监管机构私钥", "民政部公益监管中心", regulatorPair[0], true, "NEW")); + results.add(buildKeyResult("监管机构公钥", "民政部公益监管中心", regulatorPair[1], false, "NEW")); + + return results; + } + + /** + * 按需获取密钥对:若数据库中已存在则直接返回,否则自动生成SM2密钥对并存入数据库。 + * 返回 [privateKey, publicKey] + */ + private String[] getOrCreateKeyPair(InstitutionKeySubject subject, String privateKeyType, + String publicKeyType, String holder) { + String privateKey = keyService.getOrCreatePrivateKey(subject, privateKeyType, holder); + String publicKey = keyService.getOrCreatePublicKey(subject, publicKeyType, holder); + return new String[]{privateKey, publicKey}; + } + + private RemoteWalletKeyResult buildKeyResult(String keyType, String holder, String keyValue, + boolean isPrivateKey, String source) { + return new RemoteWalletKeyResult(keyType, holder, "SM2", keyValue, isPrivateKey, source); + } + + /** + * 校验远程开立和临柜开立的企业信息一致性 + * @param remoteApp 远程开立申请 + * @param corpApp 临柜开立申请 + */ + private void validateEnterpriseInfoConsistency(RemoteWalletApplication remoteApp, + CorporateWalletApplication corpApp) { + java.util.List errors = new java.util.ArrayList<>(); + + // 1. 校验统一社会信用代码(最关键) + if (!remoteApp.getCreditCode().equals(corpApp.getCreditCode())) { + errors.add("统一社会信用代码不一致:远程开立为【" + remoteApp.getCreditCode() + "】," + + "临柜开立为【" + corpApp.getCreditCode() + "】"); + } + + // 2. 校验企业名称 + if (!remoteApp.getCorpName().equals(corpApp.getCorpName())) { + errors.add("企业名称不一致:远程开立为【" + remoteApp.getCorpName() + "】," + + "临柜开立为【" + corpApp.getCorpName() + "】"); + } + + // 3. 校验法定代表人 + if (!remoteApp.getLegalPerson().equals(corpApp.getLegalPerson())) { + errors.add("法定代表人不一致:远程开立为【" + remoteApp.getLegalPerson() + "】," + + "临柜开立为【" + corpApp.getLegalPerson() + "】"); + } + + // 4. 如果有错误,抛出异常 + if (!errors.isEmpty()) { + String errorMsg = "企业信息不一致,无法提交:\n" + String.join("\n", errors); + throw new BusinessException(ErrorCode.VALIDATION_ERROR, errorMsg); + } + } } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java index 4237f54..3bd15c2 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java @@ -66,6 +66,8 @@ public class SalaryBatchService { private SalaryScoreService scoreService; @Resource private InstitutionKeyService institutionKeyService; + @Resource + private CorporateWalletKeyService keyService; // ==================== 步骤一:调取区块信息 ==================== @@ -87,6 +89,9 @@ public class SalaryBatchService { batch.fetchBlockchain(batchId, corpWalletId, OPERATOR_NAME, timestamp); repository.update(batch); + // 标记步骤3完成:数币操作员导入工资明细 + scoreService.markStepCompleted(subject.getUserId(), 3); + return new SalaryBlockchainResult(corpWalletId, batchId, OPERATOR_NAME, timestamp); } @@ -103,6 +108,9 @@ public class SalaryBatchService { batch.concatenate(message); repository.update(batch); + // 继续步骤3:拼接原文(属于数币操作员流程的一部分) + // 不单独计分,合并到步骤3中 + return new SalaryConcatenateResult(message); } @@ -122,6 +130,9 @@ public class SalaryBatchService { batch.computeDigest("SM3", digest); repository.update(batch); + // 继续步骤3:生成摘要(属于数币操作员流程的一部分) + // 不单独计分,合并到步骤3中 + return new SalaryDigestResult(digest); } @@ -155,6 +166,9 @@ public class SalaryBatchService { batch.sign(signature); repository.update(batch); + // 继续步骤3:SM2签名(属于数币操作员流程的一部分) + // 不单独计分,合并到步骤3中 + return new SalarySignatureResult(signature); } @@ -174,6 +188,9 @@ public class SalaryBatchService { batch.output(json); repository.update(batch); + // 继续步骤3:输出(属于数币操作员流程的一部分) + // 不单独计分,合并到步骤3中 + return new SalaryOutputResult(json); } @@ -275,6 +292,9 @@ public class SalaryBatchService { batch.checkPermission(singleStatus, batchStatus, dailyStatus, reviewResult); repository.update(batch); + // 标记步骤4完成:数币复核员审核与权限审批 + scoreService.markStepCompleted(subject.getUserId(), 4); + return new SalaryReviewCheckResult( singleStatus, SINGLE_LIMIT.setScale(2).toPlainString(), maxSingleAmount.setScale(2).toPlainString(), singleDesc, batchStatus, BATCH_LIMIT.setScale(2).toPlainString(), batchTotal.setScale(2).toPlainString(), batchDesc, @@ -488,6 +508,9 @@ public class SalaryBatchService { batch.outputConfirm(json); repository.update(batch); + // 标记步骤5完成:交易确认与工资发放 + scoreService.markStepCompleted(subject.getUserId(), 5); + return new SalaryConfirmOutputResult(json); } @@ -527,35 +550,17 @@ public class SalaryBatchService { } /** - * 从 corporate_wallet_key_info 表获取数币操作员私钥 + * 从 corporate_wallet_key_info 表获取数币操作员私钥(若不存在则自动生成) */ private String findOperatorPrivateKey(InstitutionKeySubject subject) { - CorporateWalletKeyInfoEntity entity = keyInfoMapper.selectOne( - new LambdaQueryWrapper() - .eq(CorporateWalletKeyInfoEntity::getUserId, subject.getUserId()) - .eq(CorporateWalletKeyInfoEntity::getSchoolId, subject.getSchoolId()) - .eq(CorporateWalletKeyInfoEntity::getClassId, subject.getClassId()) - .eq(CorporateWalletKeyInfoEntity::getKeyType, OPERATOR_KEY_TYPE) - .eq(CorporateWalletKeyInfoEntity::getDeleted, false) - .orderByDesc(CorporateWalletKeyInfoEntity::getCreatedAt) - .last("LIMIT 1")); - return entity == null ? null : entity.getKeyValue(); + return keyService.getOrCreatePrivateKey(subject, OPERATOR_KEY_TYPE, OPERATOR_NAME); } /** - * 从 corporate_wallet_key_info 表获取数币复核员私钥 + * 从 corporate_wallet_key_info 表获取数币复核员私钥(若不存在则自动生成) */ private String findReviewerPrivateKey(InstitutionKeySubject subject) { - CorporateWalletKeyInfoEntity entity = keyInfoMapper.selectOne( - new LambdaQueryWrapper() - .eq(CorporateWalletKeyInfoEntity::getUserId, subject.getUserId()) - .eq(CorporateWalletKeyInfoEntity::getSchoolId, subject.getSchoolId()) - .eq(CorporateWalletKeyInfoEntity::getClassId, subject.getClassId()) - .eq(CorporateWalletKeyInfoEntity::getKeyType, REVIEWER_KEY_TYPE) - .eq(CorporateWalletKeyInfoEntity::getDeleted, false) - .orderByDesc(CorporateWalletKeyInfoEntity::getCreatedAt) - .last("LIMIT 1")); - return entity == null ? null : entity.getKeyValue(); + return keyService.getOrCreatePrivateKey(subject, REVIEWER_KEY_TYPE, REVIEWER_NAME); } /** diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java index 502c97e..83a897e 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java @@ -42,6 +42,40 @@ public class SalaryScoreService { return accumulatedErrorCount; } + /** + * 标记步骤完成,更新进度和成绩 + * @param userId 用户ID + * @param completedStep 完成的步骤编号(1-7) + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void markStepCompleted(String userId, int completedStep) { + if (userId == null || userId.trim().isEmpty()) { + return; + } + StuModuleScoreDetailsEntity detail = getOrCreate(userId); + int errorCount = errorCount(detail.getCompletionStatus()); + + // 模块六共7个步骤: + // 1-临柜开立, 2-远程开立, 3-数币操作员导入, 4-数币复核员审核, + // 5-交易确认与工资发放, 6-捐赠企业发起, 7-受赠机构接收 + int totalSteps = 7; + int normalizedStep = Math.max(0, Math.min(totalSteps, completedStep)); + + // 计算进度百分比:completedStep / totalSteps * 100 + double completedSchedule = BigDecimal.valueOf(normalizedStep) + .multiply(BigDecimal.valueOf(TOTAL_SCORE)) + .divide(BigDecimal.valueOf(totalSteps), 2, RoundingMode.HALF_UP) + .doubleValue(); + + // 取最大进度(防止回退) + double schedule = Math.max(progress(detail.getSchedule()), completedSchedule); + + detail.setCompletionStatus(String.valueOf(errorCount)); + detail.setSchedule(schedule); + detail.setScoreProject(score(errorCount, schedule)); + scoreDetailsMapper.updateById(detail); + } + private StuModuleScoreDetailsEntity getOrCreate(String userId) { StuModuleScoreDetailsEntity existing = scoreDetailsMapper.selectOne( new LambdaQueryWrapper() diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/RemoteWalletController.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/RemoteWalletController.java index 01d1a90..bdc1221 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/RemoteWalletController.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/RemoteWalletController.java @@ -7,10 +7,12 @@ import com.yau.digitalrmb.corporatewallet.application.RemoteDigestResult; import com.yau.digitalrmb.corporatewallet.application.RemoteExtractResult; import com.yau.digitalrmb.corporatewallet.application.RemoteFaceRecognitionResult; import com.yau.digitalrmb.corporatewallet.application.RemoteSignatureResult; +import com.yau.digitalrmb.corporatewallet.application.RemoteWalletKeyResult; import com.yau.digitalrmb.corporatewallet.application.RemoteWalletResult; import com.yau.digitalrmb.corporatewallet.application.RemoteWalletService; import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteFaceRecognitionRequest; import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteWalletApplicationRequest; +import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteWalletSignRequest; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.shared.api.ApiResponse; @@ -33,6 +35,16 @@ public class RemoteWalletController { @Resource private RemoteWalletService remoteWalletService; + // ==================== 密钥管理 ==================== + + @PostMapping("/keys") + @Operation(summary = "获取全部密钥信息", + description = "复用央行第一密钥和商业银行第二密钥,按需生成企业法人、数币操作员、数币复核员、监管机构密钥(若不存在则新增,已存在则复用)") + public ApiResponse> getAllKeys() { + return ApiResponse.success(remoteWalletService.getAllKeys( + InstitutionKeySubject.from(AuthContextHolder.get()), AuthContextHolder.get().getUsername()), traceId()); + } + // ==================== 步骤一:企业法人在线填写信息 ==================== @PostMapping("/concatenate") @@ -82,10 +94,11 @@ public class RemoteWalletController { @PostMapping("/sign") @Operation(summary = "步骤二-SM2计算签名", - description = "生成SM2密钥对对签约原文进行签名,返回私钥和签名值") - public ApiResponse computeSignature() { + description = "传入企业法人私钥进行SM2签名,私钥不正确则错误数+1") + public ApiResponse computeSignature( + @Valid @RequestBody RemoteWalletSignRequest request) { return ApiResponse.success(remoteWalletService.computeSignature( - InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey()), traceId()); } @PostMapping("/contract-output") diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryBatchController.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryBatchController.java index 5e1831f..10c7f15 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryBatchController.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryBatchController.java @@ -24,7 +24,7 @@ import javax.validation.Valid; @RestController @RequestMapping("/api/v1/salary") -@Tag(name = "模块六-企业发放数字人民币工资 - 步骤1:数币操作员导入工资明细", description = "数币操作员导入工资明细、签名并发送全流程") +@Tag(name = "模块六-2-数币操作员:导入工资明细", description = "数币操作员导入工资明细、签名并发送全流程") public class SalaryBatchController { @Resource private SalaryBatchService salaryBatchService; diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryConfirmController.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryConfirmController.java index 5a713cf..b47f377 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryConfirmController.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryConfirmController.java @@ -20,7 +20,7 @@ import javax.validation.Valid; @RestController @RequestMapping("/api/v1/salary/confirm") -@Tag(name = "模块六-企业发放数字人民币工资 - 步骤3:交易确认与工资发放", description = "提取交易确认原文、生成摘要、央行签名并输出全流程") +@Tag(name = "模块六-2-企步骤3:交易确认与工资发放", description = "提取交易确认原文、生成摘要、央行签名并输出全流程") public class SalaryConfirmController { @Resource private SalaryBatchService salaryBatchService; diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryReviewController.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryReviewController.java index 0266ef4..d14fa82 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryReviewController.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryReviewController.java @@ -21,7 +21,7 @@ import javax.validation.Valid; @RestController @RequestMapping("/api/v1/salary/review") -@Tag(name = "模块六-企业发放数字人民币工资 - 步骤2:数币复核员审核与权限审批", description = "数币复核员提取审核原文、权限校验、摘要、签名并输出全流程") +@Tag(name = "模块六-2-数币复核员审核与权限审批", description = "数币复核员提取审核原文、权限校验、摘要、签名并输出全流程") public class SalaryReviewController { @Resource private SalaryBatchService salaryBatchService; diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationVerificationService.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationVerificationService.java index d1d8df8..f911826 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationVerificationService.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyGenerationVerificationService.java @@ -100,16 +100,25 @@ public class CurrencyGenerationVerificationService { @Transactional public CurrencyQuotaVerificationResult verifyQuota(InstitutionKeySubject subject, String operator) { + // 额度验证是每个用户独立的,使用当前用户的记录 CurrencyGenerationVerification value = requireLatest(subject); + + // 【幂等性检查】如果已经验证过,直接返回结果,不执行任何更新操作 if (value.getStatus().ordinal() >= CurrencyGenerationVerification.Status.QUOTA_VERIFIED.ordinal()) { return new CurrencyQuotaVerificationResult(value); } + + // 执行额度验证 + // 本次申请金额从步骤二请求报文中获取(value.getAmount()) + // 已用额度暂时设为0,剩余额度 = 总额度 - 本次申请 + BigDecimal currentAmount = value.getAmount() != null ? value.getAmount() : BigDecimal.ZERO; + BigDecimal usedQuota = BigDecimal.ZERO; // 暂时不计算系统已用额度 + BigDecimal remainingQuota = TOTAL_QUOTA.subtract(currentAmount).max(BigDecimal.ZERO); + CurrencyGenerationVerification.Status expected = value.getStatus(); - BigDecimal usedQuota = requestMapper.sumActiveAmount(); - BigDecimal remainingQuota = TOTAL_QUOTA.subtract(usedQuota).max(BigDecimal.ZERO); invoke(() -> value.verifyQuota(TOTAL_QUOTA, usedQuota, remainingQuota)); repository.update(value, expected, "VERIFY_QUOTA", - "验证全系统50000元实训额度,累计已用额度包含本次申请金额", operator); + "验证全系统50000元实训额度,本次申请" + currentAmount.toPlainString() + "元", operator); return new CurrencyQuotaVerificationResult(value); } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerification.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerification.java index 064fc55..2e37816 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerification.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerification.java @@ -148,13 +148,13 @@ public class CurrencyGenerationVerification { } public void verifyQuota(BigDecimal total, BigDecimal used, BigDecimal remaining) { - if (!Boolean.TRUE.equals(digestMatches)) { - throw new IllegalStateException("请先匹配摘要一和摘要二"); - } + // 额度验证是系统级操作,不依赖摘要匹配状态 + // 移除 digestMatches 检查,允许独立进行额度验证 totalQuota = total; usedQuota = used; remainingQuota = remaining; - quotaSufficient = used.compareTo(total) <= 0; + // 判断本次申请金额是否超过剩余额度 + quotaSufficient = amount.compareTo(remaining) <= 0; if (quotaSufficient) { status = Status.QUOTA_VERIFIED; } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerificationRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerificationRepository.java index 66d3b93..cc72961 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerificationRepository.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/CurrencyGenerationVerificationRepository.java @@ -7,6 +7,13 @@ public interface CurrencyGenerationVerificationRepository { Optional findByIdAndSubject(long verificationId, String userId, long schoolId, long classId); Optional findLatestBySubject(String userId, long schoolId, long classId); + /** + * 查询系统共享的验证记录(用于全系统共享的额度验证) + * @param schoolId 学校ID + * @param classId 班级ID + * @return 系统共享的验证记录 + */ + Optional findSystemSharedRecord(long schoolId, long classId); void update(CurrencyGenerationVerification verification, CurrencyGenerationVerification.Status expectedStatus, String operation, String detail, String operator); diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java index 78c0766..91007a8 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/domain/InstitutionIdentityCryptography.java @@ -3,6 +3,12 @@ package com.yau.digitalrmb.institutionidentity.domain; public interface InstitutionIdentityCryptography { String sm3(String text); InstitutionSm2KeyPair generateSm2KeyPair(); + /** + * 从私钥推导公钥 + * @param privateKey 私钥(十六进制字符串) + * @return 公钥(压缩格式的十六进制字符串) + */ + String derivePublicKey(String privateKey); String sign(String privateKey, String message); boolean verify(String publicKey, String message, String signature); } diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisCurrencyGenerationVerificationRepository.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisCurrencyGenerationVerificationRepository.java index 345677f..024e3b2 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisCurrencyGenerationVerificationRepository.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/MybatisCurrencyGenerationVerificationRepository.java @@ -60,6 +60,20 @@ public class MybatisCurrencyGenerationVerificationRepository implements Currency return Optional.ofNullable(entity).map(this::toDomain); } + @Override + public Optional findSystemSharedRecord(long schoolId, long classId) { + // 使用固定的 SYSTEM 用户ID查询系统共享的验证记录 + CurrencyGenerationVerificationEntity entity = mapper.selectOne( + new LambdaQueryWrapper() + .eq(CurrencyGenerationVerificationEntity::getUserId, "SYSTEM") + .eq(CurrencyGenerationVerificationEntity::getSchoolId, schoolId) + .eq(CurrencyGenerationVerificationEntity::getClassId, classId) + .eq(CurrencyGenerationVerificationEntity::getDeleted, false) + .orderByDesc(CurrencyGenerationVerificationEntity::getCreatedAt) + .last("LIMIT 1")); + return Optional.ofNullable(entity).map(this::toDomain); + } + @Override public void update(CurrencyGenerationVerification value, CurrencyGenerationVerification.Status expectedStatus, diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java index a991f9e..d480ef4 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/infrastructure/SmInstitutionIdentityCryptography.java @@ -14,6 +14,7 @@ import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithID; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.crypto.signers.SM2Signer; +import org.bouncycastle.math.ec.ECPoint; import org.springframework.stereotype.Component; import java.math.BigInteger; @@ -51,6 +52,13 @@ public class SmInstitutionIdentityCryptography implements InstitutionIdentityCry hex(publicKey.getQ().getEncoded(false))); } + @Override + public String derivePublicKey(String privateKey) { + BigInteger d = new BigInteger(privateKey, 16); + ECPoint Q = DOMAIN.getG().multiply(d); + return hex(Q.getEncoded(false)); + } + @Override public String sign(String privateKey, String message) { ECPrivateKeyParameters key = new ECPrivateKeyParameters(new BigInteger(privateKey, 16), DOMAIN);