feat:文件上传的功能开发

master
jiazheng.zhao 2 weeks ago
parent fc53367daa
commit f19428560a

@ -58,15 +58,15 @@ public class CorporateWalletKeyService {
// 4. 数币操作员密钥(新增)
String[] operatorPair = findOrCreateNewKeyPair(subject, "数币操作员私钥", "数币操作员公钥",
"李四", operator);
results.add(buildResult("数币操作员私钥", "李四", operatorPair[0], true, "NEW"));
results.add(buildResult("数币操作员公钥", "李四", operatorPair[1], false, "NEW"));
"王五", operator);
results.add(buildResult("数币操作员私钥", "王五", operatorPair[0], true, "NEW"));
results.add(buildResult("数币操作员公钥", "王五", operatorPair[1], false, "NEW"));
// 5. 数币复核员密钥(新增)
String[] reviewerPair = findOrCreateNewKeyPair(subject, "数币复核员私钥", "数币复核员公钥",
"王五", operator);
results.add(buildResult("数币复核员私钥", "王五", reviewerPair[0], true, "NEW"));
results.add(buildResult("数币复核员公钥", "王五", reviewerPair[1], false, "NEW"));
"赵六", operator);
results.add(buildResult("数币复核员私钥", "赵六", reviewerPair[0], true, "NEW"));
results.add(buildResult("数币复核员公钥", "赵六", reviewerPair[1], false, "NEW"));
// 6. 监管机构密钥(新增)
String[] regulatorPair = findOrCreateNewKeyPair(subject, "监管机构私钥", "监管机构公钥",

@ -18,14 +18,22 @@ public class CorporateWalletResult {
private final String status;
@Schema(description = "激活时间", example = "20260801101000")
private final String activateTime;
@Schema(description = "开立渠道:临柜开立 / 远程开立(复用时标注原渠道)")
private final String channel;
public CorporateWalletResult(String walletId, String corpName, String creditCode,
String walletType, String status, String activateTime) {
this(walletId, corpName, creditCode, walletType, status, activateTime, "临柜开立");
}
public CorporateWalletResult(String walletId, String corpName, String creditCode,
String walletType, String status, String activateTime, String channel) {
this.walletId = walletId;
this.corpName = corpName;
this.creditCode = creditCode;
this.walletType = walletType;
this.status = status;
this.activateTime = activateTime;
this.channel = channel;
}
}

@ -2,6 +2,8 @@ package com.yau.digitalrmb.corporatewallet.application;
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.interfaces.rest.dto.CorporateWalletApplicationRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.infrastructure.SmInstitutionIdentityCryptography;
@ -22,6 +24,8 @@ public class CorporateWalletService {
@Resource
private CorporateWalletApplicationRepository repository;
@Resource
private RemoteWalletApplicationRepository remoteRepository;
@Resource
private SmInstitutionIdentityCryptography cryptography;
/**
@ -145,6 +149,8 @@ public class CorporateWalletService {
/**
*
*
* ID
*/
@Transactional
public CorporateWalletResult outputWallet(InstitutionKeySubject subject) {
@ -155,7 +161,21 @@ public class CorporateWalletService {
if (app.getWalletId() != null) {
return new CorporateWalletResult(
app.getWalletId(), app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", app.getActivateTime());
app.getWalletType(), "ACTIVATED", app.getActivateTime(), "临柜开立");
}
// 跨渠道检查:远程开立是否已激活同信用代码的钱包
RemoteWalletApplication remoteActivated = remoteRepository
.findActivatedByCreditCode(app.getCreditCode(), subject.getUserId(),
subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (remoteActivated != null) {
// 复用远程开立的钱包ID不再生成新钱包
app.activateWallet(remoteActivated.getWalletId(), remoteActivated.getActivateTime());
repository.update(app);
return new CorporateWalletResult(
remoteActivated.getWalletId(), app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", remoteActivated.getActivateTime(), "远程开立");
}
String walletId = "CORP_" + app.getDigestValue();
@ -167,7 +187,7 @@ public class CorporateWalletService {
return new CorporateWalletResult(
walletId, app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", activateTime);
app.getWalletType(), "ACTIVATED", activateTime, "临柜开立");
}
private CorporateWalletApplication findOrCreate(InstitutionKeySubject subject) {

@ -20,10 +20,18 @@ public class RemoteWalletResult {
private final String activateTime;
@Schema(description = "人脸识别记录", example = "已验证")
private final String faceRecognitionLog;
@Schema(description = "开立渠道:远程开立 / 临柜开立(复用时标注原渠道)")
private final String channel;
public RemoteWalletResult(String walletId, String corpName, String creditCode,
String walletType, String status, String activateTime,
String faceRecognitionLog) {
this(walletId, corpName, creditCode, walletType, status, activateTime, faceRecognitionLog, "远程开立");
}
public RemoteWalletResult(String walletId, String corpName, String creditCode,
String walletType, String status, String activateTime,
String faceRecognitionLog, String channel) {
this.walletId = walletId;
this.corpName = corpName;
this.creditCode = creditCode;
@ -31,5 +39,6 @@ public class RemoteWalletResult {
this.status = status;
this.activateTime = activateTime;
this.faceRecognitionLog = faceRecognitionLog;
this.channel = channel;
}
}

@ -1,5 +1,7 @@
package com.yau.digitalrmb.corporatewallet.application;
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.interfaces.rest.dto.RemoteWalletApplicationRequest;
@ -23,6 +25,8 @@ public class RemoteWalletService {
@Resource
private RemoteWalletApplicationRepository repository;
@Resource
private CorporateWalletApplicationRepository corporateRepository;
@Resource
private SmInstitutionIdentityCryptography cryptography;
// ==================== 步骤一:企业法人在线填写信息 ====================
@ -194,6 +198,11 @@ public class RemoteWalletService {
return new RemoteDigestResult("SM3", digest);
}
/**
*
*
* ID
*/
@Transactional
public RemoteWalletResult outputWallet(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject);
@ -203,7 +212,21 @@ public class RemoteWalletService {
if (app.getWalletId() != null) {
return new RemoteWalletResult(
app.getWalletId(), app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", app.getActivateTime(), "已验证");
app.getWalletType(), "ACTIVATED", app.getActivateTime(), "已验证", "远程开立");
}
// 跨渠道检查:临柜开立是否已激活同信用代码的钱包
CorporateWalletApplication corpActivated = corporateRepository
.findActivatedByCreditCode(app.getCreditCode(), subject.getUserId(),
subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (corpActivated != null) {
// 复用临柜开立的钱包ID不再生成新钱包
app.activateWallet(corpActivated.getWalletId(), corpActivated.getActivateTime());
repository.update(app);
return new RemoteWalletResult(
corpActivated.getWalletId(), app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", corpActivated.getActivateTime(), "已验证", "临柜开立");
}
String walletId = "CORP_" + app.getWalletDigestValue();
@ -215,7 +238,7 @@ public class RemoteWalletService {
return new RemoteWalletResult(
walletId, app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", activateTime, "已验证");
app.getWalletType(), "ACTIVATED", activateTime, "已验证", "远程开立");
}
// ==================== 内部方法 ====================

@ -0,0 +1,318 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatch;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatchRepository;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class SalaryBatchService {
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final String OPERATOR_NAME = "王五";
private static final String OPERATOR_KEY_TYPE = "数币操作员私钥";
// 内置工资明细数据
private static final String[][] EMPLOYEE_DATA = {
{"张明", "WALLET_EMP_001", "技术部", "8500.00"},
{"李二", "WALLET_EMP_002", "市场部", "7200.00"},
{"王小", "WALLET_EMP_003", "财务部", "6800.00"},
{"赵六", "WALLET_EMP_004", "人事部", "5600.00"},
{"钱七", "WALLET_EMP_005", "技术部", "9200.00"}
};
@Resource
private SalaryBatchRepository repository;
@Resource
private CorporateWalletApplicationRepository corporateWalletRepository;
@Resource
private RemoteWalletApplicationRepository remoteWalletRepository;
@Resource
private CorporateWalletKeyInfoMapper keyInfoMapper;
@Resource
private InstitutionIdentityCryptography cryptography;
@Resource
private SalaryScoreService scoreService;
// ==================== 步骤一:调取区块信息 ====================
@Transactional
public SalaryBlockchainResult fetchBlockchain(InstitutionKeySubject subject, String operator) {
SalaryBatch batch = findOrCreate(subject);
// 付款钱包从步骤一(对公钱包开立)获取
String corpWalletId = findCorporateWalletId(subject);
if (corpWalletId == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成对公钱包开立(步骤一)");
}
// 批次号随机生成
String batchId = generateBatchId();
// 时间戳
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
batch.fetchBlockchain(batchId, corpWalletId, OPERATOR_NAME, timestamp);
repository.update(batch);
return new SalaryBlockchainResult(corpWalletId, batchId, OPERATOR_NAME, timestamp);
}
// ==================== 步骤二:拼接工资发放申请原文 ====================
@Transactional
public SalaryConcatenateResult concatenate(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.BLOCKCHAIN_FETCHED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先调取区块信息");
}
String message = buildConcatenatedMessage(batch);
batch.concatenate(message);
repository.update(batch);
return new SalaryConcatenateResult(message);
}
// ==================== 步骤三:生成摘要 ====================
@Transactional
public SalaryDigestResult computeDigest(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.CONCATENATED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先拼接工资发放申请原文");
}
if (batch.getDigestValue() != null) {
return new SalaryDigestResult(batch.getDigestValue());
}
String digest = cryptography.sm3(batch.getConcatenatedMessage());
batch.computeDigest("SM3", digest);
repository.update(batch);
return new SalaryDigestResult(digest);
}
// ==================== 步骤四SM2签名 ====================
@Transactional
public SalarySignatureResult sign(InstitutionKeySubject subject, String providedPrivateKey) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.DIGESTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成摘要");
}
if (batch.getSignature() != null) {
return new SalarySignatureResult(batch.getSignature());
}
// 从 corporate_wallet_key_info 表获取数币操作员私钥
String storedPrivateKey = findOperatorPrivateKey(subject);
if (storedPrivateKey == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "数币操作员私钥不存在,请先获取密钥");
}
// 验证传入的私钥是否正确
if (!storedPrivateKey.equalsIgnoreCase(providedPrivateKey != null ? providedPrivateKey.trim() : "")) {
// 错误数加1
scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数币操作员私钥不正确,错误次数+1");
}
// 使用数币操作员私钥对摘要进行SM2签名
String signature = cryptography.sign(storedPrivateKey, batch.getConcatenatedMessage());
batch.sign(signature);
repository.update(batch);
return new SalarySignatureResult(signature);
}
// ==================== 步骤五:输出 ====================
@Transactional
public SalaryOutputResult output(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.SIGNED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM2签名");
}
if (batch.getOutputJson() != null) {
return new SalaryOutputResult(batch.getOutputJson());
}
String json = buildOutputJson(batch);
batch.output(json);
repository.update(batch);
return new SalaryOutputResult(json);
}
// ==================== 步骤六:发送 ====================
@Transactional
public SalarySendResult send(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.OUTPUT.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成输出");
}
if (batch.getStatus() == SalaryBatch.Status.SENT) {
return new SalarySendResult("待审核", batch.getSubmitTime());
}
String submitTime = LocalDateTime.now().format(TIMESTAMP_FORMAT);
batch.send(submitTime);
repository.update(batch);
return new SalarySendResult("待审核", submitTime);
}
// ==================== 页面查询 ====================
@Transactional(readOnly = true)
public SalaryPageResult pageQuery(InstitutionKeySubject subject) {
// 始终返回内置工资明细
List<SalaryPageResult.EmployeeDetail> payroll = new ArrayList<>();
for (int i = 0; i < EMPLOYEE_DATA.length; i++) {
payroll.add(new SalaryPageResult.EmployeeDetail(
i + 1, EMPLOYEE_DATA[i][0], EMPLOYEE_DATA[i][1],
EMPLOYEE_DATA[i][2], EMPLOYEE_DATA[i][3]));
}
String totalAmount = calculateTotalAmount();
int employeeCount = EMPLOYEE_DATA.length;
// 查询已有批次数据,若不存在则返回空字段(不报错)
SalaryBatch batch = repository
.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (batch == null) {
return new SalaryPageResult(payroll, totalAmount, employeeCount,
null, null, null, null, null, null, null, null, null);
}
return new SalaryPageResult(payroll, totalAmount, employeeCount,
batch.getBatchId(), batch.getCorpWalletId(), batch.getOperatorName(),
batch.getOperatorTimestamp(), batch.getConcatenatedMessage(),
batch.getDigestValue(), batch.getSignature(), batch.getOutputJson(),
batch.getStatus() != null ? batch.getStatus().name() : null);
}
// ==================== 内部方法 ====================
private SalaryBatch findOrCreate(InstitutionKeySubject subject) {
return repository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseGet(() -> repository.save(
SalaryBatch.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId())));
}
private SalaryBatch findRequired(InstitutionKeySubject subject) {
return repository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"工资批次不存在,请先调取区块信息"));
}
/**
* /ID
*/
private String findCorporateWalletId(InstitutionKeySubject subject) {
// 先查临柜开立
CorporateWalletApplication corpApp = corporateWalletRepository
.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (corpApp != null && corpApp.getWalletId() != null && !corpApp.getWalletId().isEmpty()) {
return corpApp.getWalletId();
}
// 再查远程开立
RemoteWalletApplication remoteApp = remoteWalletRepository
.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (remoteApp != null && remoteApp.getWalletId() != null && !remoteApp.getWalletId().isEmpty()) {
return remoteApp.getWalletId();
}
return null;
}
/**
* corporate_wallet_key_info
*/
private String findOperatorPrivateKey(InstitutionKeySubject subject) {
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();
}
private String generateBatchId() {
String datePart = LocalDateTime.now().format(DATE_FORMAT);
int seq = ThreadLocalRandom.current().nextInt(100, 1000);
return "BATCH_" + datePart + "_" + String.format("%03d", seq);
}
private String buildConcatenatedMessage(SalaryBatch batch) {
StringBuilder sb = new StringBuilder();
sb.append(batch.getBatchId());
sb.append("|").append(batch.getCorpWalletId());
sb.append("|").append(batch.getOperatorName());
for (String[] emp : EMPLOYEE_DATA) {
sb.append("|").append(emp[0]); // 姓名
sb.append("|").append(emp[1]); // 钱包ID
sb.append("|").append(emp[3]); // 金额
}
sb.append("|").append(batch.getOperatorTimestamp());
return sb.toString();
}
private String buildOutputJson(SalaryBatch batch) {
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("\"batchId\":\"").append(batch.getBatchId()).append("\",");
sb.append("\"corpWalletId\":\"").append(batch.getCorpWalletId()).append("\",");
sb.append("\"operator\":\"").append(batch.getOperatorName()).append("\",");
sb.append("\"totalAmount\":\"").append(calculateTotalAmount()).append("\",");
sb.append("\"employeeCount\":").append(EMPLOYEE_DATA.length).append(",");
sb.append("\"payroll\":[");
for (int i = 0; i < EMPLOYEE_DATA.length; i++) {
if (i > 0) sb.append(",");
sb.append("{\"name\":\"").append(EMPLOYEE_DATA[i][0]).append("\",");
sb.append("\"walletId\":\"").append(EMPLOYEE_DATA[i][1]).append("\",");
sb.append("\"amount\":\"").append(EMPLOYEE_DATA[i][3]).append("\"}");
}
sb.append("],");
sb.append("\"digest\":\"").append(batch.getDigestValue()).append("\",");
sb.append("\"operatorSignature\":\"").append(batch.getSignature()).append("\",");
sb.append("\"status\":\"待审核\",");
sb.append("\"submitTime\":\"").append(batch.getOperatorTimestamp()).append("\"");
sb.append("}");
return sb.toString();
}
private String calculateTotalAmount() {
double total = 0;
for (String[] emp : EMPLOYEE_DATA) {
total += Double.parseDouble(emp[3]);
}
return String.format("%.2f", total);
}
}

@ -0,0 +1,24 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "调取区块信息结果")
public class SalaryBlockchainResult {
@Schema(description = "付款钱包ID从步骤一对公钱包开立获取")
private final String corpWalletId;
@Schema(description = "批次号(随机生成)")
private final String batchId;
@Schema(description = "操作员(数币操作员)")
private final String operatorName;
@Schema(description = "时间戳")
private final String timestamp;
public SalaryBlockchainResult(String corpWalletId, String batchId, String operatorName, String timestamp) {
this.corpWalletId = corpWalletId;
this.batchId = batchId;
this.operatorName = operatorName;
this.timestamp = timestamp;
}
}

@ -0,0 +1,15 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "拼接工资发放申请原文结果")
public class SalaryConcatenateResult {
@Schema(description = "拼接原文(管道分隔)")
private final String concatenatedMessage;
public SalaryConcatenateResult(String concatenatedMessage) {
this.concatenatedMessage = concatenatedMessage;
}
}

@ -0,0 +1,15 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "SM3摘要结果只返回摘要值")
public class SalaryDigestResult {
@Schema(description = "摘要值")
private final String digestValue;
public SalaryDigestResult(String digestValue) {
this.digestValue = digestValue;
}
}

@ -0,0 +1,15 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "输出JSON结果")
public class SalaryOutputResult {
@Schema(description = "输出JSON报文")
private final String outputJson;
public SalaryOutputResult(String outputJson) {
this.outputJson = outputJson;
}
}

@ -0,0 +1,76 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import java.util.List;
@Getter
@Schema(description = "工资明细页面查询结果")
public class SalaryPageResult {
@Schema(description = "工资明细列表")
private final List<EmployeeDetail> payroll;
@Schema(description = "合计金额")
private final String totalAmount;
@Schema(description = "员工数量")
private final int employeeCount;
@Schema(description = "批次号(若已调取区块信息)")
private final String batchId;
@Schema(description = "付款钱包ID若已调取区块信息")
private final String corpWalletId;
@Schema(description = "操作员(若已调取区块信息)")
private final String operatorName;
@Schema(description = "时间戳(若已调取区块信息)")
private final String timestamp;
@Schema(description = "拼接原文(若已拼接)")
private final String concatenatedMessage;
@Schema(description = "摘要值(若已生成摘要)")
private final String digestValue;
@Schema(description = "签名值(若已签名)")
private final String signature;
@Schema(description = "输出JSON若已输出")
private final String outputJson;
@Schema(description = "状态(若已发送)")
private final String status;
public SalaryPageResult(List<EmployeeDetail> payroll, String totalAmount, int employeeCount,
String batchId, String corpWalletId, String operatorName, String timestamp,
String concatenatedMessage, String digestValue, String signature,
String outputJson, String status) {
this.payroll = payroll;
this.totalAmount = totalAmount;
this.employeeCount = employeeCount;
this.batchId = batchId;
this.corpWalletId = corpWalletId;
this.operatorName = operatorName;
this.timestamp = timestamp;
this.concatenatedMessage = concatenatedMessage;
this.digestValue = digestValue;
this.signature = signature;
this.outputJson = outputJson;
this.status = status;
}
@Getter
@Schema(description = "员工工资明细")
public static class EmployeeDetail {
@Schema(description = "序号")
private final int seqNo;
@Schema(description = "姓名")
private final String name;
@Schema(description = "钱包ID")
private final String walletId;
@Schema(description = "部门")
private final String department;
@Schema(description = "工资金额(元)")
private final String amount;
public EmployeeDetail(int seqNo, String name, String walletId, String department, String amount) {
this.seqNo = seqNo;
this.name = name;
this.walletId = walletId;
this.department = department;
this.amount = amount;
}
}
}

@ -0,0 +1,94 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.UUID;
/**
*
*
*/
@Service
public class SalaryScoreService {
public static final String MODULE_NAME = "企业发放数字人民币工资";
private static final int MODULE_SERIAL_NUMBER = 6;
private static final double TOTAL_SCORE = 100.0;
@Resource
private StuModuleScoreDetailsMapper scoreDetailsMapper;
@Value("${salary.score.wrong-deduct:0.50}")
private BigDecimal wrongDeduct = new BigDecimal("0.50");
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int recordError(String userId) {
if (userId == null || userId.trim().isEmpty()) {
return 0;
}
StuModuleScoreDetailsEntity detail = getOrCreate(userId);
int accumulatedErrorCount = errorCount(detail.getCompletionStatus()) + 1;
detail.setCompletionStatus(String.valueOf(accumulatedErrorCount));
detail.setScoreProject(score(accumulatedErrorCount, progress(detail.getSchedule())));
scoreDetailsMapper.updateById(detail);
return accumulatedErrorCount;
}
private StuModuleScoreDetailsEntity getOrCreate(String userId) {
StuModuleScoreDetailsEntity existing = scoreDetailsMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, userId)
.eq(StuModuleScoreDetailsEntity::getMoudule, MODULE_NAME)
.eq(StuModuleScoreDetailsEntity::getSerialNumber, MODULE_SERIAL_NUMBER)
.last("LIMIT 1"));
if (existing != null) {
return existing;
}
StuModuleScoreDetailsEntity created = new StuModuleScoreDetailsEntity();
created.setId(UUID.randomUUID().toString());
created.setMoudule(MODULE_NAME);
created.setLearningProjects("实验实训");
created.setAssessmentItems("工资发放流程错误次数");
created.setScoringCriteria(wrongDeduct.stripTrailingZeros().toPlainString());
created.setCompletionStatus("0");
created.setScoreProject(0.0);
created.setUserId(userId);
created.setTotalScore(TOTAL_SCORE);
created.setSerialNumber(MODULE_SERIAL_NUMBER);
created.setSchedule(0.0);
scoreDetailsMapper.insert(created);
return created;
}
private double score(int errorCount, double schedule) {
BigDecimal base = BigDecimal.valueOf(TOTAL_SCORE)
.subtract(wrongDeduct.multiply(BigDecimal.valueOf(Math.max(0, errorCount))))
.max(BigDecimal.ZERO);
return base.multiply(BigDecimal.valueOf(schedule))
.divide(BigDecimal.valueOf(TOTAL_SCORE), 2, RoundingMode.HALF_UP)
.doubleValue();
}
private double progress(Double schedule) {
return schedule == null ? 0.0 : schedule;
}
private int errorCount(String completionStatus) {
if (completionStatus == null || completionStatus.trim().isEmpty()) {
return 0;
}
try {
return Math.max(0, Integer.parseInt(completionStatus));
} catch (NumberFormatException exception) {
return 0;
}
}
}

@ -0,0 +1,18 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "发送结果")
public class SalarySendResult {
@Schema(description = "状态:待审核")
private final String status;
@Schema(description = "发送时间")
private final String submitTime;
public SalarySendResult(String status, String submitTime) {
this.status = status;
this.submitTime = submitTime;
}
}

@ -0,0 +1,15 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "SM2签名结果只返回签名值")
public class SalarySignatureResult {
@Schema(description = "签名值")
private final String signature;
public SalarySignatureResult(String signature) {
this.signature = signature;
}
}

@ -6,5 +6,6 @@ public interface CorporateWalletApplicationRepository {
CorporateWalletApplication save(CorporateWalletApplication application);
Optional<CorporateWalletApplication> findLatest(String userId, long schoolId, long classId);
Optional<CorporateWalletApplication> findByApplicationId(String applicationId);
Optional<CorporateWalletApplication> findActivatedByCreditCode(String creditCode, String userId, long schoolId, long classId);
void update(CorporateWalletApplication application);
}

@ -5,5 +5,6 @@ import java.util.Optional;
public interface RemoteWalletApplicationRepository {
RemoteWalletApplication save(RemoteWalletApplication application);
Optional<RemoteWalletApplication> findLatest(String userId, long schoolId, long classId);
Optional<RemoteWalletApplication> findActivatedByCreditCode(String creditCode, String userId, long schoolId, long classId);
void update(RemoteWalletApplication application);
}

@ -0,0 +1,110 @@
package com.yau.digitalrmb.corporatewallet.domain;
public class SalaryBatch {
public enum Status {
DRAFT, BLOCKCHAIN_FETCHED, CONCATENATED, DIGESTED, SIGNED, OUTPUT, SENT
}
private Long id;
private String userId;
private long schoolId;
private long classId;
// 调取区块信息
private String batchId;
private String corpWalletId;
private String operatorName;
private String operatorTimestamp;
// 拼接原文
private String concatenatedMessage;
// SM3摘要
private String digestAlgorithm;
private String digestValue;
// SM2签名
private String signature;
// 输出JSON
private String outputJson;
// 状态
private Status status;
private String submitTime;
public static SalaryBatch create(String userId, long schoolId, long classId) {
SalaryBatch batch = new SalaryBatch();
batch.userId = userId;
batch.schoolId = schoolId;
batch.classId = classId;
batch.status = Status.DRAFT;
return batch;
}
public void fetchBlockchain(String batchId, String corpWalletId, String operatorName, String timestamp) {
this.batchId = batchId;
this.corpWalletId = corpWalletId;
this.operatorName = operatorName;
this.operatorTimestamp = timestamp;
this.status = Status.BLOCKCHAIN_FETCHED;
}
public void concatenate(String message) {
this.concatenatedMessage = message;
this.status = Status.CONCATENATED;
}
public void computeDigest(String algorithm, String digest) {
this.digestAlgorithm = algorithm;
this.digestValue = digest;
this.status = Status.DIGESTED;
}
public void sign(String signature) {
this.signature = signature;
this.status = Status.SIGNED;
}
public void output(String outputJson) {
this.outputJson = outputJson;
this.status = Status.OUTPUT;
}
public void send(String submitTime) {
this.submitTime = submitTime;
this.status = Status.SENT;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public long getSchoolId() { return schoolId; }
public void setSchoolId(long schoolId) { this.schoolId = schoolId; }
public long getClassId() { return classId; }
public void setClassId(long classId) { this.classId = classId; }
public String getBatchId() { return batchId; }
public void setBatchId(String batchId) { this.batchId = batchId; }
public String getCorpWalletId() { return corpWalletId; }
public void setCorpWalletId(String corpWalletId) { this.corpWalletId = corpWalletId; }
public String getOperatorName() { return operatorName; }
public void setOperatorName(String operatorName) { this.operatorName = operatorName; }
public String getOperatorTimestamp() { return operatorTimestamp; }
public void setOperatorTimestamp(String operatorTimestamp) { this.operatorTimestamp = operatorTimestamp; }
public String getConcatenatedMessage() { return concatenatedMessage; }
public void setConcatenatedMessage(String concatenatedMessage) { this.concatenatedMessage = concatenatedMessage; }
public String getDigestAlgorithm() { return digestAlgorithm; }
public void setDigestAlgorithm(String digestAlgorithm) { this.digestAlgorithm = digestAlgorithm; }
public String getDigestValue() { return digestValue; }
public void setDigestValue(String digestValue) { this.digestValue = digestValue; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
public String getOutputJson() { return outputJson; }
public void setOutputJson(String outputJson) { this.outputJson = outputJson; }
public Status getStatus() { return status; }
public void setStatus(Status status) { this.status = status; }
public String getSubmitTime() { return submitTime; }
public void setSubmitTime(String submitTime) { this.submitTime = submitTime; }
}

@ -0,0 +1,9 @@
package com.yau.digitalrmb.corporatewallet.domain;
import java.util.Optional;
public interface SalaryBatchRepository {
SalaryBatch save(SalaryBatch batch);
Optional<SalaryBatch> findLatest(String userId, long schoolId, long classId);
void update(SalaryBatch batch);
}

@ -44,6 +44,22 @@ public class MybatisCorporateWalletApplicationRepository implements CorporateWal
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public Optional<CorporateWalletApplication> findActivatedByCreditCode(String creditCode, String userId, long schoolId, long classId) {
CorporateWalletApplicationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<CorporateWalletApplicationEntity>()
.eq(CorporateWalletApplicationEntity::getCreditCode, creditCode)
.eq(CorporateWalletApplicationEntity::getUserId, userId)
.eq(CorporateWalletApplicationEntity::getSchoolId, schoolId)
.eq(CorporateWalletApplicationEntity::getClassId, classId)
.eq(CorporateWalletApplicationEntity::getDeleted, false)
.isNotNull(CorporateWalletApplicationEntity::getWalletId)
.ne(CorporateWalletApplicationEntity::getWalletId, "")
.orderByDesc(CorporateWalletApplicationEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public void update(CorporateWalletApplication application) {
mapper.updateById(toEntity(application));

@ -34,6 +34,22 @@ public class MybatisRemoteWalletApplicationRepository implements RemoteWalletApp
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public Optional<RemoteWalletApplication> findActivatedByCreditCode(String creditCode, String userId, long schoolId, long classId) {
RemoteWalletApplicationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<RemoteWalletApplicationEntity>()
.eq(RemoteWalletApplicationEntity::getCreditCode, creditCode)
.eq(RemoteWalletApplicationEntity::getUserId, userId)
.eq(RemoteWalletApplicationEntity::getSchoolId, schoolId)
.eq(RemoteWalletApplicationEntity::getClassId, classId)
.eq(RemoteWalletApplicationEntity::getDeleted, false)
.isNotNull(RemoteWalletApplicationEntity::getWalletId)
.ne(RemoteWalletApplicationEntity::getWalletId, "")
.orderByDesc(RemoteWalletApplicationEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public void update(RemoteWalletApplication application) {
mapper.updateById(toEntity(application));

@ -0,0 +1,81 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatch;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatchRepository;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.Optional;
@Repository
public class MybatisSalaryBatchRepository implements SalaryBatchRepository {
@Resource
private SalaryBatchMapper mapper;
@Override
public SalaryBatch save(SalaryBatch batch) {
SalaryBatchEntity entity = toEntity(batch);
mapper.insert(entity);
batch.setId(entity.getId());
return batch;
}
@Override
public Optional<SalaryBatch> findLatest(String userId, long schoolId, long classId) {
SalaryBatchEntity entity = mapper.selectOne(
new LambdaQueryWrapper<SalaryBatchEntity>()
.eq(SalaryBatchEntity::getUserId, userId)
.eq(SalaryBatchEntity::getSchoolId, schoolId)
.eq(SalaryBatchEntity::getClassId, classId)
.eq(SalaryBatchEntity::getDeleted, false)
.orderByDesc(SalaryBatchEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public void update(SalaryBatch batch) {
mapper.updateById(toEntity(batch));
}
private SalaryBatchEntity toEntity(SalaryBatch batch) {
SalaryBatchEntity entity = new SalaryBatchEntity();
entity.setId(batch.getId());
entity.setUserId(batch.getUserId());
entity.setSchoolId(batch.getSchoolId());
entity.setClassId(batch.getClassId());
entity.setBatchId(batch.getBatchId());
entity.setCorpWalletId(batch.getCorpWalletId());
entity.setOperatorName(batch.getOperatorName());
entity.setOperatorTimestamp(batch.getOperatorTimestamp());
entity.setConcatenatedMessage(batch.getConcatenatedMessage());
entity.setDigestAlgorithm(batch.getDigestAlgorithm());
entity.setDigestValue(batch.getDigestValue());
entity.setSignature(batch.getSignature());
entity.setOutputJson(batch.getOutputJson());
entity.setStatus(batch.getStatus() == null ? null : batch.getStatus().name());
entity.setSubmitTime(batch.getSubmitTime());
return entity;
}
private SalaryBatch toDomain(SalaryBatchEntity entity) {
SalaryBatch batch = new SalaryBatch();
batch.setId(entity.getId());
batch.setUserId(entity.getUserId());
batch.setSchoolId(entity.getSchoolId());
batch.setClassId(entity.getClassId());
batch.setBatchId(entity.getBatchId());
batch.setCorpWalletId(entity.getCorpWalletId());
batch.setOperatorName(entity.getOperatorName());
batch.setOperatorTimestamp(entity.getOperatorTimestamp());
batch.setConcatenatedMessage(entity.getConcatenatedMessage());
batch.setDigestAlgorithm(entity.getDigestAlgorithm());
batch.setDigestValue(entity.getDigestValue());
batch.setSignature(entity.getSignature());
batch.setOutputJson(entity.getOutputJson());
batch.setStatus(entity.getStatus() == null ? null : SalaryBatch.Status.valueOf(entity.getStatus()));
batch.setSubmitTime(entity.getSubmitTime());
return batch;
}
}

@ -0,0 +1,41 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName("salary_batch")
public class SalaryBatchEntity extends AuditableEntity {
@TableField("user_id")
private String userId;
@TableField("school_id")
private Long schoolId;
@TableField("class_id")
private Long classId;
@TableField("batch_id")
private String batchId;
@TableField("corp_wallet_id")
private String corpWalletId;
@TableField("operator_name")
private String operatorName;
@TableField("operator_timestamp")
private String operatorTimestamp;
@TableField("concatenated_message")
private String concatenatedMessage;
@TableField("digest_algorithm")
private String digestAlgorithm;
@TableField("digest_value")
private String digestValue;
@TableField("signature")
private String signature;
@TableField("output_json")
private String outputJson;
@TableField("status")
private String status;
@TableField("submit_time")
private String submitTime;
}

@ -0,0 +1,8 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SalaryBatchMapper extends BaseMapper<SalaryBatchEntity> {
}

@ -0,0 +1,92 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.corporatewallet.application.SalaryBatchService;
import com.yau.digitalrmb.corporatewallet.application.SalaryBlockchainResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryConcatenateResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryDigestResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryOutputResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryPageResult;
import com.yau.digitalrmb.corporatewallet.application.SalarySendResult;
import com.yau.digitalrmb.corporatewallet.application.SalarySignatureResult;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.SalarySignRequest;
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.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/salary")
@Tag(name = "模块六-企业发放数字人民币工资 - 步骤1数币操作员导入工资明细", description = "数币操作员导入工资明细、签名并发送全流程")
public class SalaryBatchController {
@Resource
private SalaryBatchService salaryBatchService;
@PostMapping("/blockchain")
@Operation(summary = "调取区块信息",
description = "从步骤一对公钱包开立获取付款钱包ID随机生成批次号操作员为王五生成时间戳")
public ApiResponse<SalaryBlockchainResult> fetchBlockchain() {
JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(salaryBatchService.fetchBlockchain(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
}
@PostMapping("/concatenate")
@Operation(summary = "拼接工资发放申请原文",
description = "拼接批次号|付款钱包|操作员|员工姓名|钱包ID|金额|...|时间戳")
public ApiResponse<SalaryConcatenateResult> concatenate() {
return ApiResponse.success(salaryBatchService.concatenate(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/digest")
@Operation(summary = "生成摘要",
description = "对拼接原文进行SM3运算只返回摘要值")
public ApiResponse<SalaryDigestResult> computeDigest() {
return ApiResponse.success(salaryBatchService.computeDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/sign")
@Operation(summary = "SM2签名",
description = "传入数币操作员私钥进行SM2签名私钥不正确则错误数+1只返回签名值")
public ApiResponse<SalarySignatureResult> sign(@Valid @RequestBody SalarySignRequest request) {
return ApiResponse.success(salaryBatchService.sign(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey()), traceId());
}
@PostMapping("/output")
@Operation(summary = "输出",
description = "生成包含批次号、钱包、工资明细、摘要、签名等信息的JSON报文")
public ApiResponse<SalaryOutputResult> output() {
return ApiResponse.success(salaryBatchService.output(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/send")
@Operation(summary = "发送",
description = "发送工资发放申请到数币复核员审核,状态变为待审核")
public ApiResponse<SalarySendResult> send() {
return ApiResponse.success(salaryBatchService.send(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@GetMapping("/page")
@Operation(summary = "页面查询",
description = "查询工资明细和当前步骤状态,前面步骤未完成时返回工资明细但其他字段为空(不报错)")
public ApiResponse<SalaryPageResult> pageQuery() {
return ApiResponse.success(salaryBatchService.pageQuery(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -0,0 +1,14 @@
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 SalarySignRequest {
@NotBlank(message = "数币操作员私钥不能为空")
@Schema(description = "数币操作员私钥(十六进制)", required = true)
private String privateKey;
}

@ -0,0 +1,18 @@
-- 模块六:密钥信息表 - 存储央行、商业银行、企业法人、数币操作员、复核员、监管机构的SM2密钥对
CREATE TABLE IF NOT EXISTS corporate_wallet_key_info (
id BIGINT PRIMARY KEY COMMENT '主键ID雪花算法生成',
user_id VARCHAR(36) NOT NULL COMMENT '用户ID关联登录用户',
school_id BIGINT NOT NULL COMMENT '学校ID',
class_id BIGINT NOT NULL COMMENT '班级ID',
key_type VARCHAR(64) NOT NULL COMMENT '密钥类型(如:央行第一私钥、央行第一公钥、商业银行第二私钥、企业法人私钥、数币操作员私钥、数币复核员私钥、监管机构私钥等)',
holder VARCHAR(128) NOT NULL COMMENT '持有方(如:中国人民银行、中国银行、张三、李四、王五、赵六、民政部公益监管中心)',
key_algorithm VARCHAR(16) NOT NULL DEFAULT 'SM2' COMMENT '密码算法SM2',
key_value TEXT NOT NULL COMMENT '完整值十六进制私钥或公钥的HEX字符串',
is_private_key BOOLEAN NOT NULL COMMENT '是否为私钥true-私钥, false-公钥',
source VARCHAR(32) NOT NULL DEFAULT 'NEW' COMMENT '密钥来源REUSED-复用已有(央行/商业银行), NEW-新生成, SIGNED-签约生成(企业法人)',
created_at TIMESTAMP NOT NULL COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL COMMENT '更新时间',
created_by VARCHAR(64) NOT NULL COMMENT '创建人',
updated_by VARCHAR(64) NOT NULL COMMENT '更新人',
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除标记false-未删除, true-已删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-密钥信息表';

@ -0,0 +1,36 @@
-- 模块六:工资发放批次表 - 数币操作员导入工资明细、签名并发送
CREATE TABLE IF NOT EXISTS salary_batch (
id BIGINT PRIMARY KEY COMMENT '主键ID雪花算法生成',
user_id VARCHAR(36) NOT NULL COMMENT '用户ID关联登录用户',
school_id BIGINT NOT NULL COMMENT '学校ID',
class_id BIGINT NOT NULL COMMENT '班级ID',
-- 调取区块信息
batch_id VARCHAR(64) COMMENT '批次号BATCH_20260801_001',
corp_wallet_id VARCHAR(128) COMMENT '付款钱包ID从步骤一对公钱包开立获取CORP_3A4B...',
operator_name VARCHAR(64) COMMENT '操作员姓名(如:王五)',
operator_timestamp VARCHAR(20) COMMENT '操作时间戳格式yyyyMMddHHmmss20260801120000',
-- 拼接原文
concatenated_message TEXT COMMENT '拼接工资发放申请原文(管道分隔)',
-- SM3摘要
digest_algorithm VARCHAR(16) COMMENT '摘要算法SM3',
digest_value VARCHAR(128) COMMENT 'SM3摘要值十六进制大写',
-- SM2签名
signature TEXT COMMENT 'SM2签名值DER编码十六进制',
-- 输出JSON
output_json TEXT COMMENT '输出JSON报文',
-- 状态
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态DRAFT-草稿, BLOCKCHAIN_FETCHED-已调取区块信息, CONCATENATED-已拼接, DIGESTED-已生成摘要, SIGNED-已签名, OUTPUT-已输出, SENT-已发送',
submit_time VARCHAR(20) COMMENT '发送时间格式yyyyMMddHHmmss',
created_at TIMESTAMP NOT NULL COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL COMMENT '更新时间',
created_by VARCHAR(64) NOT NULL COMMENT '创建人',
updated_by VARCHAR(64) NOT NULL COMMENT '更新人',
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除标记false-未删除, true-已删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-工资发放批次表';

@ -1022,6 +1022,30 @@ CREATE TABLE IF NOT EXISTS corporate_wallet_key_info (
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除标记false-未删除, true-已删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-密钥信息表';
-- 模块六:工资发放批次表 - 数币操作员导入工资明细、签名并发送
CREATE TABLE IF NOT EXISTS salary_batch (
id BIGINT PRIMARY KEY COMMENT '主键ID雪花算法生成',
user_id VARCHAR(36) NOT NULL COMMENT '用户ID关联登录用户',
school_id BIGINT NOT NULL COMMENT '学校ID',
class_id BIGINT NOT NULL COMMENT '班级ID',
batch_id VARCHAR(64) COMMENT '批次号BATCH_20260801_001',
corp_wallet_id VARCHAR(128) COMMENT '付款钱包ID从步骤一对公钱包开立获取',
operator_name VARCHAR(64) COMMENT '操作员姓名(如:王五)',
operator_timestamp VARCHAR(20) COMMENT '操作时间戳格式yyyyMMddHHmmss',
concatenated_message TEXT COMMENT '拼接工资发放申请原文(管道分隔)',
digest_algorithm VARCHAR(16) COMMENT '摘要算法SM3',
digest_value VARCHAR(128) COMMENT 'SM3摘要值十六进制大写',
signature TEXT COMMENT 'SM2签名值DER编码十六进制',
output_json TEXT COMMENT '输出JSON报文',
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态DRAFT-草稿, BLOCKCHAIN_FETCHED-已调取区块信息, CONCATENATED-已拼接, DIGESTED-已生成摘要, SIGNED-已签名, OUTPUT-已输出, SENT-已发送',
submit_time VARCHAR(20) COMMENT '发送时间格式yyyyMMddHHmmss',
created_at TIMESTAMP NOT NULL COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL COMMENT '更新时间',
created_by VARCHAR(64) NOT NULL COMMENT '创建人',
updated_by VARCHAR(64) NOT NULL COMMENT '更新人',
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除标记false-未删除, true-已删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-工资发放批次表';
-- 步骤四商业银行端:生成合约信息与开通确认
CREATE TABLE IF NOT EXISTS smart_contract_generation (
id BIGINT PRIMARY KEY,

@ -671,3 +671,27 @@ CREATE TABLE IF NOT EXISTS corporate_wallet_key_info (
updated_by VARCHAR(64) NOT NULL COMMENT '更新人',
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-密钥信息表';
-- 模块六:工资发放批次表
CREATE TABLE IF NOT EXISTS salary_batch (
id BIGINT PRIMARY KEY COMMENT '主键ID',
user_id VARCHAR(36) NOT NULL COMMENT '用户ID',
school_id BIGINT NOT NULL COMMENT '学校ID',
class_id BIGINT NOT NULL COMMENT '班级ID',
batch_id VARCHAR(64) COMMENT '批次号',
corp_wallet_id VARCHAR(128) COMMENT '付款钱包ID',
operator_name VARCHAR(64) COMMENT '操作员姓名',
operator_timestamp VARCHAR(20) COMMENT '操作时间戳',
concatenated_message TEXT COMMENT '拼接原文',
digest_algorithm VARCHAR(16) COMMENT '摘要算法',
digest_value VARCHAR(128) COMMENT 'SM3摘要值',
signature TEXT COMMENT 'SM2签名值',
output_json TEXT COMMENT '输出JSON报文',
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态',
submit_time VARCHAR(20) COMMENT '发送时间',
created_at TIMESTAMP NOT NULL COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL COMMENT '更新时间',
created_by VARCHAR(64) NOT NULL COMMENT '创建人',
updated_by VARCHAR(64) NOT NULL COMMENT '更新人',
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-工资发放批次表';

Loading…
Cancel
Save