This commit is contained in:
2026-08-17 14:46:44 +08:00
parent b914bb7532
commit a34b5d5ba0
38 changed files with 249 additions and 115 deletions
@@ -14,8 +14,7 @@ public interface WarehouseSupplierClient {
ApiResponse<Snapshot> get(@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long supplierId,
@RequestParam(defaultValue = "true") boolean requireActive,
@RequestParam(required = false) String category,
@RequestParam(required = false) String scope);
@RequestParam(required = false) String category);
record Snapshot(long supplierId, String name, String category, String scope, String status) {}
record Snapshot(long supplierId, String name, String category, String status) {}
}
@@ -127,7 +127,8 @@ public final class LoadingPickDtos {
public record Task(Long id, Long loadingPlanId, Long containerBookingId, Long inboundId,
Integer inboundVersion, Integer loadingVersion, String cabinetNo,
String currentContainerNo, String taskStatus,
String currentContainerNo, String sourceType, String recordStatus,
Long mergedIntoContainerBookingId, String taskStatus,
String workflowStatus, Integer version, Assignment assignment,
Review review, List<PickItem> items) {
}
@@ -135,6 +136,7 @@ public final class LoadingPickDtos {
/** PDA 工作台只展示当前作业员可领取或已领取的进行中任务。 */
public record PendingTask(Long taskId, Long loadingPlanId, Long containerBookingId,
String cabinetNo, String currentContainerNo,
String sourceType, String recordStatus, Long mergedIntoContainerBookingId,
Long warehouseId, String warehouseName, String taskStatus,
String workflowStatus, Integer version, Integer assignmentVersion,
Integer itemCount, Integer pendingItemCount) {
@@ -49,6 +49,8 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
SELECT task.id AS taskId, task.loading_plan_id AS loadingPlanId,
task.container_booking_id AS containerBookingId, task.cabinet_no AS cabinetNo,
booking.container_no AS containerNo, booking.warehouse_id AS warehouseId,
booking.source_type AS sourceType, booking.record_status AS recordStatus,
booking.merged_into_container_booking_id AS mergedIntoContainerBookingId,
booking.warehouse_name AS warehouseName, task.task_status AS taskStatus,
CASE WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED' ELSE 'IN_PROGRESS' END AS workflowStatus,
task.version AS version, COALESCE(assignment.version, 0) AS assignmentVersion,
@@ -67,6 +69,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
</if>
GROUP BY task.id, task.loading_plan_id, task.container_booking_id, task.cabinet_no,
booking.container_no, booking.warehouse_id,
booking.source_type, booking.record_status, booking.merged_into_container_booking_id,
booking.warehouse_name, task.task_status, task.version,
assignment.pick_task_id, assignment.version
ORDER BY task.id DESC
@@ -82,6 +85,9 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
private String containerNo;
private Long containerBookingId;
private String cabinetNo;
private String sourceType;
private String recordStatus;
private Long mergedIntoContainerBookingId;
private Long warehouseId;
private String warehouseName;
private String taskStatus;
@@ -101,6 +107,12 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
public void setContainerBookingId(Long value) { this.containerBookingId = value; }
public String getCabinetNo() { return cabinetNo; }
public void setCabinetNo(String value) { this.cabinetNo = value; }
public String getSourceType() { return sourceType; }
public void setSourceType(String sourceType) { this.sourceType = sourceType; }
public String getRecordStatus() { return recordStatus; }
public void setRecordStatus(String recordStatus) { this.recordStatus = recordStatus; }
public Long getMergedIntoContainerBookingId() { return mergedIntoContainerBookingId; }
public void setMergedIntoContainerBookingId(Long value) { this.mergedIntoContainerBookingId = value; }
public Long getWarehouseId() { return warehouseId; }
public void setWarehouseId(Long warehouseId) { this.warehouseId = warehouseId; }
public String getWarehouseName() { return warehouseName; }
@@ -185,6 +185,7 @@ public class LoadingPickService {
return repository.pendingTasks(operatorId, normalizedKeyword).stream()
.map(row -> new PendingTask(row.getTaskId(), row.getLoadingPlanId(), row.getContainerBookingId(),
row.getCabinetNo(), row.getContainerNo(),
row.getSourceType(), row.getRecordStatus(), row.getMergedIntoContainerBookingId(),
row.getWarehouseId(), row.getWarehouseName(), row.getTaskStatus(), row.getWorkflowStatus(),
row.getVersion(), row.getAssignmentVersion(), row.getItemCount(), row.getPendingItemCount()))
.toList();
@@ -607,6 +608,9 @@ public class LoadingPickService {
: assigned == null ? "UNASSIGNED"
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
var booking = smartLoadingRepository.booking(task.getContainerBookingId());
if (booking == null || !"ACTIVE".equals(booking.getRecordStatus())) {
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或柜次已非有效存续记录");
}
var owners = repository.inboundOwners(task.getId());
Long detailInboundId = source.isEmpty() ? null : source.get(0).getInboundId();
var owner = detailInboundId == null ? null : owners.stream()
@@ -616,7 +620,8 @@ public class LoadingPickService {
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerBookingId(),
owner == null ? null : owner.inboundId(), inbound == null ? null : inbound.version(),
inbound == null ? null : inbound.version(), task.getCabinetNo(),
booking == null ? null : booking.getContainerNo(), task.getTaskStatus(),
booking.getContainerNo(), booking.getSourceType(), booking.getRecordStatus(),
booking.getMergedIntoContainerBookingId(), task.getTaskStatus(),
workflowStatus, task.getVersion(), assignment, review, items);
}
@@ -15,29 +15,32 @@ public class WarehouseSupplierSnapshotService {
@Value("${baiyue.security.internal-service-token}") private String internalToken;
public WarehouseSupplierClient.Snapshot booking(Long supplierId, String suppliedName) {
return resolve(supplierId, suppliedName, "BOOKING", "INTERNATIONAL");
return resolve(supplierId, suppliedName, "BOOKING");
}
public WarehouseSupplierClient.Snapshot trucking(Long supplierId, String suppliedName) {
return resolve(supplierId, suppliedName, "TRUCKING", null);
return resolve(supplierId, suppliedName, "TRUCKING");
}
public WarehouseSupplierClient.Snapshot customs(Long supplierId, String suppliedName) {
return resolve(supplierId, suppliedName, "CUSTOMS", null);
return resolve(supplierId, suppliedName, "CUSTOMS");
}
private WarehouseSupplierClient.Snapshot resolve(Long supplierId, String suppliedName,
String category, String scope) {
String category) {
if (supplierId == null || supplierId <= 0) {
if (StringUtils.hasText(suppliedName)) throw new BizException(ResultCode.BAD_REQUEST,
"供应商名称必须来自有效的供应商ID,禁止自由文本");
return null;
}
try {
var response = client.get(internalToken, supplierId, true, category, scope);
var response = client.get(internalToken, supplierId, true, category);
if (response == null || !response.successful() || response.data() == null
|| response.data().supplierId() != supplierId
|| !"ACTIVE".equals(response.data().status())
|| !category.equals(response.data().category())
|| !StringUtils.hasText(response.data().name())) {
throw new BizException(ResultCode.CONFLICT, "所选供应商不存在、未启用或类别/范围不匹配");
throw new BizException(ResultCode.CONFLICT, "所选供应商不存在、未启用或类别不匹配");
}
if (StringUtils.hasText(suppliedName)
&& !suppliedName.trim().equals(response.data().name())) {
@@ -80,7 +80,7 @@ components:
warehouseId: {type: integer, format: int64, minimum: 1, description: ACTIVE domestic transit warehouse ID; warehouseName is resolved and frozen server-side.}
shippingCompanyId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional shipping-company master-data ID; the carrier is independent from the logistics channel and booking-agent Supplier.}
shippingCompanyName: {type: string, maxLength: 128, nullable: true, description: Optional shipping-company name snapshot.}
bookingAgentId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional ACTIVE Warehouse supplier ID validated as BOOKING with INTERNATIONAL scope.}
bookingAgentId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional ACTIVE Warehouse supplier ID validated only as category BOOKING; booking suppliers do not use domestic/international scope.}
bookingAgentName: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint only; the persisted snapshot always comes from Warehouse and cannot be supplied without bookingAgentId.}
serviceTerm: {type: string, maxLength: 32, description: Required free-text Service Term snapshot, for example CY-CY.}
freightTerm: {type: string, maxLength: 32, description: Required free-text Freight Term snapshot, for example NINGBO.}
@@ -34,7 +34,9 @@ paths:
- {$ref: '#/components/parameters/TaskId'}
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
responses:
'200': {description: Task with current transport display fields}
'200':
description: Task with current transport display and ACTIVE container lifecycle fields
content: {application/json: {schema: {$ref: '#/components/schemas/Task'}}}
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/claim:
post:
summary: Atomically claim an unassigned task for the authenticated operator
@@ -124,7 +126,7 @@ components:
schemas:
Task:
type: object
required: [id, loadingPlanId, containerBookingId, inboundId, inboundVersion, loadingVersion, cabinetNo, taskStatus, workflowStatus, version, items]
required: [id, loadingPlanId, containerBookingId, inboundId, inboundVersion, loadingVersion, cabinetNo, sourceType, recordStatus, taskStatus, workflowStatus, version, items]
properties:
id: {type: integer, format: int64}
loadingPlanId: {type: integer, format: int64}
@@ -134,6 +136,9 @@ components:
loadingVersion: {type: integer, minimum: 0, description: Compatibility alias of inboundVersion from the same fl_inbound_order row; /loading/complete uses this inbound loading-stage version}
cabinetNo: {type: string}
currentContainerNo: {type: string, nullable: true, description: Mutable transport display field; changing it never resynchronizes the task or item versions and it is never task identity}
sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING], description: Source of the surviving stable container identity}
recordStatus: {type: string, enum: [ACTIVE], description: Task detail is served only from the ACTIVE container booking currently owned by the loading plan}
mergedIntoContainerBookingId: {type: integer, format: int64, nullable: true, description: Always null for the surviving ACTIVE operational container; populated only on hidden MERGED source history}
taskStatus: {type: string, enum: [LOADING, COMPLETED]}
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, COMPLETED]}
version: {type: integer, minimum: 0}
@@ -164,13 +169,16 @@ components:
version: {type: integer, minimum: 0}
PendingTask:
type: object
required: [taskId, loadingPlanId, containerBookingId, cabinetNo, warehouseId, taskStatus, workflowStatus, version, assignmentVersion, itemCount, pendingItemCount]
required: [taskId, loadingPlanId, containerBookingId, cabinetNo, sourceType, recordStatus, warehouseId, taskStatus, workflowStatus, version, assignmentVersion, itemCount, pendingItemCount]
properties:
taskId: {type: integer, format: int64}
loadingPlanId: {type: integer, format: int64}
containerBookingId: {type: integer, format: int64}
cabinetNo: {type: string}
currentContainerNo: {type: string, nullable: true}
sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING]}
recordStatus: {type: string, enum: [ACTIVE], description: Enforced by the server-side pending-task query; PDA clients must not infer or relax this filter}
mergedIntoContainerBookingId: {type: integer, format: int64, nullable: true, description: Always null for an ACTIVE pending task}
warehouseId: {type: integer, format: int64}
warehouseName: {type: string, nullable: true}
taskStatus: {type: string, enum: [LOADING]}
@@ -19,13 +19,13 @@ class WarehouseSupplierClientContractTest {
FeignClient annotation = WarehouseSupplierClient.class.getAnnotation(FeignClient.class);
assertThat(annotation.path()).isEqualTo("/internal/warehouse/v1/suppliers");
Method method = WarehouseSupplierClient.class.getDeclaredMethod("get", String.class, long.class,
boolean.class, String.class, String.class);
boolean.class, String.class);
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly("/{supplierId}");
assertThat(method.getParameterAnnotations()[0][0]).isInstanceOf(RequestHeader.class);
assertThat(((RequestHeader) method.getParameterAnnotations()[0][0]).value())
.isEqualTo("X-Internal-Service-Token");
assertThat(method.getParameterAnnotations()[3][0]).isInstanceOf(RequestParam.class);
assertThat(method.getParameterAnnotations()[4][0]).isInstanceOf(RequestParam.class);
assertThat(method.getParameterCount()).isEqualTo(4);
}
@Test
@@ -33,7 +33,9 @@ class WarehouseSupplierClientContractTest {
String ocean = Files.readString(Path.of("src/main/resources/openapi/ocean-bookings-v1.yaml"), StandardCharsets.UTF_8);
String truck = Files.readString(Path.of("src/main/resources/openapi/truck-appointments-v1.yaml"), StandardCharsets.UTF_8);
String customs = Files.readString(Path.of("src/main/resources/openapi/customs-v1.yaml"), StandardCharsets.UTF_8);
assertThat(ocean).contains("bookingAgentId:", "BOOKING", "INTERNATIONAL", "persisted snapshot always comes from Warehouse");
assertThat(ocean).contains("bookingAgentId:", "category BOOKING",
"do not use domestic/international scope", "persisted snapshot always comes from Warehouse")
.doesNotContain("BOOKING with INTERNATIONAL scope");
assertThat(truck).contains("carrierId:", "TRUCKING", "persisted snapshot comes from Warehouse");
assertThat(customs).contains("customsAgentId", "CUSTOMS", "free text cannot bypass supplier validation");
}
@@ -36,6 +36,9 @@ class PdaConvergenceOpenApiContractTest {
.contains("evidence identity is taskId+inboundId+loadingPlanId+containerBookingId")
.contains("Current fl_inbound_order.version for inboundId")
.contains("Compatibility alias of inboundVersion from the same fl_inbound_order row")
.contains("sourceType", "recordStatus", "mergedIntoContainerBookingId")
.contains("Task detail is served only from the ACTIVE container booking")
.contains("Enforced by the server-side pending-task query")
.contains("CompleteLoadingRequest");
String taskLookup = yaml.substring(
@@ -47,6 +50,10 @@ class PdaConvergenceOpenApiContractTest {
assertThat(taskLookup)
.contains("name: cabinetNo", "name: loadingPlanId")
.doesNotContain("name: containerNo");
String taskDetail = yaml.substring(
yaml.indexOf(" /api/platform/v1/first-leg/pda-loading/tasks/{taskId}:"),
yaml.indexOf(" /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/claim:"));
assertThat(taskDetail).contains("#/components/schemas/Task", "ACTIVE container lifecycle fields");
assertThat(batchRequest)
.contains("loadingPlanId:", "containerBookingId:")
.doesNotContain("containerNo:");
@@ -34,7 +34,7 @@ class CustomsServiceTest {
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "internalToken", "token");
when(suppliers.customs(anyLong(), anyString()))
.thenReturn(new com.baiyue.firstleg.client.WarehouseSupplierClient.Snapshot(88L, "线下报关代理", "CUSTOMS", null, "ACTIVE"));
.thenReturn(new com.baiyue.firstleg.client.WarehouseSupplierClient.Snapshot(88L, "线下报关代理", "CUSTOMS", "ACTIVE"));
when(repository.completeOperation(anyLong(), anyString(), anyLong())).thenReturn(1);
}
@@ -29,6 +29,9 @@ class LoadingPickPdaTaskSqlContractTest {
assertThat(sql)
.contains("task.task_status = 'LOADING'")
.contains("booking.record_status = 'ACTIVE'")
.contains("booking.source_type AS sourceType")
.contains("booking.record_status AS recordStatus")
.contains("booking.merged_into_container_booking_id AS mergedIntoContainerBookingId")
.contains("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}")
.contains("booking.warehouse_id AS warehouseId")
.doesNotContain("tenantId");
@@ -62,7 +62,7 @@ class ContainerServiceTest {
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"));
.thenReturn(new WarehouseSupplierClient.Snapshot(202L, "拖车公司A", "TRUCKING", "ACTIVE"));
}
@Test
@@ -17,6 +17,7 @@ import com.baiyue.firstleg.entity.LoadingPickItemEntity;
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
@@ -30,6 +31,7 @@ import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEn
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.InOrder;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@@ -70,6 +72,16 @@ class LoadingPickServiceTest {
@Resource
private LoadingPickService service;
@BeforeEach
void activeOperationalContainer() {
ContainerBookingEntity booking = new ContainerBookingEntity();
booking.setId(99L);
booking.setContainerNo("CSLU1234567");
booking.setSourceType("SIMULATION");
booking.setRecordStatus("ACTIVE");
when(smartLoadingRepository.booking(99L)).thenReturn(booking);
}
@Test
void opensTaskOnlyAfterReleasedBookingAndRealContainerNumber() {
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
@@ -212,6 +224,7 @@ class LoadingPickServiceTest {
task.setContainerBookingId(40L); task.setCabinetNo("CAB-001");
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L); booking.setContainerNo("MSCU-NEW");
booking.setSourceType("SIMULATION"); booking.setRecordStatus("ACTIVE");
when(repository.taskByCabinet("CAB-001", 101L)).thenReturn(task);
when(repository.taskByPlan(10L, 101L)).thenReturn(task);
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
@@ -224,6 +237,53 @@ class LoadingPickServiceTest {
assertThat(byCabinet.id()).isEqualTo(20L);
assertThat(byPlan.id()).isEqualTo(20L);
assertThat(byCabinet.currentContainerNo()).isEqualTo("MSCU-NEW");
assertThat(byCabinet.sourceType()).isEqualTo("SIMULATION");
assertThat(byCabinet.recordStatus()).isEqualTo("ACTIVE");
assertThat(byCabinet.mergedIntoContainerBookingId()).isNull();
}
@Test
void pendingTasksExposeServerFilteredContainerLifecycle() {
var row = new com.baiyue.firstleg.mapper.LoadingPickTaskMapper.PendingTaskRow();
row.setTaskId(20L);
row.setLoadingPlanId(10L);
row.setContainerBookingId(99L);
row.setCabinetNo("CAB-001");
row.setContainerNo("MSCU-001");
row.setSourceType("SIMULATION");
row.setRecordStatus("ACTIVE");
row.setMergedIntoContainerBookingId(null);
row.setWarehouseId(101L);
row.setTaskStatus("LOADING");
row.setWorkflowStatus("UNASSIGNED");
row.setVersion(0);
row.setAssignmentVersion(0);
row.setItemCount(1);
row.setPendingItemCount(1);
when(repository.pendingTasks(9L, null)).thenReturn(List.of(row));
var result = service.pendingTasks(9L, null).get(0);
assertThat(result.sourceType()).isEqualTo("SIMULATION");
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
assertThat(result.mergedIntoContainerBookingId()).isNull();
}
@Test
void taskDetailRejectsMergedContainerSource() {
LoadingPickTaskEntity task = task();
ContainerBookingEntity merged = new ContainerBookingEntity();
merged.setId(99L);
merged.setSourceType("OCEAN_BOOKING");
merged.setRecordStatus("MERGED");
merged.setMergedIntoContainerBookingId(88L);
when(repository.task(20L)).thenReturn(task);
when(smartLoadingRepository.booking(99L)).thenReturn(merged);
when(repository.items(20L, 101L)).thenReturn(List.of());
when(repository.cartonLabels(20L)).thenReturn(List.of());
assertThatThrownBy(() -> service.task(20L, 101L))
.isInstanceOf(BizException.class).hasMessageContaining("已非有效存续记录");
}
@Test
@@ -345,6 +405,9 @@ class LoadingPickServiceTest {
assertThat(result.loadingVersion()).isEqualTo(7);
assertThat(result.loadingPlanId()).isEqualTo(10L);
assertThat(result.containerBookingId()).isEqualTo(99L);
assertThat(result.sourceType()).isEqualTo("SIMULATION");
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
assertThat(result.mergedIntoContainerBookingId()).isNull();
}
@Test
@@ -34,19 +34,29 @@ class WarehouseSupplierSnapshotServiceTest {
}
@Test
void resolvesActiveBookingSupplierWithFixedScopeAndServerName() {
when(client.get("internal-token", 7L, true, "BOOKING", "INTERNATIONAL"))
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(7L, "海运代理", "BOOKING", "INTERNATIONAL", "ACTIVE")));
void resolvesActiveBookingSupplierByCategoryWithoutScopeAndUsesServerName() {
when(client.get("internal-token", 7L, true, "BOOKING"))
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(7L, "海运代理", "BOOKING", "ACTIVE")));
var snapshot = service.booking(7L, "海运代理");
assertThat(snapshot.name()).isEqualTo("海运代理");
verify(client).get("internal-token", 7L, true, "BOOKING", "INTERNATIONAL");
verify(client).get("internal-token", 7L, true, "BOOKING");
}
@Test
void rejectsClientNameThatDiffersFromWarehouseSnapshot() {
when(client.get(anyString(), eq(9L), eq(true), eq("CUSTOMS"), isNull()))
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(9L, "真实报关行", "CUSTOMS", null, "ACTIVE")));
when(client.get(anyString(), eq(9L), eq(true), eq("CUSTOMS")))
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(9L, "真实报关行", "CUSTOMS", "ACTIVE")));
assertThatThrownBy(() -> service.customs(9L, "伪造名称"))
.isInstanceOf(BizException.class).hasMessageContaining("不一致");
}
@Test
void rejectsSupplierSnapshotWithWrongCategoryOrInactiveStatus() {
when(client.get("internal-token", 7L, true, "BOOKING"))
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(
7L, "错误代理", "TRUCKING", "ACTIVE")));
assertThatThrownBy(() -> service.booking(7L, null))
.isInstanceOf(BizException.class).hasMessageContaining("类别不匹配");
}
}