From 146a494f7c9f444bc9a5e9a45fca48f6d247de74 Mon Sep 17 00:00:00 2001 From: "jiazheng.zhao" <2624533894@qq.com> Date: Thu, 20 Aug 2026 13:18:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/SalaryBatchService.java | 410 +++++++++++++++++- .../SalaryConfirmDigestResult.java | 15 + .../SalaryConfirmExtractResult.java | 28 ++ .../SalaryConfirmOutputResult.java | 15 + .../SalaryConfirmSignatureResult.java | 15 + .../application/SalaryPageResult.java | 50 ++- .../application/SalaryReviewCheckResult.java | 57 +++ .../application/SalaryReviewDigestResult.java | 15 + .../SalaryReviewExtractResult.java | 31 ++ .../application/SalaryReviewOutputResult.java | 15 + .../SalaryReviewSignatureResult.java | 15 + .../corporatewallet/domain/SalaryBatch.java | 112 ++++- .../MybatisSalaryBatchRepository.java | 34 ++ .../infrastructure/SalaryBatchEntity.java | 38 ++ .../rest/SalaryConfirmController.java | 63 +++ .../rest/SalaryReviewController.java | 72 +++ .../rest/dto/SalaryConfirmSignRequest.java | 14 + .../rest/dto/SalaryReviewSignRequest.java | 14 + .../application/CurrencyIdRuleResult.java | 64 +++ .../StandardCurrencyRuleResult.java | 76 ++++ .../db/manual/salary_batch_upgrade.sql | 23 +- src/main/resources/schema.sql | 19 +- src/test/resources/schema.sql | 17 + 23 files changed, 1205 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmDigestResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmExtractResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmOutputResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmSignatureResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewCheckResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewDigestResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewExtractResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewOutputResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewSignatureResult.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryConfirmController.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryReviewController.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/SalaryConfirmSignRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/SalaryReviewSignRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyIdRuleResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyRuleResult.java diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java index 58f0458..4237f54 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java @@ -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() + .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(); + } } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmDigestResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmDigestResult.java new file mode 100644 index 0000000..f2bc336 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmDigestResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmExtractResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmExtractResult.java new file mode 100644 index 0000000..19df59a --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmExtractResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmOutputResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmOutputResult.java new file mode 100644 index 0000000..c5177bf --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmOutputResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmSignatureResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmSignatureResult.java new file mode 100644 index 0000000..75af804 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryConfirmSignatureResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryPageResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryPageResult.java index 643f0cb..b2d4088 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryPageResult.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryPageResult.java @@ -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 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 diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewCheckResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewCheckResult.java new file mode 100644 index 0000000..80d7a44 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewCheckResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewDigestResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewDigestResult.java new file mode 100644 index 0000000..196f8ec --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewDigestResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewExtractResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewExtractResult.java new file mode 100644 index 0000000..4fb2737 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewExtractResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewOutputResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewOutputResult.java new file mode 100644 index 0000000..4b12d4c --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewOutputResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewSignatureResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewSignatureResult.java new file mode 100644 index 0000000..290a771 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryReviewSignatureResult.java @@ -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; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/domain/SalaryBatch.java b/src/main/java/com/yau/digitalrmb/corporatewallet/domain/SalaryBatch.java index 539b9d2..3af025b 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/domain/SalaryBatch.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/domain/SalaryBatch.java @@ -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; } } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java index b170e97..eb38960 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java @@ -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; } } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/SalaryBatchEntity.java b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/SalaryBatchEntity.java index a842b57..2e1cf56 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/SalaryBatchEntity.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/SalaryBatchEntity.java @@ -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; } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryConfirmController.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryConfirmController.java new file mode 100644 index 0000000..5a713cf --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryConfirmController.java @@ -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 extractConfirmText() { + return ApiResponse.success(salaryBatchService.extractConfirmText( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + @PostMapping("/digest") + @Operation(summary = "生成交易确认摘要", + description = "对拼接交易确认原文进行SM3运算,只返回摘要值") + public ApiResponse computeConfirmDigest() { + return ApiResponse.success(salaryBatchService.computeConfirmDigest( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + @PostMapping("/sign") + @Operation(summary = "央行第一私钥签名", + description = "传入央行第一私钥进行SM2签名,私钥不正确则错误数+1,返回带CB_FINAL_前缀的签名值") + public ApiResponse 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 outputConfirm() { + return ApiResponse.success(salaryBatchService.outputConfirm( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + private String traceId() { + return MDC.get(TraceIdFilter.MDC_KEY); + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryReviewController.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryReviewController.java new file mode 100644 index 0000000..0266ef4 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/SalaryReviewController.java @@ -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 extractReviewText() { + return ApiResponse.success(salaryBatchService.extractReviewText( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + @PostMapping("/check") + @Operation(summary = "审核与权限校验", + description = "校验单笔限额(≤50000元)、批次限额(≤500000元)、日累计限额(≤1000000元),返回1-通过/2-未通过") + public ApiResponse checkPermission() { + return ApiResponse.success(salaryBatchService.checkPermission( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + @PostMapping("/digest") + @Operation(summary = "生成审核摘要", + description = "对拼接审核原文进行SM3运算,只返回摘要值") + public ApiResponse computeReviewDigest() { + return ApiResponse.success(salaryBatchService.computeReviewDigest( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + @PostMapping("/sign") + @Operation(summary = "SM2签名(复核员私钥)", + description = "传入数币复核员私钥进行SM2签名,私钥不正确则错误数+1,只返回签名值") + public ApiResponse 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 outputReview() { + return ApiResponse.success(salaryBatchService.outputReview( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + private String traceId() { + return MDC.get(TraceIdFilter.MDC_KEY); + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/SalaryConfirmSignRequest.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/SalaryConfirmSignRequest.java new file mode 100644 index 0000000..40d2bb5 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/SalaryConfirmSignRequest.java @@ -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; +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/SalaryReviewSignRequest.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/SalaryReviewSignRequest.java new file mode 100644 index 0000000..8af666d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/SalaryReviewSignRequest.java @@ -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; +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyIdRuleResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyIdRuleResult.java new file mode 100644 index 0000000..7522f3d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/CurrencyIdRuleResult.java @@ -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 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 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; + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyRuleResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyRuleResult.java new file mode 100644 index 0000000..d583ec1 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyRuleResult.java @@ -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 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 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; + } + } +} diff --git a/src/main/resources/db/manual/salary_batch_upgrade.sql b/src/main/resources/db/manual/salary_batch_upgrade.sql index 6f7d94d..b375ab5 100644 --- a/src/main/resources/db/manual/salary_batch_upgrade.sql +++ b/src/main/resources/db/manual/salary_batch_upgrade.sql @@ -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 '创建人', diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 5efd4fc..fcf9f5a 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -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 '创建人', diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 68817eb..787946e 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -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 '创建人',