This commit is contained in:
2026-08-13 20:00:30 +08:00
parent 50251240f4
commit 5bc62a109e
28 changed files with 602 additions and 176 deletions
@@ -9,9 +9,11 @@ import java.util.List;
@FeignClient(name="firstleg-service", contextId="financeFirstlegCostSnapshotClient", url="${baiyue.clients.firstleg.url:}", path="/internal/firstleg/v1/finance-cost-snapshots")
public interface FirstlegCostSnapshotClient {
@GetMapping("/customers/{customerId}/containers/{containerId}")
ApiResponse<ContainerSnapshot> container(@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long customerId, @PathVariable long containerId);
record ContainerSnapshot(long containerId, long customerId, List<OrderVolume> orders) {}
@GetMapping("/customers/{customerId}/loading-plans/{loadingPlanId}")
ApiResponse<ContainerSnapshot> loadingPlan(@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long customerId,
@PathVariable long loadingPlanId);
record ContainerSnapshot(long loadingPlanId, long containerBookingId, long customerId,
List<OrderVolume> orders) {}
record OrderVolume(long orderId, Long inboundId, BigDecimal volumeCbm) {}
}
@@ -175,12 +175,20 @@ public class PlatformFinanceController {
java.util.List<FinanceService.ContainerOrderVolume> volumes = java.util.List.of();
if ("CONTAINER_SHARED".equals(request.costType())) {
if (request.containerId() == null) throw new IllegalArgumentException("整柜成本必须指定柜次");
var response = costSnapshotClient.container(fileToken, request.customerId(), request.containerId());
var response = costSnapshotClient.loadingPlan(fileToken, request.customerId(), request.containerId());
var snapshot = response == null ? null : response.data();
if (snapshot == null || snapshot.customerId() != request.customerId() || snapshot.containerId() != request.containerId() || snapshot.orders() == null || snapshot.orders().isEmpty()) {
if (response == null || !response.successful()) {
throw new IllegalArgumentException(response == null || response.message() == null
? "Firstleg柜内订单体积快照不可用" : response.message());
}
if (snapshot == null || snapshot.customerId() != request.customerId()
|| snapshot.loadingPlanId() != request.containerId()
|| snapshot.containerBookingId() <= 0
|| snapshot.orders() == null || snapshot.orders().isEmpty()) {
throw new IllegalArgumentException("Firstleg未返回有效柜内订单体积快照");
}
volumes = snapshot.orders().stream().map(v -> new FinanceService.ContainerOrderVolume(v.orderId(), v.inboundId(), snapshot.containerId(), snapshot.customerId(), v.volumeCbm())).toList();
volumes = snapshot.orders().stream().map(v -> new FinanceService.ContainerOrderVolume(v.orderId(),
v.inboundId(), snapshot.loadingPlanId(), snapshot.customerId(), v.volumeCbm())).toList();
}
return FinanceResponses.success(resourceView(service.createCost(request.customerId(), user.userId(), internal, volumes, key)));
}
@@ -65,6 +65,13 @@ CREATE TABLE IF NOT EXISTS fin_subject (
UNIQUE KEY uk_fin_subject_code (subject_code)
) ENGINE=InnoDB COMMENT='平台财务主体;首期仅一个有效主体';
INSERT INTO fin_subject
(subject_code, subject_name, status, version, created_by, updated_by)
VALUES
('PLATFORM_DEFAULT', '平台', 'ACTIVE', 0, 0, 0)
ON DUPLICATE KEY UPDATE
subject_name = VALUES(subject_name), status = 'ACTIVE', updated_by = 0;
CREATE TABLE IF NOT EXISTS fin_customer_settlement_profile (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
@@ -26,7 +26,7 @@ class FinanceContractTest {
assertTrue(service.contains("status IN ('ISSUED','PARTIALLY_PAID','PAID')"));
String controller = Files.readString(Path.of("src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java"));
assertTrue(controller.contains("/vouchers") && controller.contains("MultipartFile"));
assertTrue(controller.contains("costSnapshotClient.container") && controller.contains("createCost(request.customerId(), user.userId(), internal, volumes, key)"));
assertTrue(controller.contains("costSnapshotClient.loadingPlan") && controller.contains("createCost(request.customerId(), user.userId(), internal, volumes, key)"));
assertTrue(service.contains("validateVoucherFiles(tenantId, request.voucherFileIds())"));
String operations = Files.readString(Path.of("src/main/java/com/baiyue/finance/service/FinancePhase1OperationService.java"));
assertTrue(operations.contains("bl.source_id=f.id"));
@@ -0,0 +1,25 @@
package com.baiyue.finance;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class FinanceSubjectSeedContractTest {
@Test
void moduleAndUnifiedSqlSeedOneActivePlatformSubjectIdempotently() throws Exception {
String module = Files.readString(Path.of("src/main/resources/db/finance.sql"), StandardCharsets.UTF_8);
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
for (String sql : new String[] {module, unified}) {
assertThat(sql)
.contains("INSERT INTO fin_subject")
.contains("('PLATFORM_DEFAULT', '平台', 'ACTIVE', 0, 0, 0)")
.contains("ON DUPLICATE KEY UPDATE")
.contains("status = 'ACTIVE'");
}
}
}
@@ -14,6 +14,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -45,15 +46,15 @@ class PlatformFinanceControllerBehaviorTest {
var request = new PlatformFinanceController.CostRequest(
10L, "CONTAINER_SHARED", null, 88L, "清关费", new BigDecimal("300.00"),
"CNY", List.of("91"), "整柜成本");
var snapshot = new FirstlegCostSnapshotClient.ContainerSnapshot(88L, 10L, List.of(
var snapshot = new FirstlegCostSnapshotClient.ContainerSnapshot(88L, 99L, 10L, List.of(
new FirstlegCostSnapshotClient.OrderVolume(101L, 1001L, new BigDecimal("1.25")),
new FirstlegCostSnapshotClient.OrderVolume(102L, 1002L, new BigDecimal("2.75"))));
when(snapshotClient.container("internal-token", 10L, 88L)).thenReturn(ApiResponse.success(snapshot));
when(snapshotClient.loadingPlan("internal-token", 10L, 88L)).thenReturn(ApiResponse.success(snapshot));
when(service.createCost(eq(10L), eq(7L), any(), any(), eq("cost-key"))).thenReturn(Map.of("id", 1L));
controller.cost(user, "cost-key", request);
verify(snapshotClient).container("internal-token", 10L, 88L);
verify(snapshotClient).loadingPlan("internal-token", 10L, 88L);
verify(service).createCost(eq(10L), eq(7L), any(), eq(List.of(
new FinanceService.ContainerOrderVolume(101L, 1001L, 88L, 10L, new BigDecimal("1.25")),
new FinanceService.ContainerOrderVolume(102L, 1002L, 88L, 10L, new BigDecimal("2.75")))), eq("cost-key"));
@@ -64,8 +65,35 @@ class PlatformFinanceControllerBehaviorTest {
var request = new PlatformFinanceController.CostRequest(
10L, "CONTAINER_SHARED", null, 88L, "拖车费", BigDecimal.TEN,
"CNY", List.of("91"), null);
when(snapshotClient.container("internal-token", 10L, 88L)).thenReturn(ApiResponse.success(
new FirstlegCostSnapshotClient.ContainerSnapshot(88L, 10L, List.of())));
when(snapshotClient.loadingPlan("internal-token", 10L, 88L)).thenReturn(ApiResponse.success(
new FirstlegCostSnapshotClient.ContainerSnapshot(88L, 99L, 10L, List.of())));
assertThatThrownBy(() -> controller.cost(user, "cost-key", request))
.hasMessageContaining("未返回有效柜内订单体积快照");
verify(service, never()).createCost(any(Long.class), any(Long.class), any(), any(), any());
}
@Test
void containerCostPreservesFirstlegBusinessFailure() {
var request = new PlatformFinanceController.CostRequest(
10L, "CONTAINER_SHARED", null, 88L, "拖车费", BigDecimal.TEN,
"CNY", List.of("91"), null);
when(snapshotClient.loadingPlan("internal-token", 10L, 88L)).thenReturn(
new ApiResponse<>("A0404", "柜次不存在", null, "request", "trace", Instant.now()));
assertThatThrownBy(() -> controller.cost(user, "cost-key", request))
.hasMessage("柜次不存在");
verify(service, never()).createCost(any(Long.class), any(Long.class), any(), any(), any());
}
@Test
void containerCostRejectsMismatchedLoadingPlanIdentity() {
var request = new PlatformFinanceController.CostRequest(
10L, "CONTAINER_SHARED", null, 88L, "拖车费", BigDecimal.TEN,
"CNY", List.of("91"), null);
var snapshot = new FirstlegCostSnapshotClient.ContainerSnapshot(89L, 99L, 10L, List.of(
new FirstlegCostSnapshotClient.OrderVolume(101L, 1001L, BigDecimal.ONE)));
when(snapshotClient.loadingPlan("internal-token", 10L, 88L)).thenReturn(ApiResponse.success(snapshot));
assertThatThrownBy(() -> controller.cost(user, "cost-key", request))
.hasMessageContaining("未返回有效柜内订单体积快照");