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