From c243884e8862be07c180be64978c5dea06f50323 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Wed, 16 Sep 2026 09:40:48 +0800 Subject: [PATCH] fix(salary): isolate batches and sign digest value --- .../application/SalaryBatchService.java | 16 ++-- .../MybatisSalaryBatchRepository.java | 1 + .../application/SalaryBatchServiceTest.java | 90 +++++++++++++++++++ 3 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchServiceTest.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 3bd15c2..d3fe05d 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java @@ -73,14 +73,16 @@ public class SalaryBatchService { @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, "请先完成对公钱包开立(步骤一)"); } + // 每次调取区块信息都代表一个新的工资批次,避免复用上一批次的摘要、签名和状态。 + SalaryBatch batch = repository.save( + SalaryBatch.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId())); + // 批次号随机生成 String batchId = generateBatchId(); // 时间戳 @@ -161,8 +163,8 @@ public class SalaryBatchService { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数币操作员私钥不正确,错误次数+1"); } - // 使用数币操作员私钥对摘要进行SM2签名 - String signature = cryptography.sign(storedPrivateKey, batch.getConcatenatedMessage()); + // 按原型要求,使用数币操作员私钥对SM3摘要值进行SM2签名。 + String signature = cryptography.sign(storedPrivateKey, batch.getDigestValue()); batch.sign(signature); repository.update(batch); @@ -516,12 +518,6 @@ public class SalaryBatchService { // ==================== 内部方法 ==================== - 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, 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 eb38960..8c5d2ff 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java @@ -30,6 +30,7 @@ public class MybatisSalaryBatchRepository implements SalaryBatchRepository { .eq(SalaryBatchEntity::getClassId, classId) .eq(SalaryBatchEntity::getDeleted, false) .orderByDesc(SalaryBatchEntity::getCreatedAt) + .orderByDesc(SalaryBatchEntity::getId) .last("LIMIT 1")); return entity == null ? Optional.empty() : Optional.of(toDomain(entity)); } diff --git a/src/test/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchServiceTest.java b/src/test/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchServiceTest.java new file mode 100644 index 0000000..6080d7c --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchServiceTest.java @@ -0,0 +1,90 @@ +package com.yau.digitalrmb.corporatewallet.application; + +import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication; +import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository; +import com.yau.digitalrmb.corporatewallet.domain.SalaryBatch; +import com.yau.digitalrmb.corporatewallet.domain.SalaryBatchRepository; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SalaryBatchServiceTest { + private static final InstitutionKeySubject SUBJECT = + new InstitutionKeySubject("salary-user", 1001L, 2001L); + private static final String PRIVATE_KEY = "OPERATOR_PRIVATE_KEY"; + + @Mock private SalaryBatchRepository repository; + @Mock private CorporateWalletApplicationRepository corporateWalletRepository; + @Mock private InstitutionIdentityCryptography cryptography; + @Mock private SalaryScoreService scoreService; + @Mock private CorporateWalletKeyService keyService; + @InjectMocks private SalaryBatchService service; + + @Test + void startsFreshBatchAndSignsCurrentDigestWhenWorkflowRunsAgain() { + AtomicReference latest = new AtomicReference<>(); + AtomicLong ids = new AtomicLong(10L); + when(repository.save(any(SalaryBatch.class))).thenAnswer(invocation -> { + SalaryBatch batch = invocation.getArgument(0); + batch.setId(ids.incrementAndGet()); + latest.set(batch); + return batch; + }); + when(repository.findLatest(SUBJECT.getUserId(), SUBJECT.getSchoolId(), SUBJECT.getClassId())) + .thenAnswer(invocation -> Optional.ofNullable(latest.get())); + + CorporateWalletApplication wallet = CorporateWalletApplication.create( + SUBJECT.getUserId(), SUBJECT.getSchoolId(), SUBJECT.getClassId()); + wallet.setWalletId("CORP_WALLET_001"); + when(corporateWalletRepository.findLatest( + SUBJECT.getUserId(), SUBJECT.getSchoolId(), SUBJECT.getClassId())) + .thenReturn(Optional.of(wallet)); + when(cryptography.sm3(any(String.class))).thenReturn("DIGEST_ONE", "DIGEST_TWO"); + when(keyService.getOrCreatePrivateKey( + any(InstitutionKeySubject.class), eq("数币操作员私钥"), eq("王五"))) + .thenReturn(PRIVATE_KEY); + when(cryptography.sign(PRIVATE_KEY, "DIGEST_ONE")).thenReturn("SIGNATURE_ONE"); + when(cryptography.sign(PRIVATE_KEY, "DIGEST_TWO")).thenReturn("SIGNATURE_TWO"); + + service.fetchBlockchain(SUBJECT, "tester"); + service.concatenate(SUBJECT); + SalaryDigestResult firstDigest = service.computeDigest(SUBJECT); + SalaryBatch firstBatch = latest.get(); + assertThat(firstBatch.getStatus()).isEqualTo(SalaryBatch.Status.DIGESTED); + SalarySignatureResult firstSignature = service.sign(SUBJECT, PRIVATE_KEY); + + service.fetchBlockchain(SUBJECT, "tester"); + service.concatenate(SUBJECT); + SalaryDigestResult secondDigest = service.computeDigest(SUBJECT); + SalaryBatch secondBatch = latest.get(); + assertThat(secondBatch.getStatus()).isEqualTo(SalaryBatch.Status.DIGESTED); + SalarySignatureResult secondSignature = service.sign(SUBJECT, PRIVATE_KEY); + + assertThat(secondBatch).isNotSameAs(firstBatch); + assertThat(secondBatch.getId()).isNotEqualTo(firstBatch.getId()); + assertThat(firstDigest.getDigestValue()).isEqualTo("DIGEST_ONE"); + assertThat(secondDigest.getDigestValue()).isEqualTo("DIGEST_TWO"); + assertThat(firstSignature.getSignature()).isEqualTo("SIGNATURE_ONE"); + assertThat(secondSignature.getSignature()).isEqualTo("SIGNATURE_TWO"); + + verify(repository, times(2)).save(any(SalaryBatch.class)); + verify(cryptography).sign(PRIVATE_KEY, "DIGEST_ONE"); + verify(cryptography).sign(PRIVATE_KEY, "DIGEST_TWO"); + } +}