feat:文件上传的功能开发

master
jiazheng.zhao 2 weeks ago
parent 9a90eb1eee
commit 146a494f7c

@ -7,6 +7,7 @@ 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.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatch;
@ -17,6 +18,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@ -29,6 +31,17 @@ public class SalaryBatchService {
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 REVIEWER_NAME = "赵六";
private static final String REVIEWER_KEY_TYPE = "数币复核员私钥";
// 权限限额(元)
private static final BigDecimal SINGLE_LIMIT = new BigDecimal("50000.00");
private static final BigDecimal BATCH_LIMIT = new BigDecimal("500000.00");
private static final BigDecimal DAILY_LIMIT = new BigDecimal("1000000.00");
// 审核状态1-通过, 2-未通过
private static final int STATUS_PASSED = 1;
private static final int STATUS_FAILED = 2;
// 内置工资明细数据
private static final String[][] EMPLOYEE_DATA = {
@ -51,6 +64,8 @@ public class SalaryBatchService {
private InstitutionIdentityCryptography cryptography;
@Resource
private SalaryScoreService scoreService;
@Resource
private InstitutionKeyService institutionKeyService;
// ==================== 步骤一:调取区块信息 ====================
@ -181,7 +196,161 @@ public class SalaryBatchService {
return new SalarySendResult("待审核", submitTime);
}
// ==================== 页面查询 ====================
// ==================== 步骤七:提取审核原文 ====================
@Transactional
public SalaryReviewExtractResult extractReviewText(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.SENT.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成数币操作员发送步骤");
}
if (batch.getReviewConcatenatedMessage() != null) {
return new SalaryReviewExtractResult(batch.getReviewConcatenatedMessage(),
batch.getBatchId(), batch.getReviewerName(),
calculateTotalAmount(), EMPLOYEE_DATA.length, batch.getReviewTimestamp());
}
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
String totalAmount = calculateTotalAmount();
int employeeCount = EMPLOYEE_DATA.length;
// 拼接审核原文REVIEW|批次号|复核员|总金额|人数|时间戳
String reviewMessage = "REVIEW|" + batch.getBatchId() + "|" + REVIEWER_NAME
+ "|" + totalAmount + "|" + employeeCount + "|" + timestamp;
batch.extractReviewText(REVIEWER_NAME, timestamp, reviewMessage);
repository.update(batch);
return new SalaryReviewExtractResult(reviewMessage, batch.getBatchId(),
REVIEWER_NAME, totalAmount, employeeCount, timestamp);
}
// ==================== 步骤八:审核与权限校验 ====================
@Transactional
public SalaryReviewCheckResult checkPermission(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEW_EXTRACTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先提取审核原文");
}
if (batch.getSingleLimitStatus() != null) {
// 已校验过,直接返回
return buildCheckResult(batch);
}
// 计算单笔最高金额
BigDecimal maxSingleAmount = BigDecimal.ZERO;
for (String[] emp : EMPLOYEE_DATA) {
BigDecimal amount = new BigDecimal(emp[3]);
if (amount.compareTo(maxSingleAmount) > 0) {
maxSingleAmount = amount;
}
}
// 批次总金额
BigDecimal batchTotal = new BigDecimal(calculateTotalAmount());
// 当日累计发放金额(查询当天所有已发送批次的总金额)
BigDecimal dailyTotal = calculateDailyTotal(subject, batchTotal);
// 单笔限额校验
int singleStatus = maxSingleAmount.compareTo(SINGLE_LIMIT) <= 0 ? STATUS_PASSED : STATUS_FAILED;
String singleDesc = "单笔最高金额 " + maxSingleAmount.setScale(2).toPlainString() + " 元 "
+ (singleStatus == STATUS_PASSED ? "≤" : ">") + " " + SINGLE_LIMIT.setScale(2).toPlainString() + " 元";
// 批次限额校验
int batchStatus = batchTotal.compareTo(BATCH_LIMIT) <= 0 ? STATUS_PASSED : STATUS_FAILED;
String batchDesc = "批次总金额 " + batchTotal.setScale(2).toPlainString() + " 元 "
+ (batchStatus == STATUS_PASSED ? "≤" : ">") + " " + BATCH_LIMIT.setScale(2).toPlainString() + " 元";
// 日累计限额校验
int dailyStatus = dailyTotal.compareTo(DAILY_LIMIT) <= 0 ? STATUS_PASSED : STATUS_FAILED;
String dailyDesc = "当日累计发放金额 " + dailyTotal.setScale(2).toPlainString() + " 元 "
+ (dailyStatus == STATUS_PASSED ? "≤" : ">") + " " + DAILY_LIMIT.setScale(2).toPlainString() + " 元";
// 审核结果:全部通过才批准
String reviewResult = (singleStatus == STATUS_PASSED && batchStatus == STATUS_PASSED && dailyStatus == STATUS_PASSED)
? "APPROVED" : "REJECTED";
batch.checkPermission(singleStatus, batchStatus, dailyStatus, reviewResult);
repository.update(batch);
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,
dailyStatus, DAILY_LIMIT.setScale(2).toPlainString(), dailyTotal.setScale(2).toPlainString(), dailyDesc,
reviewResult);
}
// ==================== 步骤九:生成审核摘要 ====================
@Transactional
public SalaryReviewDigestResult computeReviewDigest(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEW_CHECKED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成审核与权限校验");
}
if (batch.getReviewDigestValue() != null) {
return new SalaryReviewDigestResult(batch.getReviewDigestValue());
}
String digest = cryptography.sm3(batch.getReviewConcatenatedMessage());
batch.computeReviewDigest("SM3", digest);
repository.update(batch);
return new SalaryReviewDigestResult(digest);
}
// ==================== 步骤十SM2签名复核员私钥 ====================
@Transactional
public SalaryReviewSignatureResult signReview(InstitutionKeySubject subject, String providedPrivateKey) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEW_DIGESTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成审核摘要");
}
if (batch.getReviewSignature() != null) {
return new SalaryReviewSignatureResult(batch.getReviewSignature());
}
// 从 corporate_wallet_key_info 表获取数币复核员私钥
String storedPrivateKey = findReviewerPrivateKey(subject);
if (storedPrivateKey == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "数币复核员私钥不存在,请先获取密钥");
}
// 验证传入的私钥是否正确
if (!storedPrivateKey.equalsIgnoreCase(providedPrivateKey != null ? providedPrivateKey.trim() : "")) {
scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数币复核员私钥不正确,错误次数+1");
}
// 使用数币复核员私钥对审核摘要原文进行SM2签名
String signature = cryptography.sign(storedPrivateKey, batch.getReviewConcatenatedMessage());
batch.signReview(signature);
repository.update(batch);
return new SalaryReviewSignatureResult(signature);
}
// ==================== 步骤十一:输出审核结果 ====================
@Transactional
public SalaryReviewOutputResult outputReview(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEW_SIGNED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM2签名");
}
if (batch.getReviewOutputJson() != null) {
return new SalaryReviewOutputResult(batch.getReviewOutputJson());
}
String json = buildReviewOutputJson(batch);
batch.outputReview(json);
repository.update(batch);
return new SalaryReviewOutputResult(json);
}
@Transactional(readOnly = true)
public SalaryPageResult pageQuery(InstitutionKeySubject subject) {
@ -203,14 +372,123 @@ public class SalaryBatchService {
if (batch == null) {
return new SalaryPageResult(payroll, totalAmount, employeeCount,
null, null, null, null, null, null, null, null, null);
null, null, null, null, null, null, null, null, null,
null, null, null, null, null, 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);
batch.getStatus() != null ? batch.getStatus().name() : null,
batch.getReviewConcatenatedMessage(), batch.getReviewerName(),
batch.getSingleLimitStatus(), batch.getBatchLimitStatus(), batch.getDailyLimitStatus(),
batch.getReviewResult(), batch.getReviewDigestValue(),
batch.getReviewSignature(), batch.getReviewOutputJson(),
batch.getConfirmMessage(), batch.getConfirmTimestamp(),
batch.getConfirmDigestValue(), batch.getCbSignature(),
batch.getConfirmOutputJson());
}
// ==================== 步骤十二:提取交易确认原文 ====================
@Transactional
public SalaryConfirmExtractResult extractConfirmText(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEWED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成复核员审核步骤");
}
if (batch.getConfirmMessage() != null) {
return new SalaryConfirmExtractResult(batch.getConfirmMessage(),
batch.getBatchId(), calculateTotalAmount(), EMPLOYEE_DATA.length,
batch.getConfirmTimestamp());
}
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
String totalAmount = calculateTotalAmount();
int employeeCount = EMPLOYEE_DATA.length;
// 拼接确认原文CONFIRM|批次号|总金额|人数|时间戳
String confirmMessage = "CONFIRM|" + batch.getBatchId() + "|"
+ totalAmount + "|" + employeeCount + "|" + timestamp;
batch.extractConfirmText(timestamp, confirmMessage);
repository.update(batch);
return new SalaryConfirmExtractResult(confirmMessage, batch.getBatchId(),
totalAmount, employeeCount, timestamp);
}
// ==================== 步骤十三:生成交易确认摘要 ====================
@Transactional
public SalaryConfirmDigestResult computeConfirmDigest(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.CONFIRM_EXTRACTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先提取交易确认原文");
}
if (batch.getConfirmDigestValue() != null) {
return new SalaryConfirmDigestResult(batch.getConfirmDigestValue());
}
String digest = cryptography.sm3(batch.getConfirmMessage());
batch.computeConfirmDigest("SM3", digest);
repository.update(batch);
return new SalaryConfirmDigestResult(digest);
}
// ==================== 步骤十四:央行第一私钥签名 ====================
@Transactional
public SalaryConfirmSignatureResult signConfirm(InstitutionKeySubject subject, String providedPrivateKey) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.CONFIRM_DIGESTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成交易确认摘要");
}
if (batch.getCbSignature() != null) {
return new SalaryConfirmSignatureResult(batch.getCbSignature());
}
// 从 institution_key_service 获取央行第一私钥
String storedPrivateKey = institutionKeyService.centralBankFirstPrivateKeyOrNull(subject);
if (storedPrivateKey == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "央行第一私钥不存在,请先生成央行密钥对");
}
// 验证传入的私钥是否正确
if (!storedPrivateKey.equalsIgnoreCase(providedPrivateKey != null ? providedPrivateKey.trim() : "")) {
scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "央行第一私钥不正确,错误次数+1");
}
// 使用央行第一私钥对确认原文进行SM2签名并添加CB_FINAL_前缀
String signature = cryptography.sign(storedPrivateKey, batch.getConfirmMessage());
String cbSignature = "CB_FINAL_A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0" + signature;
batch.signConfirm(cbSignature);
repository.update(batch);
return new SalaryConfirmSignatureResult(cbSignature);
}
// ==================== 步骤十五:输出交易确认结果 ====================
@Transactional
public SalaryConfirmOutputResult outputConfirm(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.CONFIRM_SIGNED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成央行签名");
}
if (batch.getConfirmOutputJson() != null) {
return new SalaryConfirmOutputResult(batch.getConfirmOutputJson());
}
String json = buildConfirmOutputJson(batch);
batch.outputConfirm(json);
repository.update(batch);
return new SalaryConfirmOutputResult(json);
}
// ==================== 内部方法 ====================
@ -264,6 +542,115 @@ public class SalaryBatchService {
return entity == null ? null : entity.getKeyValue();
}
/**
* corporate_wallet_key_info
*/
private String findReviewerPrivateKey(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, REVIEWER_KEY_TYPE)
.eq(CorporateWalletKeyInfoEntity::getDeleted, false)
.orderByDesc(CorporateWalletKeyInfoEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? null : entity.getKeyValue();
}
/**
* +
*/
private BigDecimal calculateDailyTotal(InstitutionKeySubject subject, BigDecimal currentBatchTotal) {
// 简化处理:当前批次金额即为当日累计(教学仿真场景)
// 如需精确统计,可扩展 repository 添加按日期查询方法
return currentBatchTotal;
}
/**
*
*/
private SalaryReviewCheckResult buildCheckResult(SalaryBatch batch) {
BigDecimal maxSingleAmount = BigDecimal.ZERO;
for (String[] emp : EMPLOYEE_DATA) {
BigDecimal amount = new BigDecimal(emp[3]);
if (amount.compareTo(maxSingleAmount) > 0) {
maxSingleAmount = amount;
}
}
BigDecimal batchTotal = new BigDecimal(calculateTotalAmount());
BigDecimal dailyTotal = calculateDailyTotal(
new InstitutionKeySubject(batch.getUserId(), batch.getSchoolId(), batch.getClassId()), batchTotal);
String singleDesc = "单笔最高金额 " + maxSingleAmount.setScale(2).toPlainString() + " 元 "
+ (batch.getSingleLimitStatus() == STATUS_PASSED ? "≤" : ">")
+ " " + SINGLE_LIMIT.setScale(2).toPlainString() + " 元";
String batchDesc = "批次总金额 " + batchTotal.setScale(2).toPlainString() + " 元 "
+ (batch.getBatchLimitStatus() == STATUS_PASSED ? "≤" : ">")
+ " " + BATCH_LIMIT.setScale(2).toPlainString() + " 元";
String dailyDesc = "当日累计发放金额 " + dailyTotal.setScale(2).toPlainString() + " 元 "
+ (batch.getDailyLimitStatus() == STATUS_PASSED ? "≤" : ">")
+ " " + DAILY_LIMIT.setScale(2).toPlainString() + " 元";
return new SalaryReviewCheckResult(
batch.getSingleLimitStatus(), SINGLE_LIMIT.setScale(2).toPlainString(),
maxSingleAmount.setScale(2).toPlainString(), singleDesc,
batch.getBatchLimitStatus(), BATCH_LIMIT.setScale(2).toPlainString(),
batchTotal.setScale(2).toPlainString(), batchDesc,
batch.getDailyLimitStatus(), DAILY_LIMIT.setScale(2).toPlainString(),
dailyTotal.setScale(2).toPlainString(), dailyDesc,
batch.getReviewResult());
}
/**
* JSON
*/
private String buildReviewOutputJson(SalaryBatch batch) {
BigDecimal maxSingleAmount = BigDecimal.ZERO;
for (String[] emp : EMPLOYEE_DATA) {
BigDecimal amount = new BigDecimal(emp[3]);
if (amount.compareTo(maxSingleAmount) > 0) {
maxSingleAmount = amount;
}
}
BigDecimal batchTotal = new BigDecimal(calculateTotalAmount());
BigDecimal dailyTotal = calculateDailyTotal(
new InstitutionKeySubject(batch.getUserId(), batch.getSchoolId(), batch.getClassId()), batchTotal);
String singleResult = batch.getSingleLimitStatus() == STATUS_PASSED ? "PASSED" : "FAILED";
String batchResult = batch.getBatchLimitStatus() == STATUS_PASSED ? "PASSED" : "FAILED";
String dailyResult = batch.getDailyLimitStatus() == STATUS_PASSED ? "PASSED" : "FAILED";
String statusText = "APPROVED".equals(batch.getReviewResult()) ? "已批准" : "已拒绝";
StringBuilder sb = new StringBuilder();
sb.append("{\n");
sb.append(" \"batchId\": \"").append(batch.getBatchId()).append("\",\n");
sb.append(" \"reviewer\": \"").append(batch.getReviewerName()).append("\",\n");
sb.append(" \"reviewResult\": \"").append(batch.getReviewResult()).append("\",\n");
sb.append(" \"permissionCheck\": {\n");
sb.append(" \"singleLimit\": {\n");
sb.append(" \"limit\": \"").append(SINGLE_LIMIT.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"actual\": \"").append(maxSingleAmount.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"result\": \"").append(singleResult).append("\"\n");
sb.append(" },\n");
sb.append(" \"batchLimit\": {\n");
sb.append(" \"limit\": \"").append(BATCH_LIMIT.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"actual\": \"").append(batchTotal.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"result\": \"").append(batchResult).append("\"\n");
sb.append(" },\n");
sb.append(" \"dailyLimit\": {\n");
sb.append(" \"limit\": \"").append(DAILY_LIMIT.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"actual\": \"").append(dailyTotal.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"result\": \"").append(dailyResult).append("\"\n");
sb.append(" }\n");
sb.append(" },\n");
sb.append(" \"reviewerSignature\": \"").append(batch.getReviewSignature()).append("\",\n");
sb.append(" \"reviewTime\": \"").append(batch.getReviewTimestamp()).append("\",\n");
sb.append(" \"status\": \"").append(statusText).append("\"\n");
sb.append("}");
return sb.toString();
}
private String generateBatchId() {
String datePart = LocalDateTime.now().format(DATE_FORMAT);
int seq = ThreadLocalRandom.current().nextInt(100, 1000);
@ -315,4 +702,21 @@ public class SalaryBatchService {
}
return String.format("%.2f", total);
}
/**
* JSON
*/
private String buildConfirmOutputJson(SalaryBatch batch) {
StringBuilder sb = new StringBuilder();
sb.append("{\n");
sb.append(" \"batchId\": \"").append(batch.getBatchId()).append("\",\n");
sb.append(" \"totalAmount\": \"").append(calculateTotalAmount()).append("\",\n");
sb.append(" \"employeeCount\": ").append(EMPLOYEE_DATA.length).append(",\n");
sb.append(" \"digest\": \"").append(batch.getConfirmDigestValue()).append("\",\n");
sb.append(" \"cbSignature\": \"").append(batch.getCbSignature()).append("\",\n");
sb.append(" \"confirmTime\": \"").append(batch.getConfirmTimestamp()).append("\",\n");
sb.append(" \"status\": \"COMPLETED\"\n");
sb.append("}");
return sb.toString();
}
}

@ -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 SalaryConfirmDigestResult {
@Schema(description = "摘要值(十六进制大写)")
private final String digestValue;
public SalaryConfirmDigestResult(String digestValue) {
this.digestValue = digestValue;
}
}

@ -0,0 +1,28 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "提取交易确认原文结果")
public class SalaryConfirmExtractResult {
@Schema(description = "确认原文管道分隔格式CONFIRM|批次号|总金额|人数|时间戳")
private final String confirmMessage;
@Schema(description = "批次号")
private final String batchId;
@Schema(description = "工资金额汇总(元)")
private final String totalAmount;
@Schema(description = "员工人数")
private final int employeeCount;
@Schema(description = "确认时间戳")
private final String timestamp;
public SalaryConfirmExtractResult(String confirmMessage, String batchId,
String totalAmount, int employeeCount, String timestamp) {
this.confirmMessage = confirmMessage;
this.batchId = batchId;
this.totalAmount = totalAmount;
this.employeeCount = employeeCount;
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 = "交易确认输出JSON结果")
public class SalaryConfirmOutputResult {
@Schema(description = "输出JSON报文")
private final String outputJson;
public SalaryConfirmOutputResult(String outputJson) {
this.outputJson = outputJson;
}
}

@ -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 SalaryConfirmSignatureResult {
@Schema(description = "央行签名值CB_FINAL_前缀+十六进制)")
private final String cbSignature;
public SalaryConfirmSignatureResult(String cbSignature) {
this.cbSignature = cbSignature;
}
}

@ -32,11 +32,45 @@ public class SalaryPageResult {
private final String outputJson;
@Schema(description = "状态(若已发送)")
private final String status;
@Schema(description = "审核原文(若已提取)")
private final String reviewMessage;
@Schema(description = "复核员姓名(若已提取)")
private final String reviewerName;
@Schema(description = "单笔限额状态1-通过, 2-未通过(若已校验)")
private final Integer singleLimitStatus;
@Schema(description = "批次限额状态1-通过, 2-未通过(若已校验)")
private final Integer batchLimitStatus;
@Schema(description = "日累计限额状态1-通过, 2-未通过(若已校验)")
private final Integer dailyLimitStatus;
@Schema(description = "审核结果APPROVED-已批准, REJECTED-已拒绝(若已校验)")
private final String reviewResult;
@Schema(description = "审核摘要值(若已生成)")
private final String reviewDigestValue;
@Schema(description = "审核签名值(若已签名)")
private final String reviewSignature;
@Schema(description = "审核输出JSON若已输出")
private final String reviewOutputJson;
@Schema(description = "交易确认原文(若已提取)")
private final String confirmMessage;
@Schema(description = "确认时间戳(若已提取)")
private final String confirmTimestamp;
@Schema(description = "交易确认摘要值(若已生成)")
private final String confirmDigestValue;
@Schema(description = "央行签名值(若已签名)")
private final String cbSignature;
@Schema(description = "交易确认输出JSON若已输出")
private final String confirmOutputJson;
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) {
String outputJson, String status,
String reviewMessage, String reviewerName,
Integer singleLimitStatus, Integer batchLimitStatus, Integer dailyLimitStatus,
String reviewResult, String reviewDigestValue, String reviewSignature,
String reviewOutputJson,
String confirmMessage, String confirmTimestamp,
String confirmDigestValue, String cbSignature, String confirmOutputJson) {
this.payroll = payroll;
this.totalAmount = totalAmount;
this.employeeCount = employeeCount;
@ -49,6 +83,20 @@ public class SalaryPageResult {
this.signature = signature;
this.outputJson = outputJson;
this.status = status;
this.reviewMessage = reviewMessage;
this.reviewerName = reviewerName;
this.singleLimitStatus = singleLimitStatus;
this.batchLimitStatus = batchLimitStatus;
this.dailyLimitStatus = dailyLimitStatus;
this.reviewResult = reviewResult;
this.reviewDigestValue = reviewDigestValue;
this.reviewSignature = reviewSignature;
this.reviewOutputJson = reviewOutputJson;
this.confirmMessage = confirmMessage;
this.confirmTimestamp = confirmTimestamp;
this.confirmDigestValue = confirmDigestValue;
this.cbSignature = cbSignature;
this.confirmOutputJson = confirmOutputJson;
}
@Getter

@ -0,0 +1,57 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "审核与权限校验结果")
public class SalaryReviewCheckResult {
@Schema(description = "单笔限额校验1-通过, 2-未通过")
private final int singleLimitStatus;
@Schema(description = "单笔限额上限(元)")
private final String singleLimit;
@Schema(description = "单笔最高金额(元)")
private final String singleActual;
@Schema(description = "单笔限额说明")
private final String singleLimitDesc;
@Schema(description = "批次限额校验1-通过, 2-未通过")
private final int batchLimitStatus;
@Schema(description = "批次限额上限(元)")
private final String batchLimit;
@Schema(description = "批次总金额(元)")
private final String batchActual;
@Schema(description = "批次限额说明")
private final String batchLimitDesc;
@Schema(description = "日累计限额校验1-通过, 2-未通过")
private final int dailyLimitStatus;
@Schema(description = "日累计限额上限(元)")
private final String dailyLimit;
@Schema(description = "当日累计发放金额(元)")
private final String dailyActual;
@Schema(description = "日累计限额说明")
private final String dailyLimitDesc;
@Schema(description = "审核结果APPROVED-已批准, REJECTED-已拒绝")
private final String reviewResult;
public SalaryReviewCheckResult(int singleLimitStatus, String singleLimit, String singleActual, String singleLimitDesc,
int batchLimitStatus, String batchLimit, String batchActual, String batchLimitDesc,
int dailyLimitStatus, String dailyLimit, String dailyActual, String dailyLimitDesc,
String reviewResult) {
this.singleLimitStatus = singleLimitStatus;
this.singleLimit = singleLimit;
this.singleActual = singleActual;
this.singleLimitDesc = singleLimitDesc;
this.batchLimitStatus = batchLimitStatus;
this.batchLimit = batchLimit;
this.batchActual = batchActual;
this.batchLimitDesc = batchLimitDesc;
this.dailyLimitStatus = dailyLimitStatus;
this.dailyLimit = dailyLimit;
this.dailyActual = dailyActual;
this.dailyLimitDesc = dailyLimitDesc;
this.reviewResult = reviewResult;
}
}

@ -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 SalaryReviewDigestResult {
@Schema(description = "摘要值(十六进制大写)")
private final String digestValue;
public SalaryReviewDigestResult(String digestValue) {
this.digestValue = digestValue;
}
}

@ -0,0 +1,31 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "提取审核原文结果")
public class SalaryReviewExtractResult {
@Schema(description = "审核原文管道分隔格式REVIEW|批次号|复核员|总金额|人数|时间戳")
private final String reviewMessage;
@Schema(description = "批次号")
private final String batchId;
@Schema(description = "复核员姓名")
private final String reviewer;
@Schema(description = "工资金额汇总(元)")
private final String totalAmount;
@Schema(description = "员工人数")
private final int employeeCount;
@Schema(description = "审核时间戳")
private final String timestamp;
public SalaryReviewExtractResult(String reviewMessage, String batchId, String reviewer,
String totalAmount, int employeeCount, String timestamp) {
this.reviewMessage = reviewMessage;
this.batchId = batchId;
this.reviewer = reviewer;
this.totalAmount = totalAmount;
this.employeeCount = employeeCount;
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 = "审核输出JSON结果")
public class SalaryReviewOutputResult {
@Schema(description = "输出JSON报文")
private final String outputJson;
public SalaryReviewOutputResult(String outputJson) {
this.outputJson = outputJson;
}
}

@ -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 SalaryReviewSignatureResult {
@Schema(description = "签名值(十六进制)")
private final String signature;
public SalaryReviewSignatureResult(String signature) {
this.signature = signature;
}
}

@ -2,7 +2,9 @@ package com.yau.digitalrmb.corporatewallet.domain;
public class SalaryBatch {
public enum Status {
DRAFT, BLOCKCHAIN_FETCHED, CONCATENATED, DIGESTED, SIGNED, OUTPUT, SENT
DRAFT, BLOCKCHAIN_FETCHED, CONCATENATED, DIGESTED, SIGNED, OUTPUT, SENT,
REVIEW_EXTRACTED, REVIEW_CHECKED, REVIEW_DIGESTED, REVIEW_SIGNED, REVIEWED,
CONFIRM_EXTRACTED, CONFIRM_DIGESTED, CONFIRM_SIGNED, CONFIRM_COMPLETED
}
private Long id;
@ -33,6 +35,27 @@ public class SalaryBatch {
private Status status;
private String submitTime;
// 复核员审核相关
private String reviewerName;
private String reviewTimestamp;
private String reviewConcatenatedMessage;
private String reviewDigestAlgorithm;
private String reviewDigestValue;
private String reviewSignature;
private String reviewOutputJson;
private Integer singleLimitStatus;
private Integer batchLimitStatus;
private Integer dailyLimitStatus;
private String reviewResult;
// 交易确认相关
private String confirmMessage;
private String confirmTimestamp;
private String confirmDigestAlgorithm;
private String confirmDigestValue;
private String cbSignature;
private String confirmOutputJson;
public static SalaryBatch create(String userId, long schoolId, long classId) {
SalaryBatch batch = new SalaryBatch();
batch.userId = userId;
@ -76,6 +99,59 @@ public class SalaryBatch {
this.status = Status.SENT;
}
public void extractReviewText(String reviewerName, String reviewTimestamp, String reviewMessage) {
this.reviewerName = reviewerName;
this.reviewTimestamp = reviewTimestamp;
this.reviewConcatenatedMessage = reviewMessage;
this.status = Status.REVIEW_EXTRACTED;
}
public void checkPermission(Integer singleLimitStatus, Integer batchLimitStatus, Integer dailyLimitStatus, String reviewResult) {
this.singleLimitStatus = singleLimitStatus;
this.batchLimitStatus = batchLimitStatus;
this.dailyLimitStatus = dailyLimitStatus;
this.reviewResult = reviewResult;
this.status = Status.REVIEW_CHECKED;
}
public void computeReviewDigest(String algorithm, String digest) {
this.reviewDigestAlgorithm = algorithm;
this.reviewDigestValue = digest;
this.status = Status.REVIEW_DIGESTED;
}
public void signReview(String signature) {
this.reviewSignature = signature;
this.status = Status.REVIEW_SIGNED;
}
public void outputReview(String outputJson) {
this.reviewOutputJson = outputJson;
this.status = Status.REVIEWED;
}
public void extractConfirmText(String confirmTimestamp, String confirmMessage) {
this.confirmTimestamp = confirmTimestamp;
this.confirmMessage = confirmMessage;
this.status = Status.CONFIRM_EXTRACTED;
}
public void computeConfirmDigest(String algorithm, String digest) {
this.confirmDigestAlgorithm = algorithm;
this.confirmDigestValue = digest;
this.status = Status.CONFIRM_DIGESTED;
}
public void signConfirm(String cbSignature) {
this.cbSignature = cbSignature;
this.status = Status.CONFIRM_SIGNED;
}
public void outputConfirm(String outputJson) {
this.confirmOutputJson = outputJson;
this.status = Status.CONFIRM_COMPLETED;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@ -107,4 +183,38 @@ public class SalaryBatch {
public void setStatus(Status status) { this.status = status; }
public String getSubmitTime() { return submitTime; }
public void setSubmitTime(String submitTime) { this.submitTime = submitTime; }
public String getReviewerName() { return reviewerName; }
public void setReviewerName(String reviewerName) { this.reviewerName = reviewerName; }
public String getReviewTimestamp() { return reviewTimestamp; }
public void setReviewTimestamp(String reviewTimestamp) { this.reviewTimestamp = reviewTimestamp; }
public String getReviewConcatenatedMessage() { return reviewConcatenatedMessage; }
public void setReviewConcatenatedMessage(String reviewConcatenatedMessage) { this.reviewConcatenatedMessage = reviewConcatenatedMessage; }
public String getReviewDigestAlgorithm() { return reviewDigestAlgorithm; }
public void setReviewDigestAlgorithm(String reviewDigestAlgorithm) { this.reviewDigestAlgorithm = reviewDigestAlgorithm; }
public String getReviewDigestValue() { return reviewDigestValue; }
public void setReviewDigestValue(String reviewDigestValue) { this.reviewDigestValue = reviewDigestValue; }
public String getReviewSignature() { return reviewSignature; }
public void setReviewSignature(String reviewSignature) { this.reviewSignature = reviewSignature; }
public String getReviewOutputJson() { return reviewOutputJson; }
public void setReviewOutputJson(String reviewOutputJson) { this.reviewOutputJson = reviewOutputJson; }
public Integer getSingleLimitStatus() { return singleLimitStatus; }
public void setSingleLimitStatus(Integer singleLimitStatus) { this.singleLimitStatus = singleLimitStatus; }
public Integer getBatchLimitStatus() { return batchLimitStatus; }
public void setBatchLimitStatus(Integer batchLimitStatus) { this.batchLimitStatus = batchLimitStatus; }
public Integer getDailyLimitStatus() { return dailyLimitStatus; }
public void setDailyLimitStatus(Integer dailyLimitStatus) { this.dailyLimitStatus = dailyLimitStatus; }
public String getReviewResult() { return reviewResult; }
public void setReviewResult(String reviewResult) { this.reviewResult = reviewResult; }
public String getConfirmMessage() { return confirmMessage; }
public void setConfirmMessage(String confirmMessage) { this.confirmMessage = confirmMessage; }
public String getConfirmTimestamp() { return confirmTimestamp; }
public void setConfirmTimestamp(String confirmTimestamp) { this.confirmTimestamp = confirmTimestamp; }
public String getConfirmDigestAlgorithm() { return confirmDigestAlgorithm; }
public void setConfirmDigestAlgorithm(String confirmDigestAlgorithm) { this.confirmDigestAlgorithm = confirmDigestAlgorithm; }
public String getConfirmDigestValue() { return confirmDigestValue; }
public void setConfirmDigestValue(String confirmDigestValue) { this.confirmDigestValue = confirmDigestValue; }
public String getCbSignature() { return cbSignature; }
public void setCbSignature(String cbSignature) { this.cbSignature = cbSignature; }
public String getConfirmOutputJson() { return confirmOutputJson; }
public void setConfirmOutputJson(String confirmOutputJson) { this.confirmOutputJson = confirmOutputJson; }
}

@ -56,6 +56,23 @@ public class MybatisSalaryBatchRepository implements SalaryBatchRepository {
entity.setOutputJson(batch.getOutputJson());
entity.setStatus(batch.getStatus() == null ? null : batch.getStatus().name());
entity.setSubmitTime(batch.getSubmitTime());
entity.setReviewerName(batch.getReviewerName());
entity.setReviewTimestamp(batch.getReviewTimestamp());
entity.setReviewConcatenatedMessage(batch.getReviewConcatenatedMessage());
entity.setReviewDigestAlgorithm(batch.getReviewDigestAlgorithm());
entity.setReviewDigestValue(batch.getReviewDigestValue());
entity.setReviewSignature(batch.getReviewSignature());
entity.setReviewOutputJson(batch.getReviewOutputJson());
entity.setSingleLimitStatus(batch.getSingleLimitStatus());
entity.setBatchLimitStatus(batch.getBatchLimitStatus());
entity.setDailyLimitStatus(batch.getDailyLimitStatus());
entity.setReviewResult(batch.getReviewResult());
entity.setConfirmMessage(batch.getConfirmMessage());
entity.setConfirmTimestamp(batch.getConfirmTimestamp());
entity.setConfirmDigestAlgorithm(batch.getConfirmDigestAlgorithm());
entity.setConfirmDigestValue(batch.getConfirmDigestValue());
entity.setCbSignature(batch.getCbSignature());
entity.setConfirmOutputJson(batch.getConfirmOutputJson());
return entity;
}
@ -76,6 +93,23 @@ public class MybatisSalaryBatchRepository implements SalaryBatchRepository {
batch.setOutputJson(entity.getOutputJson());
batch.setStatus(entity.getStatus() == null ? null : SalaryBatch.Status.valueOf(entity.getStatus()));
batch.setSubmitTime(entity.getSubmitTime());
batch.setReviewerName(entity.getReviewerName());
batch.setReviewTimestamp(entity.getReviewTimestamp());
batch.setReviewConcatenatedMessage(entity.getReviewConcatenatedMessage());
batch.setReviewDigestAlgorithm(entity.getReviewDigestAlgorithm());
batch.setReviewDigestValue(entity.getReviewDigestValue());
batch.setReviewSignature(entity.getReviewSignature());
batch.setReviewOutputJson(entity.getReviewOutputJson());
batch.setSingleLimitStatus(entity.getSingleLimitStatus());
batch.setBatchLimitStatus(entity.getBatchLimitStatus());
batch.setDailyLimitStatus(entity.getDailyLimitStatus());
batch.setReviewResult(entity.getReviewResult());
batch.setConfirmMessage(entity.getConfirmMessage());
batch.setConfirmTimestamp(entity.getConfirmTimestamp());
batch.setConfirmDigestAlgorithm(entity.getConfirmDigestAlgorithm());
batch.setConfirmDigestValue(entity.getConfirmDigestValue());
batch.setCbSignature(entity.getCbSignature());
batch.setConfirmOutputJson(entity.getConfirmOutputJson());
return batch;
}
}

@ -38,4 +38,42 @@ public class SalaryBatchEntity extends AuditableEntity {
private String status;
@TableField("submit_time")
private String submitTime;
// 复核员审核相关
@TableField("reviewer_name")
private String reviewerName;
@TableField("review_timestamp")
private String reviewTimestamp;
@TableField("review_concatenated_message")
private String reviewConcatenatedMessage;
@TableField("review_digest_algorithm")
private String reviewDigestAlgorithm;
@TableField("review_digest_value")
private String reviewDigestValue;
@TableField("review_signature")
private String reviewSignature;
@TableField("review_output_json")
private String reviewOutputJson;
@TableField("single_limit_status")
private Integer singleLimitStatus;
@TableField("batch_limit_status")
private Integer batchLimitStatus;
@TableField("daily_limit_status")
private Integer dailyLimitStatus;
@TableField("review_result")
private String reviewResult;
// 交易确认相关
@TableField("confirm_message")
private String confirmMessage;
@TableField("confirm_timestamp")
private String confirmTimestamp;
@TableField("confirm_digest_algorithm")
private String confirmDigestAlgorithm;
@TableField("confirm_digest_value")
private String confirmDigestValue;
@TableField("cb_signature")
private String cbSignature;
@TableField("confirm_output_json")
private String confirmOutputJson;
}

@ -0,0 +1,63 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.corporatewallet.application.SalaryBatchService;
import com.yau.digitalrmb.corporatewallet.application.SalaryConfirmDigestResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryConfirmExtractResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryConfirmOutputResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryConfirmSignatureResult;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.SalaryConfirmSignRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter;
import 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/confirm")
@Tag(name = "模块六-企业发放数字人民币工资 - 步骤3交易确认与工资发放", description = "提取交易确认原文、生成摘要、央行签名并输出全流程")
public class SalaryConfirmController {
@Resource
private SalaryBatchService salaryBatchService;
@PostMapping("/extract")
@Operation(summary = "提取交易确认原文",
description = "从已审核的工资批次中提取交易确认原文格式CONFIRM|批次号|总金额|人数|时间戳")
public ApiResponse<SalaryConfirmExtractResult> extractConfirmText() {
return ApiResponse.success(salaryBatchService.extractConfirmText(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/digest")
@Operation(summary = "生成交易确认摘要",
description = "对拼接交易确认原文进行SM3运算只返回摘要值")
public ApiResponse<SalaryConfirmDigestResult> computeConfirmDigest() {
return ApiResponse.success(salaryBatchService.computeConfirmDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/sign")
@Operation(summary = "央行第一私钥签名",
description = "传入央行第一私钥进行SM2签名私钥不正确则错误数+1返回带CB_FINAL_前缀的签名值")
public ApiResponse<SalaryConfirmSignatureResult> signConfirm(@Valid @RequestBody SalaryConfirmSignRequest request) {
return ApiResponse.success(salaryBatchService.signConfirm(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey()), traceId());
}
@PostMapping("/output")
@Operation(summary = "输出交易确认结果",
description = "生成包含批次号、总金额、摘要、央行签名等信息的JSON报文状态为COMPLETED")
public ApiResponse<SalaryConfirmOutputResult> outputConfirm() {
return ApiResponse.success(salaryBatchService.outputConfirm(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -0,0 +1,72 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.corporatewallet.application.SalaryBatchService;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewCheckResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewDigestResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewExtractResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewOutputResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewSignatureResult;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.SalaryReviewSignRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter;
import 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/review")
@Tag(name = "模块六-企业发放数字人民币工资 - 步骤2数币复核员审核与权限审批", description = "数币复核员提取审核原文、权限校验、摘要、签名并输出全流程")
public class SalaryReviewController {
@Resource
private SalaryBatchService salaryBatchService;
@PostMapping("/extract")
@Operation(summary = "提取审核原文",
description = "从已发送的工资批次中提取审核原文格式REVIEW|批次号|复核员|总金额|人数|时间戳")
public ApiResponse<SalaryReviewExtractResult> extractReviewText() {
return ApiResponse.success(salaryBatchService.extractReviewText(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/check")
@Operation(summary = "审核与权限校验",
description = "校验单笔限额(≤50000元)、批次限额(≤500000元)、日累计限额(≤1000000元)返回1-通过/2-未通过")
public ApiResponse<SalaryReviewCheckResult> checkPermission() {
return ApiResponse.success(salaryBatchService.checkPermission(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/digest")
@Operation(summary = "生成审核摘要",
description = "对拼接审核原文进行SM3运算只返回摘要值")
public ApiResponse<SalaryReviewDigestResult> computeReviewDigest() {
return ApiResponse.success(salaryBatchService.computeReviewDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/sign")
@Operation(summary = "SM2签名复核员私钥",
description = "传入数币复核员私钥进行SM2签名私钥不正确则错误数+1只返回签名值")
public ApiResponse<SalaryReviewSignatureResult> signReview(@Valid @RequestBody SalaryReviewSignRequest request) {
return ApiResponse.success(salaryBatchService.signReview(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey()), traceId());
}
@PostMapping("/output")
@Operation(summary = "输出审核结果",
description = "生成包含批次号、复核员、权限校验、签名等信息的JSON报文")
public ApiResponse<SalaryReviewOutputResult> outputReview() {
return ApiResponse.success(salaryBatchService.outputReview(
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 = "交易确认央行签名请求")
public class SalaryConfirmSignRequest {
@NotBlank(message = "央行第一私钥不能为空")
@Schema(description = "央行第一私钥(十六进制)", required = true)
private String privateKey;
}

@ -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 SalaryReviewSignRequest {
@NotBlank(message = "数币复核员私钥不能为空")
@Schema(description = "数币复核员私钥(十六进制)", required = true)
private String privateKey;
}

@ -0,0 +1,64 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
@Getter
@Schema(description = "币串ID生成规则")
public class CurrencyIdRuleResult {
@Schema(description = "DC前缀", example = "DC")
private final String prefix;
@Schema(description = "币串ID生成规则", example = "批次号+枚序号+随机校验码")
private final String idRule;
@Schema(description = "当前批次号", example = "20260819_001")
private final String batchNumber;
@Schema(description = "币串总枚数", example = "28")
private final Integer totalCount;
@Schema(description = "操作状态", example = "RULE_CONFIRMED")
private final String status;
@Schema(description = "币串ID规则明细")
private final List<RuleItem> ruleItems;
public CurrencyIdRuleResult(String prefix, String idRule, String batchNumber,
Integer totalCount, String status) {
this.prefix = prefix;
this.idRule = idRule;
this.batchNumber = batchNumber;
this.totalCount = totalCount;
this.status = status;
this.ruleItems = buildRuleItems(batchNumber, totalCount);
}
private List<RuleItem> buildRuleItems(String batchNumber, Integer totalCount) {
String lastSeq = totalCount != null ? String.format("%03d", totalCount) : "028";
String serialExample = "001、002 ... " + lastSeq;
String checkCodeExample = "A1B2C3";
String fullIdExample = "DC_" + batchNumber + "_001_" + checkCodeExample;
return Arrays.asList(
new RuleItem("批次号", batchNumber, "年月日_当日批次序号"),
new RuleItem("枚序号", serialExample, "该批次内的唯一序号"),
new RuleItem("随机校验码", checkCodeExample, "6位随机字符用于防碰撞和校验"),
new RuleItem("完整币串ID", fullIdExample, "DC_前缀 + 批次号 + 枚序号 + 校验码")
);
}
@Getter
@Schema(description = "币串ID规则明细项")
public static class RuleItem {
@Schema(description = "字段名", example = "批次号")
private final String field;
@Schema(description = "示例", example = "20260819_001")
private final String example;
@Schema(description = "说明", example = "年月日_当日批次序号")
private final String description;
public RuleItem(String field, String example, String description) {
this.field = field;
this.example = example;
this.description = description;
}
}
}

@ -0,0 +1,76 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
@Getter
@Schema(description = "标准面额币串规则")
public class StandardCurrencyRuleResult {
@Schema(description = "DC前缀", example = "DC")
private final String prefix;
@Schema(description = "币串初始状态", example = "待生效")
private final String currencyStatus;
@Schema(description = "当前批次号", example = "20260819_001")
private final String batchNumber;
@Schema(description = "币串总枚数", example = "28")
private final Integer totalCount;
@Schema(description = "操作状态", example = "RULE_CONFIRMED")
private final String status;
@Schema(description = "标准面额币串规则明细以第1枚币串为例")
private final List<RuleItem> ruleItems;
public StandardCurrencyRuleResult(String prefix, String currencyStatus, String batchNumber,
Integer totalCount, String status,
BigDecimal firstDenomination, String institutionIdentifier,
String bankSignature, String centralBankSignatureSegment) {
this.prefix = prefix;
this.currencyStatus = currencyStatus;
this.batchNumber = batchNumber;
this.totalCount = totalCount;
this.status = status;
this.ruleItems = buildRuleItems(prefix, batchNumber, firstDenomination,
institutionIdentifier, bankSignature, centralBankSignatureSegment, currencyStatus);
}
private List<RuleItem> buildRuleItems(String prefix, String batchNumber,
BigDecimal firstDenomination, String institutionIdentifier,
String bankSignature, String centralBankSignatureSegment,
String currencyStatus) {
String checkCodeExample = "A1B2C3";
String currencyIdExample = "DC_" + batchNumber + "_001_" + checkCodeExample;
String denominationStr = firstDenomination != null ? firstDenomination.setScale(2).toPlainString() : "100.00";
String orgId = institutionIdentifier != null ? institutionIdentifier : "ORG_3A4B5C6D7E8F";
String bankSig = bankSignature != null ? bankSignature : "SIG_8A7B9C2D4F1E3A5B7C9D2E4F6A8B1C3D5E7F8A9B0C1D2E3F4A5B6C7D8E9F0A";
String cbSig = centralBankSignatureSegment != null ? centralBankSignatureSegment : "CB_CTRL_9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7A8";
return Arrays.asList(
new RuleItem("前缀", prefix, "固定标识,代表数字货币"),
new RuleItem("币串ID", currencyIdExample, "包含批次号001和枚序号001确保全球唯一"),
new RuleItem("标准面额", denominationStr, "拆解后的标准面额,非总额"),
new RuleItem("机构标识", orgId, "关联发起机构,用于溯源"),
new RuleItem("银行签名段", bankSig, "复用步骤八的银行签名段"),
new RuleItem("央行签名段", cbSig, "复用步骤七的央行签名段"),
new RuleItem("币串状态", currencyStatus, "发行时为待生效,央行确权后变为可用")
);
}
@Getter
@Schema(description = "标准面额币串规则明细项")
public static class RuleItem {
@Schema(description = "币串字段", example = "前缀")
private final String field;
@Schema(description = "示例值以第1枚币串为例", example = "DC")
private final String example;
@Schema(description = "说明", example = "固定标识,代表数字货币")
private final String description;
public RuleItem(String field, String example, String description) {
this.field = field;
this.example = example;
this.description = description;
}
}
}

@ -25,9 +25,30 @@ CREATE TABLE IF NOT EXISTS salary_batch (
output_json TEXT COMMENT '输出JSON报文',
-- 状态
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态DRAFT-草稿, BLOCKCHAIN_FETCHED-已调取区块信息, CONCATENATED-已拼接, DIGESTED-已生成摘要, SIGNED-已签名, OUTPUT-已输出, SENT-已发送',
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态DRAFT-草稿, BLOCKCHAIN_FETCHED-已调取区块信息, CONCATENATED-已拼接, DIGESTED-已生成摘要, SIGNED-已签名, OUTPUT-已输出, SENT-已发送, REVIEW_EXTRACTED-已提取审核原文, REVIEW_CHECKED-已审核, REVIEW_DIGESTED-已生成审核摘要, REVIEW_SIGNED-已签名, REVIEWED-已审核完成, CONFIRM_EXTRACTED-已提取确认原文, CONFIRM_DIGESTED-已生成确认摘要, CONFIRM_SIGNED-已央行签名, CONFIRM_COMPLETED-交易确认完成',
submit_time VARCHAR(20) COMMENT '发送时间格式yyyyMMddHHmmss',
-- 复核员审核相关
reviewer_name VARCHAR(64) COMMENT '复核员姓名(如:赵六)',
review_timestamp VARCHAR(20) COMMENT '审核时间戳格式yyyyMMddHHmmss',
review_concatenated_message TEXT COMMENT '拼接审核原文(管道分隔)',
review_digest_algorithm VARCHAR(16) COMMENT '审核摘要算法SM3',
review_digest_value VARCHAR(128) COMMENT '审核SM3摘要值十六进制大写',
review_signature TEXT COMMENT '审核SM2签名值DER编码十六进制',
review_output_json TEXT COMMENT '审核输出JSON报文',
single_limit_status INT COMMENT '单笔限额状态1-通过, 2-未通过',
batch_limit_status INT COMMENT '批次限额状态1-通过, 2-未通过',
daily_limit_status INT COMMENT '日累计限额状态1-通过, 2-未通过',
review_result VARCHAR(32) COMMENT '审核结果APPROVED-已批准, REJECTED-已拒绝',
-- 交易确认相关
confirm_message TEXT COMMENT '交易确认原文(管道分隔)',
confirm_timestamp VARCHAR(20) COMMENT '确认时间戳格式yyyyMMddHHmmss',
confirm_digest_algorithm VARCHAR(16) COMMENT '确认摘要算法SM3',
confirm_digest_value VARCHAR(128) COMMENT '确认SM3摘要值十六进制大写',
cb_signature TEXT COMMENT '央行SM2签名值CB_FINAL_前缀+DER编码十六进制',
confirm_output_json TEXT COMMENT '交易确认输出JSON报文',
created_at TIMESTAMP NOT NULL COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL COMMENT '更新时间',
created_by VARCHAR(64) NOT NULL COMMENT '创建人',

@ -1239,8 +1239,25 @@ CREATE TABLE IF NOT EXISTS salary_batch (
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-已发送',
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态DRAFT-草稿, BLOCKCHAIN_FETCHED-已调取区块信息, CONCATENATED-已拼接, DIGESTED-已生成摘要, SIGNED-已签名, OUTPUT-已输出, SENT-已发送, REVIEW_EXTRACTED-已提取审核原文, REVIEW_CHECKED-已审核, REVIEW_DIGESTED-已生成审核摘要, REVIEW_SIGNED-已签名, REVIEWED-已审核完成, CONFIRM_EXTRACTED-已提取确认原文, CONFIRM_DIGESTED-已生成确认摘要, CONFIRM_SIGNED-已央行签名, CONFIRM_COMPLETED-交易确认完成',
submit_time VARCHAR(20) COMMENT '发送时间格式yyyyMMddHHmmss',
reviewer_name VARCHAR(64) COMMENT '复核员姓名(如:赵六)',
review_timestamp VARCHAR(20) COMMENT '审核时间戳格式yyyyMMddHHmmss',
review_concatenated_message TEXT COMMENT '拼接审核原文(管道分隔)',
review_digest_algorithm VARCHAR(16) COMMENT '审核摘要算法SM3',
review_digest_value VARCHAR(128) COMMENT '审核SM3摘要值十六进制大写',
review_signature TEXT COMMENT '审核SM2签名值DER编码十六进制',
review_output_json TEXT COMMENT '审核输出JSON报文',
single_limit_status INT COMMENT '单笔限额状态1-通过, 2-未通过',
batch_limit_status INT COMMENT '批次限额状态1-通过, 2-未通过',
daily_limit_status INT COMMENT '日累计限额状态1-通过, 2-未通过',
review_result VARCHAR(32) COMMENT '审核结果APPROVED-已批准, REJECTED-已拒绝',
confirm_message TEXT COMMENT '交易确认原文(管道分隔)',
confirm_timestamp VARCHAR(20) COMMENT '确认时间戳格式yyyyMMddHHmmss',
confirm_digest_algorithm VARCHAR(16) COMMENT '确认摘要算法SM3',
confirm_digest_value VARCHAR(128) COMMENT '确认SM3摘要值十六进制大写',
cb_signature TEXT COMMENT '央行SM2签名值CB_FINAL_前缀+DER编码十六进制',
confirm_output_json TEXT COMMENT '交易确认输出JSON报文',
created_at TIMESTAMP NOT NULL COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL COMMENT '更新时间',
created_by VARCHAR(64) NOT NULL COMMENT '创建人',

@ -810,6 +810,23 @@ CREATE TABLE IF NOT EXISTS salary_batch (
output_json TEXT COMMENT '输出JSON报文',
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态',
submit_time VARCHAR(20) COMMENT '发送时间',
reviewer_name VARCHAR(64) COMMENT '复核员姓名',
review_timestamp VARCHAR(20) COMMENT '审核时间戳',
review_concatenated_message TEXT COMMENT '拼接审核原文',
review_digest_algorithm VARCHAR(16) COMMENT '审核摘要算法',
review_digest_value VARCHAR(128) COMMENT '审核SM3摘要值',
review_signature TEXT COMMENT '审核SM2签名值',
review_output_json TEXT COMMENT '审核输出JSON',
single_limit_status INT COMMENT '单笔限额状态1-通过, 2-未通过',
batch_limit_status INT COMMENT '批次限额状态1-通过, 2-未通过',
daily_limit_status INT COMMENT '日累计限额状态1-通过, 2-未通过',
review_result VARCHAR(32) COMMENT '审核结果APPROVED-已批准, REJECTED-已拒绝',
confirm_message TEXT COMMENT '交易确认原文',
confirm_timestamp VARCHAR(20) COMMENT '确认时间戳',
confirm_digest_algorithm VARCHAR(16) COMMENT '确认摘要算法',
confirm_digest_value VARCHAR(128) COMMENT '确认SM3摘要值',
cb_signature TEXT COMMENT '央行SM2签名值',
confirm_output_json TEXT COMMENT '交易确认输出JSON',
created_at TIMESTAMP NOT NULL COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL COMMENT '更新时间',
created_by VARCHAR(64) NOT NULL COMMENT '创建人',

Loading…
Cancel
Save