This commit is contained in:
2026-08-15 11:12:19 +08:00
parent 557ab75319
commit c4adba57db
22 changed files with 95 additions and 32 deletions
@@ -11,6 +11,7 @@ public class CustomsSubmissionBatchEntity {
private Long customsCaseId;
private String batchNo;
private Integer batchVersion;
private Long customsAgentId;
private String customsAgentName;
private String externalReference;
private String fileSnapshotJson;
@@ -181,6 +181,7 @@ public class CustomsService {
batch.setId(IdWorker.getId()); batch.setCustomsCaseId(caseId); batch.setBatchVersion(repository.batches(caseId).size() + 1);
batch.setBatchNo("CB" + batch.getId());
var customsAgent = suppliers.customs(request.customsAgentId(), request.customsAgentName());
batch.setCustomsAgentId(customsAgent == null ? null : customsAgent.supplierId());
batch.setCustomsAgentName(customsAgent == null ? null : customsAgent.name());
batch.setExternalReference(trim(request.externalReference())); batch.setFileSnapshotJson(json(current.stream()
.map(v -> Map.of("documentId", v.getId(), "fileId", v.getFileId(), "itemCode", v.getChecklistItemCode(),
@@ -64,6 +64,7 @@ public final class ContainerDtos {
@Size(max = 128) String destinationWarehouseName,
@DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
@DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg,
@Positive Long terminalTruckingCompanyId,
@Size(max = 128) String terminalTruckingCompany,
LocalDateTime loadingAt,
LocalDateTime estimatedSailingAt,
@@ -42,6 +42,7 @@ public class ContainerBookingEntity {
private String destinationWarehouseName;
private BigDecimal capacityCbm;
private BigDecimal maxWeightKg;
private Long terminalTruckingCompanyId;
private String terminalTruckingCompany;
private LocalDateTime loadingAt;
private LocalDateTime estimatedSailingAt;
@@ -85,7 +85,8 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
departure_port_name=#{value.departurePortName}, destination_port_code=#{value.destinationPortCode},
destination_port_name=#{value.destinationPortName}, destination_warehouse_code=#{value.destinationWarehouseCode},
destination_warehouse_name=#{value.destinationWarehouseName}, capacity_cbm=#{value.capacityCbm},
max_weight_kg=#{value.maxWeightKg}, terminal_trucking_company=#{value.terminalTruckingCompany},
max_weight_kg=#{value.maxWeightKg}, terminal_trucking_company_id=#{value.terminalTruckingCompanyId},
terminal_trucking_company=#{value.terminalTruckingCompany},
loading_at=#{value.loadingAt}, estimated_sailing_at=#{value.estimatedSailingAt},
actual_sailing_at=#{value.actualSailingAt}, estimated_port_arrival_at=#{value.estimatedPortArrivalAt},
actual_port_arrival_at=#{value.actualPortArrivalAt},
@@ -44,6 +44,7 @@ public class ContainerService {
@Resource private LoadingPickService loadingPickService;
@Resource private PriceBookQueryRepository priceBooks;
@Resource private OceanBookingRepository oceanBookings;
@Resource private WarehouseSupplierSnapshotService suppliers;
@Autowired(required = false) private ContainerTransportHistoryMapper transportHistories;
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
@@ -130,9 +131,12 @@ public class ContainerService {
}
validateSchedule(request.data());
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(request.data().warehouseId());
var trucking = suppliers.trucking(request.data().terminalTruckingCompanyId(), request.data().terminalTruckingCompany());
ensureUnique(trim(request.data().containerNo()), request.data().cabinetNo().trim(), id);
copy(current, request.data());
current.setWarehouseId(warehouse.id()); current.setWarehouseName(warehouse.warehouseName());
current.setTerminalTruckingCompanyId(trucking == null ? null : trucking.supplierId());
current.setTerminalTruckingCompany(trucking == null ? null : trucking.name());
try {
if (repository.updateDraft(current, request.version(), operatorId, operatorName) != 1) {
throw conflict("货柜状态或版本已变化,请刷新后重试");
@@ -100,6 +100,7 @@ CREATE TABLE IF NOT EXISTS fl_customs_submission_batch (
customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID',
batch_no VARCHAR(40) NOT NULL COMMENT '全局唯一代理提交批次号',
batch_version INT UNSIGNED NOT NULL COMMENT '同一报关协同单内的提交批次版本序号',
customs_agent_id BIGINT UNSIGNED NULL COMMENT '本批次报关代理Supplier逻辑ID快照;历史数据无法推导时为空',
customs_agent_name VARCHAR(128) NOT NULL COMMENT '本批次接收报关资料的报关代理名称快照',
external_reference VARCHAR(128) NULL COMMENT '报关代理或外部系统返回的业务参考号',
file_snapshot_json JSON NOT NULL COMMENT '提交时冻结的已审核资料数组;元素包含documentId、fileId、itemCode、documentVersion和fileHash',
@@ -53,7 +53,8 @@ CREATE TABLE IF NOT EXISTS fl_container_booking (
destination_warehouse_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
capacity_cbm DECIMAL(18,4) NULL COMMENT '货柜容量m3',
max_weight_kg DECIMAL(18,3) NULL COMMENT '货柜最大承重kg',
terminal_trucking_company VARCHAR(128) NULL COMMENT '码头拖车公司',
terminal_trucking_company_id BIGINT UNSIGNED NULL COMMENT '码头拖车Supplier逻辑ID快照;历史数据无法推导时为空',
terminal_trucking_company VARCHAR(128) NULL COMMENT '码头拖车公司名称快照',
loading_at DATETIME(3) NULL COMMENT '计划或实际装柜时间',
estimated_sailing_at DATETIME(3) NULL COMMENT '预计开船时间',
actual_sailing_at DATETIME(3) NULL COMMENT '实际开船时间',
@@ -57,7 +57,12 @@ paths:
summary: Submit to customs agent and freeze approved file versions in a batch snapshot
description: customsAgentId must identify an ACTIVE Warehouse supplier in category CUSTOMS; the persisted agent name is resolved server-side and free text cannot bypass supplier validation.
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/SubmitBatchRequest'}}}}
responses: {'200': {description: Submission batch}}
responses:
'200':
description: Submission batch
content:
application/json:
schema: {$ref: '#/components/schemas/CustomsSubmissionBatch'}
/api/platform/v1/customs/submission-batches/{batchId}/receipts:
parameters:
- {name: batchId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
@@ -103,3 +108,20 @@ components:
externalReference: {type: string, maxLength: 128, nullable: true}
note: {type: string, maxLength: 500, nullable: true}
expectedVersion: {type: integer, minimum: 0}
CustomsSubmissionBatch:
type: object
required: [id, customsCaseId, batchNo, batchVersion, customsAgentName, status, submittedBy, submittedAt]
properties:
id: {type: string, pattern: '^[1-9][0-9]*$'}
customsCaseId: {type: string, pattern: '^[1-9][0-9]*$'}
batchNo: {type: string, maxLength: 40}
batchVersion: {type: integer, minimum: 1}
customsAgentId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: Server-validated CUSTOMS supplier ID snapshot; null only for non-migrated historical rows.}
customsAgentName: {type: string, maxLength: 128, description: Server-frozen Warehouse supplier name snapshot.}
externalReference: {type: string, maxLength: 128, nullable: true}
fileSnapshotJson: {type: string}
status: {type: string, enum: [SUBMITTED, ACKNOWLEDGED, REJECTED, COMPLETED]}
note: {type: string, maxLength: 500, nullable: true}
submittedBy: {type: string, pattern: '^[1-9][0-9]*$'}
submittedByName: {type: string, maxLength: 64, nullable: true}
submittedAt: {type: string, format: date-time}
@@ -1234,7 +1234,8 @@ components:
destinationWarehouseName: {type: string, maxLength: 128, nullable: true}
capacityCbm: {type: number, exclusiveMinimum: true, minimum: 0, nullable: true}
maxWeightKg: {type: number, exclusiveMinimum: true, minimum: 0, nullable: true}
terminalTruckingCompany: {type: string, maxLength: 128, nullable: true}
terminalTruckingCompanyId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: ACTIVE Warehouse Supplier ID validated as TRUCKING}
terminalTruckingCompany: {type: string, maxLength: 128, nullable: true, description: Server-frozen Warehouse supplier name snapshot}
loadingAt: {type: string, format: date-time, nullable: true}
estimatedSailingAt: {type: string, format: date-time, nullable: true}
actualSailingAt: {type: string, format: date-time, nullable: true}
@@ -1266,7 +1267,8 @@ components:
destinationWarehouseName: {type: string, maxLength: 128, nullable: true}
capacityCbm: {type: number, exclusiveMinimum: true, minimum: 0, nullable: true}
maxWeightKg: {type: number, exclusiveMinimum: true, minimum: 0, nullable: true}
terminalTruckingCompany: {type: string, maxLength: 128, nullable: true}
terminalTruckingCompanyId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: ACTIVE Warehouse Supplier ID validated as TRUCKING}
terminalTruckingCompany: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint; persisted name always comes from Warehouse and cannot replace the ID}
loadingAt: {type: string, format: date-time, nullable: true}
estimatedSailingAt: {type: string, format: date-time, nullable: true}
actualSailingAt: {type: string, format: date-time, nullable: true}
@@ -91,6 +91,8 @@ class CustomsServiceTest {
new SubmitBatchRequest(88L, "线下报关代理", "EXT-1", "提交", 4));
assertThat(result.getFileSnapshotJson()).contains("documentVersion").contains("INVOICE");
assertThat(result.getCustomsAgentId()).isEqualTo(88L);
assertThat(result.getCustomsAgentName()).isEqualTo("线下报关代理");
verify(repository).freeze(51L);
verify(repository).insert(any(CustomsSubmissionBatchEntity.class));
}
@@ -30,6 +30,9 @@ class Phase3FinalUnifiedSqlContractTest {
.contains("CREATE TABLE IF NOT EXISTS fl_customs_case")
.contains("CREATE TABLE IF NOT EXISTS fl_customs_document")
.contains("CREATE TABLE IF NOT EXISTS fl_customs_submission_batch")
.contains("customs_agent_id BIGINT UNSIGNED NULL")
.contains("terminal_trucking_company_id BIGINT UNSIGNED NULL")
.contains("phase3_supplier_snapshot_add_columns")
.contains("CREATE TABLE IF NOT EXISTS fl_customs_receipt")
.contains("CREATE TABLE IF NOT EXISTS fl_port_schedule")
.contains("CREATE TABLE IF NOT EXISTS fl_port_schedule_history")
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
import com.baiyue.firstleg.client.WarehouseSupplierClient;
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
@@ -51,6 +52,7 @@ class ContainerServiceTest {
@MockBean private LoadingPickService loadingPickService;
@MockBean private PriceBookQueryRepository priceBooks;
@MockBean private OceanBookingRepository oceanBookings;
@MockBean private WarehouseSupplierSnapshotService suppliers;
@MockBean private ContainerTransportHistoryMapper transportHistories;
@Resource private ContainerService service;
@@ -58,6 +60,8 @@ class ContainerServiceTest {
void setUpWarehouse() {
when(warehouses.active(anyString(), anyLong(), anyLong())).thenAnswer(invocation ->
activeWarehouse(invocation.getArgument(2, Long.class), "DOMESTIC", "上海中转仓"));
when(suppliers.trucking(202L, "拖车公司A"))
.thenReturn(new WarehouseSupplierClient.Snapshot(202L, "拖车公司A", "TRUCKING", null, "ACTIVE"));
}
@Test
@@ -320,7 +324,7 @@ class ContainerServiceTest {
private static SaveRequest request(String cabinetNo) {
return new SaveRequest("MSCU1234567", cabinetNo, 101L, "上海仓", "BOL-001", "柏悦轮",
"V001", "40HQ", 201L, "船司A", "CNSHA", "上海港", "USLAX", "洛杉矶港",
"ONT8", "ONT8", new BigDecimal("68.0000"), new BigDecimal("26000.000"), "拖车公司A",
"ONT8", "ONT8", new BigDecimal("68.0000"), new BigDecimal("26000.000"), 202L, "拖车公司A",
LocalDateTime.of(2026, 8, 10, 8, 0), LocalDateTime.of(2026, 8, 12, 8, 0), null,
LocalDateTime.of(2026, 8, 25, 8, 0), null,
LocalDateTime.of(2026, 8, 27, 8, 0), null);