This commit is contained in:
2026-08-17 17:49:03 +08:00
parent c71db4b481
commit 7c66097672
21 changed files with 1227 additions and 14 deletions
@@ -0,0 +1,50 @@
package com.baiyue.finance;
import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class FinanceExportContractTest {
private static final List<String> TYPES = List.of(
"FINANCE_CUSTOMER_ACCOUNT_LIST", "FINANCE_CUSTOMER_ACCOUNT_LEDGER", "FINANCE_FEE_SOURCE_LIST",
"FINANCE_RECONCILIATION_STATEMENT_LIST", "FINANCE_RECEIVABLE_SETTLEMENT_LIST",
"FINANCE_RECEIVABLE_SETTLEMENT_DETAIL", "FINANCE_PAYABLE_SETTLEMENT_LIST", "FINANCE_PAYABLE_SETTLEMENT_DETAIL");
@Test
void keepsOpenApiAndAllFileDefinitionsOnTheSameEightTypes() throws Exception {
String openApi = Files.readString(resolve("docs", "财务管理", "openapi", "finance-phase1-five-pages-v1.yaml"), StandardCharsets.UTF_8);
String application = Files.readString(resolve("baiyue-firstleg", "file", "src", "main", "resources", "application.yml"), StandardCharsets.UTF_8);
String local = Files.readString(resolve("baiyue-firstleg", "config", "nacos", "local", "file-service.yml"), StandardCharsets.UTF_8);
String cn = Files.readString(resolve("baiyue-firstleg", "config", "nacos", "cn", "file-service.yml"), StandardCharsets.UTF_8);
Yaml yaml = new Yaml();
assertThat((Object) yaml.load(openApi)).isNotNull();
assertThat((Object) yaml.load(application)).isNotNull();
assertThat((Object) yaml.load(local)).isNotNull();
assertThat((Object) yaml.load(cn)).isNotNull();
assertThat(openApi).contains("/internal/exports/v1/{exportType}/batches:")
.contains("customerId?, status?, settlementNo?")
.contains("所有 DETAIL 仅 settlementId")
.doesNotContain("DETAIL: settlementId, direction");
for (String type : TYPES) {
assertThat(openApi).contains("- " + type);
assertThat(application).contains(type + ":").contains("provider-path: /internal/exports/v1/" + type + "/batches");
assertThat(local).contains(type + ":").contains("provider-path: /internal/exports/v1/" + type + "/batches");
assertThat(cn).contains(type + ":").contains("provider-path: /internal/exports/v1/" + type + "/batches");
}
}
private static Path resolve(String... segments) {
Path repository = Path.of("").toAbsolutePath().normalize();
while (repository != null && !Files.exists(repository.resolve("apps").resolve("admin-web"))) repository = repository.getParent();
if (repository == null) throw new IllegalStateException("repository root not found");
return repository.resolve(Path.of(segments[0], java.util.Arrays.copyOfRange(segments, 1, segments.length)));
}
}
@@ -0,0 +1,37 @@
package com.baiyue.finance.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
import com.baiyue.common.core.export.ExportContracts.ExportBatchResponse;
import com.baiyue.finance.service.FinanceExportProviderService;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class FinanceExportProviderControllerTest {
@Test
void exposesOnlyTheInternalVersionedProviderAndRequiresTheServiceToken() {
FinanceExportProviderService service = mock(FinanceExportProviderService.class);
String token = "12345678901234567890123456789012";
FinanceExportProviderController controller = new FinanceExportProviderController(service, token);
controller.initializeToken();
ExportBatchRequest request = new ExportBatchRequest("9", "0", "PLATFORM", "7", 1, Map.of(), null, null, 100);
ExportBatchResponse response = new ExportBatchResponse(1, "sha256:test", "snapshot", List.of(), List.of(), "0", false, 0L);
when(service.exportBatch(FinanceExportProviderService.CUSTOMER_ACCOUNT_LIST, request)).thenReturn(response);
assertThat(FinanceExportProviderController.class.getAnnotation(RequestMapping.class).value())
.containsExactly("/internal/exports/v1");
assertThat(controller.batch(token, FinanceExportProviderService.CUSTOMER_ACCOUNT_LIST, request).data()).isSameAs(response);
verify(service).exportBatch(FinanceExportProviderService.CUSTOMER_ACCOUNT_LIST, request);
assertThatThrownBy(() -> controller.batch("wrong", FinanceExportProviderService.CUSTOMER_ACCOUNT_LIST, request))
.isInstanceOf(BizException.class);
}
}
@@ -0,0 +1,93 @@
package com.baiyue.finance.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
import com.baiyue.finance.dto.FinancePhase1Dtos.FeeSourceDetail;
import com.baiyue.finance.dto.FinanceReadModels.Statement;
import com.baiyue.finance.dto.ReconciliationStatementDtos.PaymentHistory;
import com.baiyue.finance.dto.ReconciliationStatementDtos.ReconciliationDetail;
import com.baiyue.finance.dto.ReconciliationStatementDtos.ReconciliationLine;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class FinanceExportProviderServiceTest {
@Mock FinanceCustomerAccountService accountService;
@Mock FinancePhase1OperationService operationService;
@Mock FinanceQueryService queryService;
@Mock ReconciliationStatementService reconciliationService;
@Mock FinanceSettlementService settlementService;
private FinanceExportProviderService service;
@BeforeEach
void setUp() {
service = new FinanceExportProviderService(accountService, operationService, queryService,
reconciliationService, settlementService, new ObjectMapper(), "12345678901234567890123456789012");
service.initializeSigningKey();
}
@Test
void exportsStatementRowsUsingBusinessContainerNumberAndStableSnapshot() {
Statement summary = new Statement(501L, 10001L, "REC-501", "客户A", LocalDate.parse("2026-08-31"),
null, new BigDecimal("300"), new BigDecimal("100"), new BigDecimal("200"), "CNY", "ISSUED", 3,
7L, "创建人A", LocalDateTime.parse("2026-08-01T09:00:00"), 8L, "更新人B", LocalDateTime.parse("2026-08-20T11:00:00"));
ReconciliationLine line = new ReconciliationLine(601L, 81L, 2, "hash", 21L, 31L, "运费",
new BigDecimal("320"), new BigDecimal("300"), "CNY", "优惠调整");
ReconciliationDetail detail = new ReconciliationDetail(501L, "REC-501", 10001L, "客户A",
LocalDate.parse("2026-08-01"), LocalDate.parse("2026-08-31"), 30, "CNY", new BigDecimal("300"),
new BigDecimal("100"), new BigDecimal("200"), false, "ISSUED", 3, 7L, "创建人A",
LocalDateTime.parse("2026-08-01T09:00:00"), 8L, "更新人B", LocalDateTime.parse("2026-08-20T11:00:00"),
"八月账单", List.of(line), List.of(new PaymentHistory(701L, "RECEIPT", LocalDate.parse("2026-08-20"),
new BigDecimal("100"), "BANK_TRANSFER", "PAY-701", 9L, "财务员", LocalDateTime.parse("2026-08-20T10:00:00"), "")), List.of());
FeeSourceDetail source = new FeeSourceDetail(81L, "CONTAINER_FEE", 9001L, 2, "RECEIVABLE", 10001L,
"客户A", null, null, 21L, "IN-21", 31L, "CONT-31", LocalDate.parse("2026-08-15"), "运费",
BigDecimal.TEN, new BigDecimal("30"), new BigDecimal("300"), "CNY", "APPROVED", true,
"AVAILABLE", true, null, null, "hash", 2, List.of());
when(queryService.statements(10001L, false)).thenReturn(List.of(summary));
when(reconciliationService.detailForPlatform(501L)).thenReturn(detail);
when(operationService.getFinanceFeeSource(81L)).thenReturn(source);
var first = service.exportBatch(FinanceExportProviderService.RECONCILIATION_STATEMENT_LIST,
request(Map.of("customerId", "10001", "containerNo", "CONT-31"), null, null));
var continued = service.exportBatch(FinanceExportProviderService.RECONCILIATION_STATEMENT_LIST,
request(Map.of("customerId", "10001", "containerNo", "CONT-31"), first.nextCursor(), first.snapshotToken()));
assertThat(first.columns()).isNotEmpty();
assertThat(first.rows()).hasSize(1);
assertThat(first.rows().get(0)).containsEntry("statementNo", "REC-501").containsEntry("customerName", "客户A");
assertThat(first.totalRows()).isEqualTo(1L);
assertThat(continued.columns()).isEmpty();
assertThat(continued.rows()).isEmpty();
assertThat(continued.snapshotToken()).isEqualTo(first.snapshotToken());
}
@Test
void rejectsUnknownParametersAndSnapshotReuseAcrossFilters() {
assertThatThrownBy(() -> service.exportBatch(FinanceExportProviderService.CUSTOMER_ACCOUNT_LIST,
request(Map.of("page", 1), null, null))).isInstanceOf(BizException.class);
var first = service.exportBatch(FinanceExportProviderService.RECONCILIATION_STATEMENT_LIST,
request(Map.of("status", "ISSUED"), null, null));
assertThatThrownBy(() -> service.exportBatch(FinanceExportProviderService.RECONCILIATION_STATEMENT_LIST,
request(Map.of("status", "VOIDED"), first.nextCursor(), first.snapshotToken())))
.isInstanceOf(BizException.class).hasMessageContaining("snapshotToken");
}
private ExportBatchRequest request(Map<String, Object> parameters, String cursor, String snapshot) {
return new ExportBatchRequest("9001", "0", "PLATFORM", "7", 1, parameters, cursor, snapshot, 100);
}
}