|
|
|
@ -7,6 +7,7 @@ import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
|
|
|
|
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
|
|
|
|
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
|
|
|
|
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletKeyInfoEntity;
|
|
|
|
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletKeyInfoEntity;
|
|
|
|
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletKeyInfoMapper;
|
|
|
|
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.application.InstitutionKeySubject;
|
|
|
|
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
|
|
|
|
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
|
|
|
|
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatch;
|
|
|
|
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatch;
|
|
|
|
@ -17,6 +18,7 @@ import org.springframework.stereotype.Service;
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
|
import javax.annotation.Resource;
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
@ -29,6 +31,17 @@ public class SalaryBatchService {
|
|
|
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
|
|
|
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
|
|
|
|
private static final String OPERATOR_NAME = "王五";
|
|
|
|
private static final String OPERATOR_NAME = "王五";
|
|
|
|
private static final String OPERATOR_KEY_TYPE = "数币操作员私钥";
|
|
|
|
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 = {
|
|
|
|
private static final String[][] EMPLOYEE_DATA = {
|
|
|
|
@ -51,6 +64,8 @@ public class SalaryBatchService {
|
|
|
|
private InstitutionIdentityCryptography cryptography;
|
|
|
|
private InstitutionIdentityCryptography cryptography;
|
|
|
|
@Resource
|
|
|
|
@Resource
|
|
|
|
private SalaryScoreService scoreService;
|
|
|
|
private SalaryScoreService scoreService;
|
|
|
|
|
|
|
|
@Resource
|
|
|
|
|
|
|
|
private InstitutionKeyService institutionKeyService;
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 步骤一:调取区块信息 ====================
|
|
|
|
// ==================== 步骤一:调取区块信息 ====================
|
|
|
|
|
|
|
|
|
|
|
|
@ -181,7 +196,161 @@ public class SalaryBatchService {
|
|
|
|
return new SalarySendResult("待审核", submitTime);
|
|
|
|
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)
|
|
|
|
@Transactional(readOnly = true)
|
|
|
|
public SalaryPageResult pageQuery(InstitutionKeySubject subject) {
|
|
|
|
public SalaryPageResult pageQuery(InstitutionKeySubject subject) {
|
|
|
|
@ -203,14 +372,123 @@ public class SalaryBatchService {
|
|
|
|
|
|
|
|
|
|
|
|
if (batch == null) {
|
|
|
|
if (batch == null) {
|
|
|
|
return new SalaryPageResult(payroll, totalAmount, employeeCount,
|
|
|
|
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,
|
|
|
|
return new SalaryPageResult(payroll, totalAmount, employeeCount,
|
|
|
|
batch.getBatchId(), batch.getCorpWalletId(), batch.getOperatorName(),
|
|
|
|
batch.getBatchId(), batch.getCorpWalletId(), batch.getOperatorName(),
|
|
|
|
batch.getOperatorTimestamp(), batch.getConcatenatedMessage(),
|
|
|
|
batch.getOperatorTimestamp(), batch.getConcatenatedMessage(),
|
|
|
|
batch.getDigestValue(), batch.getSignature(), batch.getOutputJson(),
|
|
|
|
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();
|
|
|
|
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() {
|
|
|
|
private String generateBatchId() {
|
|
|
|
String datePart = LocalDateTime.now().format(DATE_FORMAT);
|
|
|
|
String datePart = LocalDateTime.now().format(DATE_FORMAT);
|
|
|
|
int seq = ThreadLocalRandom.current().nextInt(100, 1000);
|
|
|
|
int seq = ThreadLocalRandom.current().nextInt(100, 1000);
|
|
|
|
@ -315,4 +702,21 @@ public class SalaryBatchService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return String.format("%.2f", total);
|
|
|
|
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();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|