Compare commits

..

No commits in common. 'b21d70f311ea969e7db74392ae0c30885cdd5455' and '3c4fafe1eb96bc28a57f1a2d6d69646b1f52d212' have entirely different histories.

@ -142,44 +142,6 @@ public class CorporateWalletKeyService {
keyInfoMapper.insert(entity); 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, private CorporateWalletKeyResult buildResult(String keyType, String holder, String keyValue,
boolean isPrivateKey, String source) { boolean isPrivateKey, String source) {
return new CorporateWalletKeyResult(keyType, holder, "SM2", keyValue, isPrivateKey, source); return new CorporateWalletKeyResult(keyType, holder, "SM2", keyValue, isPrivateKey, source);

@ -170,9 +170,6 @@ public class CorporateWalletService {
subject.getSchoolId(), subject.getClassId()) subject.getSchoolId(), subject.getClassId())
.orElse(null); .orElse(null);
if (remoteActivated != null) { if (remoteActivated != null) {
// 【企业信息一致性校验】比对临柜开立和远程开立的企业信息是否一致
validateEnterpriseInfoConsistency(app, remoteActivated);
// 复用远程开立的钱包ID,不再生成新钱包 // 复用远程开立的钱包ID,不再生成新钱包
app.activateWallet(remoteActivated.getWalletId(), remoteActivated.getActivateTime()); app.activateWallet(remoteActivated.getWalletId(), remoteActivated.getActivateTime());
repository.update(app); repository.update(app);
@ -181,8 +178,7 @@ public class CorporateWalletService {
app.getWalletType(), "ACTIVATED", remoteActivated.getActivateTime(), "远程开立"); app.getWalletType(), "ACTIVATED", remoteActivated.getActivateTime(), "远程开立");
} }
// 钱包ID基于统一社会信用代码生成,确保同一企业无论通过哪个渠道开立,钱包ID都一致 String walletId = "CORP_" + app.getDigestValue();
String walletId = "CORP_" + cryptography.sm3(app.getCreditCode());
String activateTime = app.getVerifyTime() != null ? app.getVerifyTime() String activateTime = app.getVerifyTime() != null ? app.getVerifyTime()
: LocalDateTime.now().format(TIMESTAMP_FORMAT); : LocalDateTime.now().format(TIMESTAMP_FORMAT);
@ -221,38 +217,4 @@ public class CorporateWalletService {
+ "\"applyTime\":\"" + applyTime + "\"," + "\"applyTime\":\"" + applyTime + "\","
+ "\"status\":\"已提交\"}"; + "\"status\":\"已提交\"}";
} }
/**
* 校验临柜开立和远程开立的企业信息一致性
* @param corpApp 临柜开立申请
* @param remoteApp 远程开立申请
*/
private void validateEnterpriseInfoConsistency(CorporateWalletApplication corpApp,
RemoteWalletApplication remoteApp) {
java.util.List<String> 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);
}
}
} }

@ -6,12 +6,15 @@ import lombok.Getter;
@Getter @Getter
@Schema(description = "远程开立-SM2签名结果") @Schema(description = "远程开立-SM2签名结果")
public class RemoteSignatureResult { public class RemoteSignatureResult {
@Schema(description = "签名私钥(十六进制)")
private final String privateKey;
@Schema(description = "签名算法", example = "SM2") @Schema(description = "签名算法", example = "SM2")
private final String algorithm; private final String algorithm;
@Schema(description = "签名值(十六进制)") @Schema(description = "签名值(十六进制)")
private final String signature; private final String signature;
public RemoteSignatureResult(String algorithm, String signature) { public RemoteSignatureResult(String privateKey, String algorithm, String signature) {
this.privateKey = privateKey;
this.algorithm = algorithm; this.algorithm = algorithm;
this.signature = signature; this.signature = signature;
} }

@ -1,31 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-密钥信息")
public class RemoteWalletKeyResult {
@Schema(description = "密钥类型(如:企业法人私钥、企业法人公钥等)")
private final String keyType;
@Schema(description = "持有方(如:张三(创新科技公司))")
private final String holder;
@Schema(description = "密码算法(SM2)")
private final String keyAlgorithm;
@Schema(description = "密钥值(十六进制)")
private final String keyValue;
@Schema(description = "是否为私钥")
private final boolean privateKey;
@Schema(description = "密钥来源:REUSED-复用已有, NEW-新生成")
private final String source;
public RemoteWalletKeyResult(String keyType, String holder, String keyAlgorithm,
String keyValue, boolean privateKey, String source) {
this.keyType = keyType;
this.holder = holder;
this.keyAlgorithm = keyAlgorithm;
this.keyValue = keyValue;
this.privateKey = privateKey;
this.source = source;
}
}

@ -1,17 +1,13 @@
package com.yau.digitalrmb.corporatewallet.application; 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.CorporateWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository; import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication; import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository; 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.corporatewallet.interfaces.rest.dto.RemoteWalletApplicationRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; 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.institutionidentity.infrastructure.SmInstitutionIdentityCryptography;
import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.shared.api.ErrorCode; import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -32,16 +28,6 @@ public class RemoteWalletService {
private CorporateWalletApplicationRepository corporateRepository; private CorporateWalletApplicationRepository corporateRepository;
@Resource @Resource
private SmInstitutionIdentityCryptography cryptography; 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 = "企业法人私钥";
// ==================== 步骤一:企业法人在线填写信息 ==================== // ==================== 步骤一:企业法人在线填写信息 ====================
@ -136,81 +122,23 @@ public class RemoteWalletService {
} }
@Transactional @Transactional
public RemoteSignatureResult computeSignature(InstitutionKeySubject subject, String providedPrivateKey) { public RemoteSignatureResult computeSignature(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject); RemoteWalletApplication app = findRequired(subject);
if (app.getSigningDigestValue() == null) { if (app.getSigningDigestValue() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM3摘要运算"); throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM3摘要运算");
} }
if (app.getSignature() != null) { if (app.getSignature() != null) {
return new RemoteSignatureResult("SM2", app.getSignature()); return new RemoteSignatureResult(app.getSignPrivateKey(), "SM2", app.getSignature());
} }
// 验证前端传入的私钥不能为空 InstitutionSm2KeyPair keyPair = cryptography.generateSm2KeyPair();
if (providedPrivateKey == null || providedPrivateKey.trim().isEmpty()) { String privateKey = keyPair.getPrivateKey();
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "企业法人私钥不能为空"); String signature = cryptography.sign(privateKey, app.getSigningExtractMessage());
}
String trimmedPrivateKey = providedPrivateKey.trim(); app.setSignPrivateKey(privateKey);
// 检查数据库中是否已存在企业法人私钥
String legalPersonName = app.getLegalPerson() != null ? app.getLegalPerson() : "企业法人";
CorporateWalletKeyInfoEntity existingKey = keyInfoMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CorporateWalletKeyInfoEntity>()
.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); app.setSignature(signature);
repository.update(app); repository.update(app);
return new RemoteSignatureResult("SM2", signature); return new RemoteSignatureResult(privateKey, "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");
}
}
/**
* 保存密钥信息到 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 @Transactional
@ -293,9 +221,6 @@ public class RemoteWalletService {
subject.getSchoolId(), subject.getClassId()) subject.getSchoolId(), subject.getClassId())
.orElse(null); .orElse(null);
if (corpActivated != null) { if (corpActivated != null) {
// 【企业信息一致性校验】比对远程开立和临柜开立的企业信息是否一致
validateEnterpriseInfoConsistency(app, corpActivated);
// 复用临柜开立的钱包ID,不再生成新钱包 // 复用临柜开立的钱包ID,不再生成新钱包
app.activateWallet(corpActivated.getWalletId(), corpActivated.getActivateTime()); app.activateWallet(corpActivated.getWalletId(), corpActivated.getActivateTime());
repository.update(app); repository.update(app);
@ -304,8 +229,7 @@ public class RemoteWalletService {
app.getWalletType(), "ACTIVATED", corpActivated.getActivateTime(), "已验证", "临柜开立"); app.getWalletType(), "ACTIVATED", corpActivated.getActivateTime(), "已验证", "临柜开立");
} }
// 钱包ID基于统一社会信用代码生成,确保同一企业无论通过哪个渠道开立,钱包ID都一致 String walletId = "CORP_" + app.getWalletDigestValue();
String walletId = "CORP_" + cryptography.sm3(app.getCreditCode());
String activateTime = app.getSignTime() != null ? app.getSignTime() String activateTime = app.getSignTime() != null ? app.getSignTime()
: LocalDateTime.now().format(TIMESTAMP_FORMAT); : LocalDateTime.now().format(TIMESTAMP_FORMAT);
@ -336,108 +260,4 @@ public class RemoteWalletService {
return "REMOTE_OPEN|" + corpName + "|" + creditCode + "|" + legalPerson + "|" return "REMOTE_OPEN|" + corpName + "|" + creditCode + "|" + legalPerson + "|"
+ phone + "|" + applyTime; + phone + "|" + applyTime;
} }
/**
* 获取远程开立所需的全部密钥信息:
* 央行第一密钥、商业银行第二密钥复用已有数据;
* 企业法人、数币操作员、数币复核员、监管机构密钥按需生成(若不存在则新增,已存在则复用)。
*/
@Transactional
public java.util.List<RemoteWalletKeyResult> getAllKeys(InstitutionKeySubject subject, String operator) {
java.util.List<RemoteWalletKeyResult> 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<String> 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);
}
}
} }

@ -66,8 +66,6 @@ public class SalaryBatchService {
private SalaryScoreService scoreService; private SalaryScoreService scoreService;
@Resource @Resource
private InstitutionKeyService institutionKeyService; private InstitutionKeyService institutionKeyService;
@Resource
private CorporateWalletKeyService keyService;
// ==================== 步骤一:调取区块信息 ==================== // ==================== 步骤一:调取区块信息 ====================
@ -89,9 +87,6 @@ public class SalaryBatchService {
batch.fetchBlockchain(batchId, corpWalletId, OPERATOR_NAME, timestamp); batch.fetchBlockchain(batchId, corpWalletId, OPERATOR_NAME, timestamp);
repository.update(batch); repository.update(batch);
// 标记步骤3完成:数币操作员导入工资明细
scoreService.markStepCompleted(subject.getUserId(), 3);
return new SalaryBlockchainResult(corpWalletId, batchId, OPERATOR_NAME, timestamp); return new SalaryBlockchainResult(corpWalletId, batchId, OPERATOR_NAME, timestamp);
} }
@ -108,9 +103,6 @@ public class SalaryBatchService {
batch.concatenate(message); batch.concatenate(message);
repository.update(batch); repository.update(batch);
// 继续步骤3:拼接原文(属于数币操作员流程的一部分)
// 不单独计分,合并到步骤3中
return new SalaryConcatenateResult(message); return new SalaryConcatenateResult(message);
} }
@ -130,9 +122,6 @@ public class SalaryBatchService {
batch.computeDigest("SM3", digest); batch.computeDigest("SM3", digest);
repository.update(batch); repository.update(batch);
// 继续步骤3:生成摘要(属于数币操作员流程的一部分)
// 不单独计分,合并到步骤3中
return new SalaryDigestResult(digest); return new SalaryDigestResult(digest);
} }
@ -166,9 +155,6 @@ public class SalaryBatchService {
batch.sign(signature); batch.sign(signature);
repository.update(batch); repository.update(batch);
// 继续步骤3:SM2签名(属于数币操作员流程的一部分)
// 不单独计分,合并到步骤3中
return new SalarySignatureResult(signature); return new SalarySignatureResult(signature);
} }
@ -188,9 +174,6 @@ public class SalaryBatchService {
batch.output(json); batch.output(json);
repository.update(batch); repository.update(batch);
// 继续步骤3:输出(属于数币操作员流程的一部分)
// 不单独计分,合并到步骤3中
return new SalaryOutputResult(json); return new SalaryOutputResult(json);
} }
@ -292,9 +275,6 @@ public class SalaryBatchService {
batch.checkPermission(singleStatus, batchStatus, dailyStatus, reviewResult); batch.checkPermission(singleStatus, batchStatus, dailyStatus, reviewResult);
repository.update(batch); repository.update(batch);
// 标记步骤4完成:数币复核员审核与权限审批
scoreService.markStepCompleted(subject.getUserId(), 4);
return new SalaryReviewCheckResult( return new SalaryReviewCheckResult(
singleStatus, SINGLE_LIMIT.setScale(2).toPlainString(), maxSingleAmount.setScale(2).toPlainString(), singleDesc, singleStatus, SINGLE_LIMIT.setScale(2).toPlainString(), maxSingleAmount.setScale(2).toPlainString(), singleDesc,
batchStatus, BATCH_LIMIT.setScale(2).toPlainString(), batchTotal.setScale(2).toPlainString(), batchDesc, batchStatus, BATCH_LIMIT.setScale(2).toPlainString(), batchTotal.setScale(2).toPlainString(), batchDesc,
@ -508,9 +488,6 @@ public class SalaryBatchService {
batch.outputConfirm(json); batch.outputConfirm(json);
repository.update(batch); repository.update(batch);
// 标记步骤5完成:交易确认与工资发放
scoreService.markStepCompleted(subject.getUserId(), 5);
return new SalaryConfirmOutputResult(json); return new SalaryConfirmOutputResult(json);
} }
@ -550,17 +527,35 @@ public class SalaryBatchService {
} }
/** /**
* 从 corporate_wallet_key_info 表获取数币操作员私钥(若不存在则自动生成) * 从 corporate_wallet_key_info 表获取数币操作员私钥
*/ */
private String findOperatorPrivateKey(InstitutionKeySubject subject) { private String findOperatorPrivateKey(InstitutionKeySubject subject) {
return keyService.getOrCreatePrivateKey(subject, OPERATOR_KEY_TYPE, OPERATOR_NAME); CorporateWalletKeyInfoEntity entity = keyInfoMapper.selectOne(
new LambdaQueryWrapper<CorporateWalletKeyInfoEntity>()
.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();
} }
/** /**
* 从 corporate_wallet_key_info 表获取数币复核员私钥(若不存在则自动生成) * 从 corporate_wallet_key_info 表获取数币复核员私钥
*/ */
private String findReviewerPrivateKey(InstitutionKeySubject subject) { private String findReviewerPrivateKey(InstitutionKeySubject subject) {
return keyService.getOrCreatePrivateKey(subject, REVIEWER_KEY_TYPE, REVIEWER_NAME); CorporateWalletKeyInfoEntity entity = keyInfoMapper.selectOne(
new LambdaQueryWrapper<CorporateWalletKeyInfoEntity>()
.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();
} }
/** /**

@ -42,40 +42,6 @@ public class SalaryScoreService {
return accumulatedErrorCount; 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) { private StuModuleScoreDetailsEntity getOrCreate(String userId) {
StuModuleScoreDetailsEntity existing = scoreDetailsMapper.selectOne( StuModuleScoreDetailsEntity existing = scoreDetailsMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>() new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()

@ -7,12 +7,10 @@ import com.yau.digitalrmb.corporatewallet.application.RemoteDigestResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteExtractResult; import com.yau.digitalrmb.corporatewallet.application.RemoteExtractResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteFaceRecognitionResult; import com.yau.digitalrmb.corporatewallet.application.RemoteFaceRecognitionResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteSignatureResult; 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.RemoteWalletResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteWalletService; import com.yau.digitalrmb.corporatewallet.application.RemoteWalletService;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteFaceRecognitionRequest; 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.RemoteWalletApplicationRequest;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteWalletSignRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder; import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.shared.api.ApiResponse; import com.yau.digitalrmb.shared.api.ApiResponse;
@ -35,16 +33,6 @@ public class RemoteWalletController {
@Resource @Resource
private RemoteWalletService remoteWalletService; private RemoteWalletService remoteWalletService;
// ==================== 密钥管理 ====================
@PostMapping("/keys")
@Operation(summary = "获取全部密钥信息",
description = "复用央行第一密钥和商业银行第二密钥,按需生成企业法人、数币操作员、数币复核员、监管机构密钥(若不存在则新增,已存在则复用)")
public ApiResponse<java.util.List<RemoteWalletKeyResult>> getAllKeys() {
return ApiResponse.success(remoteWalletService.getAllKeys(
InstitutionKeySubject.from(AuthContextHolder.get()), AuthContextHolder.get().getUsername()), traceId());
}
// ==================== 步骤一:企业法人在线填写信息 ==================== // ==================== 步骤一:企业法人在线填写信息 ====================
@PostMapping("/concatenate") @PostMapping("/concatenate")
@ -94,11 +82,10 @@ public class RemoteWalletController {
@PostMapping("/sign") @PostMapping("/sign")
@Operation(summary = "步骤二-SM2计算签名", @Operation(summary = "步骤二-SM2计算签名",
description = "传入企业法人私钥进行SM2签名,私钥不正确则错误数+1") description = "生成SM2密钥对对签约原文进行签名,返回私钥和签名值")
public ApiResponse<RemoteSignatureResult> computeSignature( public ApiResponse<RemoteSignatureResult> computeSignature() {
@Valid @RequestBody RemoteWalletSignRequest request) {
return ApiResponse.success(remoteWalletService.computeSignature( return ApiResponse.success(remoteWalletService.computeSignature(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey()), traceId()); InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
} }
@PostMapping("/contract-output") @PostMapping("/contract-output")

@ -24,7 +24,7 @@ import javax.validation.Valid;
@RestController @RestController
@RequestMapping("/api/v1/salary") @RequestMapping("/api/v1/salary")
@Tag(name = "模块六-2-数币操作员:导入工资明细", description = "数币操作员导入工资明细、签名并发送全流程") @Tag(name = "模块六-企业发放数字人民币工资 - 步骤1:数币操作员导入工资明细", description = "数币操作员导入工资明细、签名并发送全流程")
public class SalaryBatchController { public class SalaryBatchController {
@Resource @Resource
private SalaryBatchService salaryBatchService; private SalaryBatchService salaryBatchService;

@ -20,7 +20,7 @@ import javax.validation.Valid;
@RestController @RestController
@RequestMapping("/api/v1/salary/confirm") @RequestMapping("/api/v1/salary/confirm")
@Tag(name = "模块六-2-企步骤3:交易确认与工资发放", description = "提取交易确认原文、生成摘要、央行签名并输出全流程") @Tag(name = "模块六-企业发放数字人民币工资 - 步骤3:交易确认与工资发放", description = "提取交易确认原文、生成摘要、央行签名并输出全流程")
public class SalaryConfirmController { public class SalaryConfirmController {
@Resource @Resource
private SalaryBatchService salaryBatchService; private SalaryBatchService salaryBatchService;

@ -21,7 +21,7 @@ import javax.validation.Valid;
@RestController @RestController
@RequestMapping("/api/v1/salary/review") @RequestMapping("/api/v1/salary/review")
@Tag(name = "模块六-2-数币复核员审核与权限审批", description = "数币复核员提取审核原文、权限校验、摘要、签名并输出全流程") @Tag(name = "模块六-企业发放数字人民币工资 - 步骤2:数币复核员审核与权限审批", description = "数币复核员提取审核原文、权限校验、摘要、签名并输出全流程")
public class SalaryReviewController { public class SalaryReviewController {
@Resource @Resource
private SalaryBatchService salaryBatchService; private SalaryBatchService salaryBatchService;

@ -1,14 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
@Schema(description = "远程开立-SM2签名请求(企业法人私钥)")
public class RemoteWalletSignRequest {
@NotBlank(message = "企业法人私钥不能为空")
@Schema(description = "企业法人私钥(十六进制)", required = true)
private String privateKey;
}

@ -100,25 +100,16 @@ public class CurrencyGenerationVerificationService {
@Transactional @Transactional
public CurrencyQuotaVerificationResult verifyQuota(InstitutionKeySubject subject, String operator) { public CurrencyQuotaVerificationResult verifyQuota(InstitutionKeySubject subject, String operator) {
// 额度验证是每个用户独立的,使用当前用户的记录
CurrencyGenerationVerification value = requireLatest(subject); CurrencyGenerationVerification value = requireLatest(subject);
// 【幂等性检查】如果已经验证过,直接返回结果,不执行任何更新操作
if (value.getStatus().ordinal() >= CurrencyGenerationVerification.Status.QUOTA_VERIFIED.ordinal()) { if (value.getStatus().ordinal() >= CurrencyGenerationVerification.Status.QUOTA_VERIFIED.ordinal()) {
return new CurrencyQuotaVerificationResult(value); 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(); 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)); invoke(() -> value.verifyQuota(TOTAL_QUOTA, usedQuota, remainingQuota));
repository.update(value, expected, "VERIFY_QUOTA", repository.update(value, expected, "VERIFY_QUOTA",
"验证全系统50000元实训额度,本次申请" + currentAmount.toPlainString() + "元", operator); "验证全系统50000元实训额度,累计已用额度包含本次申请金额", operator);
return new CurrencyQuotaVerificationResult(value); return new CurrencyQuotaVerificationResult(value);
} }

@ -148,13 +148,13 @@ public class CurrencyGenerationVerification {
} }
public void verifyQuota(BigDecimal total, BigDecimal used, BigDecimal remaining) { public void verifyQuota(BigDecimal total, BigDecimal used, BigDecimal remaining) {
// 额度验证是系统级操作,不依赖摘要匹配状态 if (!Boolean.TRUE.equals(digestMatches)) {
// 移除 digestMatches 检查,允许独立进行额度验证 throw new IllegalStateException("请先匹配摘要一和摘要二");
}
totalQuota = total; totalQuota = total;
usedQuota = used; usedQuota = used;
remainingQuota = remaining; remainingQuota = remaining;
// 判断本次申请金额是否超过剩余额度 quotaSufficient = used.compareTo(total) <= 0;
quotaSufficient = amount.compareTo(remaining) <= 0;
if (quotaSufficient) { if (quotaSufficient) {
status = Status.QUOTA_VERIFIED; status = Status.QUOTA_VERIFIED;
} }

@ -7,13 +7,6 @@ public interface CurrencyGenerationVerificationRepository {
Optional<CurrencyGenerationVerification> findByIdAndSubject(long verificationId, String userId, Optional<CurrencyGenerationVerification> findByIdAndSubject(long verificationId, String userId,
long schoolId, long classId); long schoolId, long classId);
Optional<CurrencyGenerationVerification> findLatestBySubject(String userId, long schoolId, long classId); Optional<CurrencyGenerationVerification> findLatestBySubject(String userId, long schoolId, long classId);
/**
* 查询系统共享的验证记录(用于全系统共享的额度验证)
* @param schoolId 学校ID
* @param classId 班级ID
* @return 系统共享的验证记录
*/
Optional<CurrencyGenerationVerification> findSystemSharedRecord(long schoolId, long classId);
void update(CurrencyGenerationVerification verification, void update(CurrencyGenerationVerification verification,
CurrencyGenerationVerification.Status expectedStatus, CurrencyGenerationVerification.Status expectedStatus,
String operation, String detail, String operator); String operation, String detail, String operator);

@ -3,12 +3,6 @@ package com.yau.digitalrmb.institutionidentity.domain;
public interface InstitutionIdentityCryptography { public interface InstitutionIdentityCryptography {
String sm3(String text); String sm3(String text);
InstitutionSm2KeyPair generateSm2KeyPair(); InstitutionSm2KeyPair generateSm2KeyPair();
/**
* 从私钥推导公钥
* @param privateKey 私钥(十六进制字符串)
* @return 公钥(压缩格式的十六进制字符串)
*/
String derivePublicKey(String privateKey);
String sign(String privateKey, String message); String sign(String privateKey, String message);
boolean verify(String publicKey, String message, String signature); boolean verify(String publicKey, String message, String signature);
} }

@ -60,20 +60,6 @@ public class MybatisCurrencyGenerationVerificationRepository implements Currency
return Optional.ofNullable(entity).map(this::toDomain); return Optional.ofNullable(entity).map(this::toDomain);
} }
@Override
public Optional<CurrencyGenerationVerification> findSystemSharedRecord(long schoolId, long classId) {
// 使用固定的 SYSTEM 用户ID查询系统共享的验证记录
CurrencyGenerationVerificationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<CurrencyGenerationVerificationEntity>()
.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 @Override
public void update(CurrencyGenerationVerification value, public void update(CurrencyGenerationVerification value,
CurrencyGenerationVerification.Status expectedStatus, CurrencyGenerationVerification.Status expectedStatus,

@ -14,7 +14,6 @@ import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithID; import org.bouncycastle.crypto.params.ParametersWithID;
import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.signers.SM2Signer; import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.math.ec.ECPoint;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.math.BigInteger; import java.math.BigInteger;
@ -52,13 +51,6 @@ public class SmInstitutionIdentityCryptography implements InstitutionIdentityCry
hex(publicKey.getQ().getEncoded(false))); 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 @Override
public String sign(String privateKey, String message) { public String sign(String privateKey, String message) {
ECPrivateKeyParameters key = new ECPrivateKeyParameters(new BigInteger(privateKey, 16), DOMAIN); ECPrivateKeyParameters key = new ECPrivateKeyParameters(new BigInteger(privateKey, 16), DOMAIN);

Loading…
Cancel
Save