Test #1
@@ -4788,7 +4788,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 '实际开船时间',
|
||||
@@ -9507,7 +9508,7 @@ CREATE TABLE IF NOT EXISTS fl_customs_supplement_request (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_submission_batch (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关代理提交批次逻辑ID', 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_name VARCHAR(128) NOT NULL COMMENT '本批次接收报关资料的报关代理名称快照', external_reference VARCHAR(128) 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', status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED' COMMENT '代理提交状态:SUBMITTED已提交、ACKNOWLEDGED代理已确认、REJECTED代理已拒绝、COMPLETED代理处理完成', note VARCHAR(500) NULL COMMENT '提交报关代理时的平台备注',
|
||||
submitted_by BIGINT UNSIGNED NOT NULL COMMENT '提交报关代理的平台操作人ID', submitted_by_name VARCHAR(64) NULL COMMENT '提交时的平台操作人名称快照',
|
||||
submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '提交报关代理时间', PRIMARY KEY (id),
|
||||
@@ -9516,6 +9517,26 @@ CREATE TABLE IF NOT EXISTS fl_customs_submission_batch (
|
||||
CONSTRAINT chk_customs_batch_status CHECK (status IN ('SUBMITTED','ACKNOWLEDGED','REJECTED','COMPLETED'))
|
||||
) ENGINE=InnoDB COMMENT='报关代理提交批次及冻结文件版本';
|
||||
|
||||
-- 供应商引用快照收敛:历史名称无法反推Supplier ID,保留为空;新写入由Firstleg服务校验并持久化。
|
||||
DROP PROCEDURE IF EXISTS phase3_supplier_snapshot_add_columns;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE phase3_supplier_snapshot_add_columns()
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='fl_container_booking')
|
||||
AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='fl_container_booking' AND column_name='terminal_trucking_company_id') THEN
|
||||
ALTER TABLE fl_container_booking
|
||||
ADD COLUMN terminal_trucking_company_id BIGINT UNSIGNED NULL COMMENT '码头拖车Supplier逻辑ID快照;历史数据无法推导时为空' AFTER max_weight_kg;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='fl_customs_submission_batch')
|
||||
AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='fl_customs_submission_batch' AND column_name='customs_agent_id') THEN
|
||||
ALTER TABLE fl_customs_submission_batch
|
||||
ADD COLUMN customs_agent_id BIGINT UNSIGNED NULL COMMENT '本批次报关代理Supplier逻辑ID快照;历史数据无法推导时为空' AFTER batch_version;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL phase3_supplier_snapshot_add_columns();
|
||||
DROP PROCEDURE phase3_supplier_snapshot_add_columns;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_receipt (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关回执文件版本逻辑ID', customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', submission_batch_id BIGINT UNSIGNED NOT NULL COMMENT '产生本回执的报关代理提交批次ID',
|
||||
receipt_type VARCHAR(16) NOT NULL COMMENT '回执类型:RECEIVED已收件、DECLARED已申报、REJECTED已拒绝、INSPECTION查验通知、RELEASED放行回执、OTHER其他回执', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务回执文件逻辑ID;跨库不建立外键', receipt_version INT UNSIGNED NOT NULL COMMENT '同一提交批次和回执类型下的版本序号',
|
||||
@@ -11998,6 +12019,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'max_weight
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'shipping_company_id', '船司ID快照');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'shipping_company_name', '船司名称快照');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'terminal_trucking_company', '码头拖车公司');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'terminal_trucking_company_id', '码头拖车Supplier逻辑ID快照;历史数据无法推导时为空');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'updated_at', '货柜资料最后更新时间');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'updated_by', '最后修改人ID');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'updated_by_name', '最后修改人名称快照');
|
||||
@@ -12167,6 +12189,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'visibility',
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'batch_no', '全局唯一代理提交批次号');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'batch_version', '同一报关协同单内的提交批次版本序号');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'customs_agent_name', '本批次接收报关资料的报关代理名称快照');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'customs_agent_id', '本批次报关代理Supplier逻辑ID快照;历史数据无法推导时为空');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'customs_case_id', '所属报关协同单ID');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'external_reference', '报关代理或外部系统返回的业务参考号');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'file_snapshot_json', '提交时冻结的已审核资料数组;元素包含documentId、fileId、itemCode、documentVersion和fileHash');
|
||||
|
||||
+1
@@ -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));
|
||||
}
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
- 负责头程订单主数据、货物明细、快照、状态编排、幂等、Outbox 和内部货物查询。
|
||||
- 订单交易状态归本模块;履约/入库状态归 `firstleg/`,仓储资源归 `warehouse/`,不得跨库读取。
|
||||
- `tenant_id` 是客户隔离键;`customer_id` 是业务客户/往来单位快照,不能替代前者。
|
||||
- `tenant_id` 是客户隔离键;`customer_id` 是客户/企业账号 ID,商户下单时固定来自认证上下文,不得指向 Warehouse 往来单位。发货单位、收货单位只保存为订单 Party 引用与业务快照。
|
||||
|
||||
## 模块入口
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.baiyue.order.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name="warehouse-service", contextId="warehousePartnerClient",
|
||||
url="${baiyue.clients.warehouse.url:}", path="/internal/warehouse-partners")
|
||||
public interface PartnerClient {
|
||||
@GetMapping("/{tenantId}/{id}/active")
|
||||
ApiResponse<Partner> active(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable("tenantId") long tenantId, @PathVariable("id") long id);
|
||||
record Partner(Long id, Long tenantId, String partnerName, String status,
|
||||
String partnerType, List<String> types) { }
|
||||
}
|
||||
@@ -28,7 +28,8 @@ public final class OrderRequests {
|
||||
|
||||
@Schema(name = "FirstLegOrderSaveRequest", description = "First-leg order create or update request / 头程订单新增或更新请求")
|
||||
public record SaveRequest(
|
||||
@Schema(description = "Customer ID / 客户 ID", example = "30001") Long customerId,
|
||||
@Schema(description = "Deprecated and ignored. Customer ID is always taken from authenticated enterprise context / 已废弃并忽略,客户 ID 固定取认证企业上下文", deprecated = true)
|
||||
Long customerId,
|
||||
@Schema(description = "Customer name / 客户名称", example = "Shenzhen Example Trading Co., Ltd. / 深圳示例贸易有限公司", maxLength = 128, requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank @Size(max = 128) String customerName,
|
||||
@Schema(description = "Transport mode / 运输方式", example = "SEA", allowableValues = {"SEA", "AIR", "RAIL", "TRUCK"}, requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
|
||||
@@ -590,7 +590,8 @@ components:
|
||||
items: {$ref: '#/components/schemas/FirstLegOrderCargo'}
|
||||
description: >-
|
||||
订单保存请求中的完整字段以服务端 FirstLegOrderSaveRequest 为准;pricingTaxMode
|
||||
是新建和编辑订单的必填报价维度;历史存量订单的响应字段仍允许为空。
|
||||
是新建和编辑订单的必填报价维度;customerId 不接受客户端指定,服务端始终使用认证企业 ID;
|
||||
发货单位和收货单位仅通过 sender/receiver 保存业务快照。历史存量订单的响应字段仍允许为空。
|
||||
FirstLegBatchDraftItem:
|
||||
type: object
|
||||
required: [clientRowId, draft]
|
||||
|
||||
@@ -12,7 +12,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class WarehouseFeignContractTest {
|
||||
@Test void clientsUseNacosServiceNameAndInternalPathsByDefault() throws Exception {
|
||||
assertClient(WarehouseClient.class, "/internal/warehouses");
|
||||
assertClient(PartnerClient.class, "/internal/warehouse-partners");
|
||||
}
|
||||
|
||||
private void assertClient(Class<?> type, String expectedPath) throws Exception {
|
||||
|
||||
@@ -363,7 +363,7 @@ class OrderApplicationServiceStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicDestinationCanBeSavedWithoutPartnerSenderOrReceiver() {
|
||||
void publicDestinationUsesAuthenticatedEnterpriseAsCustomerWithoutPartySnapshots() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderEligibilityGuard eligibility = mock(OrderEligibilityGuard.class);
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
@@ -374,7 +374,7 @@ class OrderApplicationServiceStateTest {
|
||||
service(repository, eligibility, masterData)
|
||||
.createDraft(20002L, 2L, request, "public-destination-no-parties");
|
||||
|
||||
verify(repository).insertOrder(argThat(order -> order.getCustomerId() == null
|
||||
verify(repository).insertOrder(argThat(order -> Long.valueOf(20002L).equals(order.getCustomerId())
|
||||
&& "当前企业".equals(order.getCustomerName())));
|
||||
verify(repository, never()).insertParty(any());
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ class OrderMasterDataGuardTest {
|
||||
"CN", null, "深圳", null, "地址", null);
|
||||
var cargo = new OrderRequests.Cargo("货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
|
||||
null, null, null, null, null, false, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null);
|
||||
null, null, null, null, null, null, null, null, null, null, null);
|
||||
return new OrderRequests.SaveRequest(null, "当前企业", "SEA", null, "CN", destinationWarehouseId,
|
||||
"目的仓", null, null, party, party, List.of(cargo), "PICKUP", receivingWarehouseId,
|
||||
"收货仓", "CH", "渠道", null, null, "BUY_DECLARATION", null, "LCL", false,
|
||||
|
||||
+12
-1
@@ -28,7 +28,10 @@ class WarehouseSchemaContractTest {
|
||||
"contact_phone", "contact_email", "country_code", "province", "city", "district", "address_line",
|
||||
"postal_code", "status", "remark");
|
||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_partner_type")
|
||||
.contains("idx_wh_partner_type_scope");
|
||||
.contains("idx_wh_partner_type_scope")
|
||||
.contains("chk_wh_partner_type CHECK (partner_type IN ('CUSTOMER','CARRIER','SHIPPER','CONSIGNEE'))")
|
||||
.contains("chk_wh_partner_type_value CHECK (partner_type IN ('CUSTOMER','CARRIER','SHIPPER','CONSIGNEE'))")
|
||||
.doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH");
|
||||
assertBaseColumns(sql);
|
||||
}
|
||||
|
||||
@@ -138,6 +141,14 @@ class WarehouseSchemaContractTest {
|
||||
.contains("库位活动预占或占用唯一约束不完整");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedSqlUsesOnlyNonSupplierPartnerTypes() throws IOException {
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
assertThat(tableDefinition(unified, "wh_partner"))
|
||||
.contains("chk_wh_partner_type CHECK (partner_type IN ('CUSTOMER','CARRIER','SHIPPER','CONSIGNEE'))")
|
||||
.doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH");
|
||||
}
|
||||
|
||||
private static String read(String file) throws IOException {
|
||||
return Files.readString(Path.of("src/main/resources/db", file));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user