diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceService.java index 796d6ddee..917a1bc1a 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceService.java @@ -4,14 +4,12 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.baiyue.finance.client.FinanceFileClient; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; import java.math.BigDecimal; import java.sql.PreparedStatement; @@ -26,14 +24,12 @@ import java.util.Map; public class FinanceService { private final JdbcTemplate jdbc; private final ObjectMapper objectMapper; - @Autowired(required = false) - private FinanceFileClient fileClient; - @org.springframework.beans.factory.annotation.Value("${baiyue.security.internal-service-token:}") - private String fileToken; + private final FinanceVoucherValidator voucherValidator; - public FinanceService(JdbcTemplate jdbc, ObjectMapper objectMapper) { + public FinanceService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator) { this.jdbc = jdbc; this.objectMapper = objectMapper; + this.voucherValidator = voucherValidator; } public Map customerTerms(long tenantId) { @@ -203,20 +199,8 @@ public class FinanceService { } private void validateVoucherFiles(long tenantId, List fileIds) { - if (fileIds == null || fileIds.isEmpty()) throw bad("财务凭证不能为空"); - if (fileClient == null) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "File服务校验能力不可用"); - for (String raw : fileIds) { - long id; try { id = Long.parseLong(raw); } catch (Exception ex) { throw bad("凭证文件ID无效"); } - var response = fileClient.metadata(fileToken, tenantId, tenantId, id); - var metadata = response == null ? null : response.data(); - if (metadata == null || metadata.fileId() == null || !tenantIdEquals(metadata.tenantId(), tenantId) - || !"FINANCE_VOUCHER".equals(metadata.businessType()) || metadata.contentType() == null - || !metadata.contentType().toLowerCase(java.util.Locale.ROOT).startsWith("image/")) { - throw bad("凭证文件不存在、归属不匹配或不是有效图片"); - } - } + voucherValidator.validate(tenantId, fileIds, "财务凭证"); } - private static boolean tenantIdEquals(Long actual, long expected) { return actual != null && actual == expected; } @Transactional public Map createCost(long tenantId, long operatorId, CostRequest request, String idempotencyKey) { diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java index 8785bc7e8..a81311e13 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java @@ -1,6 +1,5 @@ package com.baiyue.finance.service; -import com.baiyue.finance.client.FinanceFileClient; import com.baiyue.finance.dto.FinanceSettlementDtos.CreatePayableSettlementRequest; import com.baiyue.finance.dto.FinanceSettlementDtos.CreateReceivableSettlementRequest; import com.baiyue.finance.dto.FinanceSettlementDtos.CreateSettlementPaymentRequest; @@ -14,7 +13,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.MDC; -import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; @@ -47,15 +45,12 @@ public class FinanceSettlementService { private static final Set PAYMENT_METHODS = Set.of("BANK_TRANSFER", "CASH", "OTHER"); private final JdbcTemplate jdbc; private final ObjectMapper objectMapper; - private final FinanceFileClient fileClient; + private final FinanceVoucherValidator voucherValidator; - @Value("${baiyue.security.internal-service-token:}") - private String fileToken; - - public FinanceSettlementService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceFileClient fileClient) { + public FinanceSettlementService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator) { this.jdbc = jdbc; this.objectMapper = objectMapper; - this.fileClient = fileClient; + this.voucherValidator = voucherValidator; } @Transactional @@ -355,26 +350,7 @@ public class FinanceSettlementService { } private void validateVoucherFiles(long customerId, List fileIds) { - if (fileClient == null || !StringUtils.hasText(fileToken)) throw error("FIN_503", "File服务校验能力不可用"); - for (String raw : fileIds) { - long fileId; - try { fileId = Long.parseLong(raw); } catch (RuntimeException invalid) { throw error("FIN_400", "付款凭证文件ID无效"); } - FinanceFileClient.Metadata metadata; - try { - var response = fileClient.metadata(fileToken, customerId, customerId, fileId); - metadata = response == null ? null : response.data(); - } catch (RuntimeException unavailable) { - throw error("FIN_503", "File服务校验不可用"); - } - if (metadata == null || metadata.fileId() == null || metadata.fileId() != fileId - || metadata.tenantId() == null || metadata.tenantId() != customerId - || !"FINANCE_VOUCHER".equals(metadata.businessType()) - || !String.valueOf(customerId).equals(metadata.businessId()) - || !StringUtils.hasText(metadata.contentType()) - || !metadata.contentType().toLowerCase(Locale.ROOT).startsWith("image/")) { - throw error("FIN_400", "付款凭证不存在、客户归属不匹配或文件类型无效"); - } - } + voucherValidator.validate(customerId, fileIds, "付款凭证"); } private Long reserve(long customerId, long operatorId, String operation, String key, diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceVoucherValidator.java b/finance/src/main/java/com/baiyue/finance/service/FinanceVoucherValidator.java new file mode 100644 index 000000000..ae00efec5 --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceVoucherValidator.java @@ -0,0 +1,56 @@ +package com.baiyue.finance.service; + +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.finance.client.FinanceFileClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Locale; +import java.util.HashSet; + +@Component +public class FinanceVoucherValidator { + private final FinanceFileClient fileClient; + private final String fileToken; + + @Autowired + public FinanceVoucherValidator(FinanceFileClient fileClient, + @Value("${baiyue.security.internal-service-token:}") String fileToken) { + this.fileClient = fileClient; + this.fileToken = fileToken; + } + + public void validate(long customerId, List fileIds, String message) { + if (fileIds == null || fileIds.isEmpty()) throw bad(message + "不能为空"); + if (fileClient == null || !StringUtils.hasText(fileToken)) throw unavailable("File服务校验能力不可用"); + if (new HashSet<>(fileIds).size() != fileIds.size()) throw bad(message + "文件ID不能重复"); + for (String raw : fileIds) { + final long fileId; + try { fileId = Long.parseLong(raw); } catch (RuntimeException invalid) { throw bad(message + "文件ID无效"); } + FinanceFileClient.Metadata metadata; + try { + var response = fileClient.metadata(fileToken, customerId, customerId, fileId); + metadata = response == null ? null : response.data(); + } catch (RuntimeException unavailable) { + throw unavailable("File服务校验不可用"); + } + if (metadata == null || metadata.fileId() == null || metadata.fileId() != fileId + || metadata.tenantId() == null || metadata.tenantId() != customerId + || !String.valueOf(customerId).equals(metadata.businessId()) + || !"FINANCE_VOUCHER".equals(metadata.businessType()) + || !StringUtils.hasText(metadata.contentType()) + || !metadata.contentType().toLowerCase(Locale.ROOT).startsWith("image/")) { + throw bad(message + "不存在、归属不匹配或不是有效图片"); + } + } + } + + private static BizException bad(String message) { return new BizException(ResultCode.BAD_REQUEST, message); } + private static BizException unavailable(String message) { + return new BizException(ResultCode.SERVICE_UNAVAILABLE, message); + } +} diff --git a/finance/src/test/java/com/baiyue/finance/FinanceServiceValidationTest.java b/finance/src/test/java/com/baiyue/finance/FinanceServiceValidationTest.java index b52559300..971a49cf2 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceServiceValidationTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceServiceValidationTest.java @@ -1,6 +1,7 @@ package com.baiyue.finance; import com.baiyue.finance.service.FinanceService; +import com.baiyue.finance.service.FinanceVoucherValidator; import com.baiyue.finance.client.FinanceFileClient; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.ResultCode; @@ -18,7 +19,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -import org.springframework.test.util.ReflectionTestUtils; class FinanceServiceValidationTest { @Test @@ -49,7 +49,7 @@ class FinanceServiceValidationTest { @Test void rejectsUnsupportedCostTypeBeforeWrite() { JdbcTemplate jdbc = mock(JdbcTemplate.class); - FinanceService service = new FinanceService(jdbc, new ObjectMapper()); + FinanceService service = service(jdbc); var request = new FinanceService.CostRequest("C-1", "TICKET", 1L, null, null, null, null, "MANUAL", "拖车", BigDecimal.ONE, List.of("f"), null); assertThrows(RuntimeException.class, () -> service.createCost(1L, 2L, request, "k1")); verify(jdbc, never()).update(startsWith("INSERT INTO fin_cost"), (Object[]) any()); @@ -58,7 +58,7 @@ class FinanceServiceValidationTest { @Test void rejectsNonPositiveAllocation() { JdbcTemplate jdbc = mock(JdbcTemplate.class); - FinanceService service = new FinanceService(jdbc, new ObjectMapper()); + FinanceService service = service(jdbc); var request = new FinanceService.ReceiptRequest("R-1", 1L, null, "BANK_TRANSFER", null, List.of("f"), null, List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ZERO))); assertThrows(RuntimeException.class, () -> service.createReceipt(1L, 2L, request, "k2")); @@ -67,7 +67,7 @@ class FinanceServiceValidationTest { @Test void rejectsDuplicateBillAllocationInsteadOfMerging() { JdbcTemplate jdbc = mock(JdbcTemplate.class); - FinanceService service = new FinanceService(jdbc, new ObjectMapper()); + FinanceService service = service(jdbc); var request = new FinanceService.ReceiptRequest("R-2", 1L, null, "BANK_TRANSFER", null, List.of("f"), null, List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE), new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE))); assertThrows(RuntimeException.class, () -> service.createReceipt(1L, 2L, request, "k3")); @@ -76,7 +76,8 @@ class FinanceServiceValidationTest { @Test void receiptFailsClosedWhenFileValidationCapabilityIsUnavailable() { JdbcTemplate jdbc = mock(JdbcTemplate.class); - FinanceService service = new FinanceService(jdbc, new ObjectMapper()); + FinanceService service = new FinanceService(jdbc, new ObjectMapper(), + new FinanceVoucherValidator(null, "")); var request = new FinanceService.ReceiptRequest("R-FILE", 1L, null, "BANK_TRANSFER", null, List.of("77"), null, List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE))); @@ -92,8 +93,7 @@ class FinanceServiceValidationTest { FinanceFileClient files = mock(FinanceFileClient.class); when(files.metadata(anyString(), eq(9L), eq(9L), eq(77L))).thenReturn(ApiResponse.success( new FinanceFileClient.Metadata(77L, 10L, "FINANCE_VOUCHER", "9", "v.jpg", "sha", "image/jpeg", 10L))); - FinanceService service = new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules()); - ReflectionTestUtils.setField(service, "fileClient", files); + FinanceService service = service(jdbc, files); var request = new FinanceService.ReceiptRequest("R-3", 1L, null, "BANK_TRANSFER", null, List.of("77"), null, List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE))); assertThrows(RuntimeException.class, () -> service.createReceipt(9L, 2L, request, "k4")); @@ -117,9 +117,7 @@ class FinanceServiceValidationTest { holder.getKeyList().add(java.util.Map.of("GENERATED_KEY", 701L)); return 1; }).when(jdbc).update(any(org.springframework.jdbc.core.PreparedStatementCreator.class), any(org.springframework.jdbc.support.KeyHolder.class)); - FinanceService service = new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules()); - ReflectionTestUtils.setField(service, "fileClient", files); - ReflectionTestUtils.setField(service, "fileToken", "internal-token"); + FinanceService service = service(jdbc, files); var request = new FinanceService.ReceiptRequest("R-4", 1L, java.time.LocalDateTime.of(2026, 8, 11, 10, 0), "BANK_TRANSFER", null, List.of("77"), "首笔收款", List.of(new FinanceService.ReceiptAllocation(50L, new BigDecimal("40.00")))); @@ -154,9 +152,7 @@ class FinanceServiceValidationTest { holder.getKeyList().add(java.util.Map.of("GENERATED_KEY", 701L)); return 1; }).when(jdbc).update(any(org.springframework.jdbc.core.PreparedStatementCreator.class), any(org.springframework.jdbc.support.KeyHolder.class)); - FinanceService service = new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules()); - ReflectionTestUtils.setField(service, "fileClient", files); - ReflectionTestUtils.setField(service, "fileToken", "internal-token"); + FinanceService service = service(jdbc, files); var request = new FinanceService.ReceiptRequest("R-300", 1L, java.time.LocalDateTime.of(2026, 8, 11, 10, 0), "BANK_TRANSFER", "BANK-300", List.of("77"), "三单收款", List.of( new FinanceService.ReceiptAllocation(50L, new BigDecimal("100.00")), @@ -176,7 +172,7 @@ class FinanceServiceValidationTest { void rejectsSingleOrderWhenOrderNotOwnedByCustomer() { JdbcTemplate jdbc = mock(JdbcTemplate.class); when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source"), eq(Number.class), any(), any())).thenReturn(0); - FinanceService service = new FinanceService(jdbc, new ObjectMapper()); + FinanceService service = service(jdbc); var request = new FinanceService.CostRequest("C-2", "SINGLE_ORDER", 99L, null, null, null, null, "MANUAL", "货拉拉", BigDecimal.ONE, List.of("1"), null); assertThrows(RuntimeException.class, () -> service.createCost(9L, 2L, request, "k5")); } @@ -198,9 +194,7 @@ class FinanceServiceValidationTest { holder.getKeyList().add(java.util.Map.of("GENERATED_KEY", 701L)); return 1; }).when(jdbc).update(any(org.springframework.jdbc.core.PreparedStatementCreator.class), any(org.springframework.jdbc.support.KeyHolder.class)); - FinanceService service = new FinanceService(jdbc, new ObjectMapper()); - ReflectionTestUtils.setField(service, "fileClient", files); - ReflectionTestUtils.setField(service, "fileToken", "internal-token"); + FinanceService service = service(jdbc, files); var request = new FinanceService.CostRequest("C-3", "CONTAINER_SHARED", null, null, 88L, null, null, "MANUAL", "整柜成本", new BigDecimal("300.00"), List.of("77"), null); var snapshot = List.of(new FinanceService.ContainerOrderVolume( @@ -221,7 +215,7 @@ class FinanceServiceValidationTest { .thenReturn(1); var rows = List.>of(java.util.Map.of("version", 2)); when(jdbc.queryForList(startsWith("SELECT id,bill_id,version"), eq(9L), eq(50L))).thenReturn(rows); - FinanceService service = new FinanceService(jdbc, new ObjectMapper()); + FinanceService service = service(jdbc); assertEquals(rows, service.billRevisions(9L, 50L)); } @@ -231,9 +225,19 @@ class FinanceServiceValidationTest { JdbcTemplate jdbc = mock(JdbcTemplate.class); when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_bill"), eq(Number.class), eq(50L), eq(9L))) .thenReturn(0); - FinanceService service = new FinanceService(jdbc, new ObjectMapper()); + FinanceService service = service(jdbc); assertThrows(RuntimeException.class, () -> service.billRevisions(9L, 50L)); verify(jdbc, never()).queryForList(startsWith("SELECT id,bill_id,version"), (Object[]) any()); } + + private static FinanceService service(JdbcTemplate jdbc, FinanceFileClient files) { + return new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules(), + new FinanceVoucherValidator(files, "internal-token")); + } + + private static FinanceService service(JdbcTemplate jdbc) { + return new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules(), + mock(FinanceVoucherValidator.class)); + } } diff --git a/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java index ef9417176..df28cad86 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java @@ -1,6 +1,8 @@ package com.baiyue.finance; import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.finance.client.FinanceFileClient; import com.baiyue.finance.controller.FinanceSettlementController; @@ -12,6 +14,7 @@ import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementSummary; import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementActionRequest; import com.baiyue.finance.service.FinanceEventException; import com.baiyue.finance.service.FinanceSettlementService; +import com.baiyue.finance.service.FinanceVoucherValidator; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -136,8 +139,11 @@ class FinanceSettlementBehaviorTest { when(fileClient.metadata("internal-token", 10L, 10L, 91L)).thenThrow(new IllegalStateException("down")); FinanceSettlementService service = service(jdbc, fileClient); - assertCode(() -> service.createSettlementPayment(88L, 7L, "pay-key", - payment(new BigDecimal("40.00"), 0)), "FIN_503"); + assertThatThrownBy(() -> service.createSettlementPayment(88L, 7L, "pay-key", + payment(new BigDecimal("40.00"), 0))) + .isInstanceOf(BizException.class) + .extracting(error -> ((BizException) error).getResultCode()) + .isEqualTo(ResultCode.SERVICE_UNAVAILABLE); verify(jdbc, never()).update(startsWith("INSERT INTO fin_settlement_payment"), (Object[]) any()); verify(jdbc, never()).update(startsWith("UPDATE fin_settlement SET paid_amount"), (Object[]) any()); @@ -516,10 +522,8 @@ class FinanceSettlementBehaviorTest { } private static FinanceSettlementService service(JdbcTemplate jdbc, FinanceFileClient fileClient) { - FinanceSettlementService service = new FinanceSettlementService( - jdbc, new ObjectMapper().findAndRegisterModules(), fileClient); - ReflectionTestUtils.setField(service, "fileToken", "internal-token"); - return service; + return new FinanceSettlementService(jdbc, new ObjectMapper().findAndRegisterModules(), + new FinanceVoucherValidator(fileClient, "internal-token")); } private static CreateSettlementPaymentRequest payment(BigDecimal amount, int version) { diff --git a/finance/src/test/java/com/baiyue/finance/FinanceVoucherValidatorTest.java b/finance/src/test/java/com/baiyue/finance/FinanceVoucherValidatorTest.java new file mode 100644 index 000000000..e40ddf45c --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceVoucherValidatorTest.java @@ -0,0 +1,87 @@ +package com.baiyue.finance; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.finance.client.FinanceFileClient; +import com.baiyue.finance.service.FinanceVoucherValidator; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class FinanceVoucherValidatorTest { + @Test + void acceptsCustomerOwnedFinanceImage() { + FinanceFileClient files = mock(FinanceFileClient.class); + when(files.metadata("token", 9L, 9L, 77L)).thenReturn(ApiResponse.success( + new FinanceFileClient.Metadata(77L, 9L, "FINANCE_VOUCHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L))); + + new FinanceVoucherValidator(files, "token").validate(9L, List.of("77"), "财务凭证"); + verify(files).metadata("token", 9L, 9L, 77L); + } + + @Test + void rejectsMismatchedFileIdentityOwnershipTypeAndMime() { + FinanceFileClient files = mock(FinanceFileClient.class); + List invalid = List.of( + new FinanceFileClient.Metadata(78L, 9L, "FINANCE_VOUCHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L), + new FinanceFileClient.Metadata(77L, 10L, "FINANCE_VOUCHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L), + new FinanceFileClient.Metadata(77L, 9L, "FINANCE_VOUCHER", "10", "receipt.jpg", "sha", "image/jpeg", 10L), + new FinanceFileClient.Metadata(77L, 9L, "OTHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L), + new FinanceFileClient.Metadata(77L, 9L, "FINANCE_VOUCHER", "9", "receipt.pdf", "sha", "application/pdf", 10L)); + + for (FinanceFileClient.Metadata metadata : invalid) { + when(files.metadata(eq("token"), eq(9L), eq(9L), eq(77L))).thenReturn(ApiResponse.success(metadata)); + assertThatThrownBy(() -> new FinanceVoucherValidator(files, "token") + .validate(9L, List.of("77"), "付款凭证")) + .isInstanceOf(BizException.class) + .extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.BAD_REQUEST); + } + } + + @Test + void failsClosedWhenFileDependencyUnavailable() { + FinanceFileClient files = mock(FinanceFileClient.class); + when(files.metadata("token", 9L, 9L, 77L)).thenThrow(new IllegalStateException("down")); + + assertThatThrownBy(() -> new FinanceVoucherValidator(files, "token") + .validate(9L, List.of("77"), "付款凭证")) + .isInstanceOf(BizException.class) + .extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.SERVICE_UNAVAILABLE); + } + + @Test + void rejectsMissingInvalidAndDuplicateFileIds() { + FinanceVoucherValidator validator = new FinanceVoucherValidator(mock(FinanceFileClient.class), "token"); + assertResult(() -> validator.validate(9L, List.of(), "财务凭证"), ResultCode.BAD_REQUEST); + assertResult(() -> validator.validate(9L, List.of("invalid"), "财务凭证"), ResultCode.BAD_REQUEST); + assertResult(() -> validator.validate(9L, List.of("77", "77"), "付款凭证"), ResultCode.BAD_REQUEST); + } + + @Test + void rejectsWrongFileIdAndCustomerOwnership() { + FinanceFileClient files = mock(FinanceFileClient.class); + when(files.metadata("token", 9L, 9L, 77L)).thenReturn(ApiResponse.success( + new FinanceFileClient.Metadata(78L, 10L, "FINANCE_VOUCHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L))); + assertResult(() -> new FinanceVoucherValidator(files, "token") + .validate(9L, List.of("77"), "付款凭证"), ResultCode.BAD_REQUEST); + } + + @Test + void rejectsMissingToken() { + assertResult(() -> new FinanceVoucherValidator(mock(FinanceFileClient.class), "") + .validate(9L, List.of("77"), "财务凭证"), ResultCode.SERVICE_UNAVAILABLE); + } + + private static void assertResult(org.assertj.core.api.ThrowableAssert.ThrowingCallable call, + ResultCode resultCode) { + assertThatThrownBy(call).isInstanceOf(BizException.class) + .extracting(error -> ((BizException) error).getResultCode()).isEqualTo(resultCode); + } +} diff --git a/scripts/admin-test-env-modules/notifications-contract.test.mjs b/scripts/admin-test-env-modules/notifications-contract.test.mjs index 37b878a2d..d5f5ae5a6 100644 --- a/scripts/admin-test-env-modules/notifications-contract.test.mjs +++ b/scripts/admin-test-env-modules/notifications-contract.test.mjs @@ -27,7 +27,7 @@ test('覆盖模板生命周期、版本冲突、定时任务、幂等、查询 ]) assert.match(moduleSource, new RegExp(token)) assert.match(moduleSource, /recipientType:\s*'PLATFORM_ROLE'/) assert.match(moduleSource, /roleCode:\s*'PLATFORM_ADMIN'/) - assert.match(moduleSource, /Date\.now\(\) \+ 24 \* 60 \* 60 \* 1000/) + assert.match(moduleSource, /Date\.now\(\) \+ 365 \* 24 \* 60 \* 60 \* 1000/) }) test('异常路径会取消定时任务并停用模板,不实际投递验收消息', () => { diff --git a/scripts/admin-test-env-modules/notifications.mjs b/scripts/admin-test-env-modules/notifications.mjs index 633d0b752..d68916f05 100644 --- a/scripts/admin-test-env-modules/notifications.mjs +++ b/scripts/admin-test-env-modules/notifications.mjs @@ -94,7 +94,7 @@ export async function run(context) { template?.status === 'ACTIVE' && Number(template?.version) === 3, `status=${template?.status || 'missing'}, version=${template?.version ?? 'missing'}`, OPTIONS) - const scheduledAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().slice(0, 19) + const scheduledAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19) const idempotencyKey = `${context.marker}-notification-scheduled`.slice(0, 128) const taskCreate = { tenantId: 0,