From 19a6af1c6dd80d070fa1cca9ba10b772bac3770a Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 14:45:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lastleg-service/AGENTS.md | 2 +- .../baiyue/lastleg/mapper/LastlegOrderMapper.java | 9 +++------ .../resources/openapi/overseas-business-v1.yaml | 2 +- .../ContainerPlanProgressContractTest.java | 9 +++++---- .../manual-state-ocean-trucking.mjs | 12 ++++++++---- .../phase3-container-customs-fixture.mjs | 15 ++++++++++++++- 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/lastleg-service/AGENTS.md b/lastleg-service/AGENTS.md index 8ff6423e1..47cc02fb1 100644 --- a/lastleg-service/AGENTS.md +++ b/lastleg-service/AGENTS.md @@ -12,7 +12,7 @@ | 功能 | 入口 | |---|---| | 内部模块化基线与依赖分析 | `docs/lastleg-internal-modularization-baseline.md`、`scripts/analyze-lastleg-dependencies.mjs`、`scripts/analyze-lastleg-dependencies.test.mjs`、`src/test/resources/architecture/lastleg-slice-dependencies.json`;只约束本服务内部包依赖,不改变部署单元或数据库边界 | -| 事件接收、平台订单列表/详情/导出、柜级进度、目的仓调整 | `src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java`;`/api/platform/v1/last-leg/orders/container-summaries` 按 `sourceContainerBookingId` 服务端聚合全柜订单、入仓码、计划数、未操作数和 `allPlansReady`,分页前排除 `allPlansReady=true`,`/{sourceContainerBookingId}` 返回柜内全部订单/入仓码/计划状态且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结到 `ll_order.estimated_warehouse_at` 的正式事实,按其升序、null 最后、稳定柜 ID 排序;目的仓调整仅接受国内 Warehouse Service 平台公共 `ACTIVE + FBA` 主档,写入前由 `service/FbaWarehouseValidator.java` 经内部只读目录校验 ID/编码/名称一致 | +| 事件接收、平台订单列表/详情/导出、柜级进度、目的仓调整 | `src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java`;`/api/platform/v1/last-leg/orders/container-summaries` 按 `sourceContainerBookingId` 服务端聚合全柜订单、入仓码和有效计划,分页前排除全部入仓码均已生成有效独立计划的柜,`/{sourceContainerBookingId}` 返回柜内全部订单/入仓码/计划状态且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结到 `ll_order.estimated_warehouse_at` 的正式事实,按其升序、null 最后、稳定柜 ID 排序;目的仓调整仅接受国内 Warehouse Service 平台公共 `ACTIVE + FBA` 主档,写入前由 `service/FbaWarehouseValidator.java` 经内部只读目录校验 ID/编码/名称一致 | | 客户尾程订单列表与详情 | `controller/MerchantLastlegOrderController.java`;客户范围只取认证身份,稳定类型/状态见 `model/LastlegOrderType.java`、`LastlegOrderStatus.java` | | 装柜完成事件消费 | `service/LoadingCompletedConsumer.java`;当前生产信封为 `FIRST_LEG_LOADING_PLAN_COMPLETED` V2,消费端冻结 `destinationType`:FBA 可按箱数且不生成逐箱作业账,OVERSEAS 必须逐箱校验并生成箱级事实;历史缺字段事件仅按明确 FBA 单号或目的仓与接柜仓一致性安全推断,歧义直接拒绝 | | 尾程单号生成 | `service/LastlegOrderNoGenerator.java` | diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java index 86e54d362..d1933edd4 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java @@ -21,12 +21,9 @@ public interface LastlegOrderMapper extends BaseMapper { "AND o.created_at >=#{q.createdFrom} " + "AND o.created_at <=#{q.createdTo} " + "GROUP BY o.source_container_booking_id " + - "HAVING SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END)>0 " + - "OR SUM(CASE WHEN p.id IS NULL OR NULLIF(TRIM(p.plan_no),'') IS NULL " + - "OR NULLIF(TRIM(p.status),'') IS NULL THEN 1 ELSE 0 END)>0 " + - "OR COUNT(DISTINCT p.id)<>COUNT(*) " + - "OR COUNT(NULLIF(o.container_no,''))<>COUNT(*) " + - "OR COUNT(DISTINCT NULLIF(o.container_no,''))<>1 "; + "HAVING COUNT(DISTINCT CASE WHEN p.id IS NOT NULL AND NULLIF(TRIM(p.plan_no),'') IS NOT NULL " + + "AND NULLIF(TRIM(p.status),'') IS NOT NULL THEN NULLIF(TRIM(o.inbound_code),'') END) " + + "< COUNT(DISTINCT NULLIF(TRIM(o.inbound_code),'')) "; String CONTAINER_PROGRESS_COLUMNS = "SELECT o.source_container_booking_id AS sourceContainerBookingId," + "CASE WHEN COUNT(NULLIF(o.container_no,''))=COUNT(*) AND COUNT(DISTINCT NULLIF(o.container_no,''))=1 " + diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index be0865da4..c857adbf5 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -170,7 +170,7 @@ paths: /api/platform/v1/last-leg/orders/container-summaries: get: summary: 分页查询柜级入库计划进度 - description: 权限 platform:overseas-warehouse:order:list;平台跨客户按稳定 sourceContainerBookingId 服务端聚合,不受订单分页影响,仅返回 allPlansReady=false 的待处理柜,已全部生成有效独立计划的柜在分页前排除。containerNo 只返回装柜完成事件携带并规范化冻结的真实集装箱号,不从 cabinetNo、订单号或内部 ID 推导。plannedArrivalAt 仅来自装柜完成事件 estimatedWarehouseArrivalAt 冻结的 ll_order.estimated_warehouse_at;按该时间升序、null 最后、稳定柜 ID 升序。 + description: 权限 platform:overseas-warehouse:order:list;平台跨客户按稳定 sourceContainerBookingId 服务端聚合,不受订单分页影响,仅返回仍有入仓码待生成有效独立计划的柜,全部入仓码均已生成有效独立计划的柜在分页前排除。containerNo 只返回装柜完成事件携带并规范化冻结的真实集装箱号,不从 cabinetNo、订单号或内部 ID 推导。plannedArrivalAt 仅来自装柜完成事件 estimatedWarehouseArrivalAt 冻结的 ll_order.estimated_warehouse_at;按该时间升序、null 最后、稳定柜 ID 升序。 security: [{ bearerAuth: [] }] parameters: - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/ContainerPlanProgressContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/ContainerPlanProgressContractTest.java index 6d19343c9..72db7a52e 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/ContainerPlanProgressContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/ContainerPlanProgressContractTest.java @@ -38,8 +38,8 @@ class ContainerPlanProgressContractTest { String sql = String.join("", rows.value()).toLowerCase(); assertTrue(sql.contains("left join ll_inbound_plan")); assertTrue(sql.contains("o.estimated_warehouse_at")); - assertTrue(sql.contains("having sum(case when nullif(trim(o.inbound_code),'') is null then 1 else 0 end)>0")); - assertTrue(sql.contains("or count(distinct p.id)<>count(*)")); + assertTrue(sql.contains("having count(distinct case when p.id is not null")); + assertTrue(sql.contains("< count(distinct nullif(trim(o.inbound_code),''))")); assertTrue(sql.contains("progress.plannedarrivalat asc")); assertTrue(sql.contains("progress.sourcecontainerbookingid asc")); assertTrue(sql.contains("limit #{limit} offset #{offset}")); @@ -56,7 +56,7 @@ class ContainerPlanProgressContractTest { assertTrue(openApi.contains("/api/platform/v1/last-leg/orders/container-summaries/{sourceContainerBookingId}:")); assertTrue(openApi.contains("ContainerPlanProgressSummary:")); assertTrue(openApi.contains("allPlansReady:")); - assertTrue(openApi.contains("仅返回 allPlansReady=false 的待处理柜")); + assertTrue(openApi.contains("仅返回仍有入仓码待生成有效独立计划的柜")); assertTrue(openApi.contains("ll_order.estimated_warehouse_at")); assertTrue(schema.contains("idx_ll_order_container_progress(source_container_booking_id,estimated_warehouse_at,created_at,id)")); assertTrue(deployment.contains("idx_ll_order_container_progress(source_container_booking_id,estimated_warehouse_at,created_at,id)")); @@ -75,7 +75,8 @@ class ContainerPlanProgressContractTest { assertTrue(sql.contains("source_container_booking_id<>''")); assertTrue(sql.contains("LEFT JOIN ll_inbound_plan")); - assertTrue(sql.contains("HAVING SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END)>0")); + assertTrue(sql.contains("HAVING COUNT(DISTINCT CASE WHEN p.id IS NOT NULL")); + assertTrue(sql.contains("< COUNT(DISTINCT NULLIF(TRIM(o.inbound_code),''))")); assertTrue(!sql.contains("<")); } } diff --git a/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs b/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs index 1a489baff..82d7fa925 100644 --- a/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs +++ b/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs @@ -195,12 +195,16 @@ export async function run(context, matrix = context.createdResources.manualState await verify(context, OCEAN_BASE, status, resourceId) } - const releasedOcean = matrix.pages.oceanBookings.fixtures.RELEASED - if (!releasedOcean.id || !releasedOcean.warehouseId) { - throw new Error('RELEASED 订舱缺少拖车所需的仓库归属') - } matrix.pages.truckAppointments = { requiredStatuses: TRUCK_STATUSES, fixtures: {} } for (const [index, status] of TRUCK_STATUSES.entries()) { + const released = await buildOcean(context, template, 'RELEASED', index + 100) + const releasedOcean = { + id: id(released?.oceanBooking?.id), + warehouseId: id(released?.oceanBooking?.warehouseId), + } + if (!releasedOcean.id || !releasedOcean.warehouseId) { + throw new Error(`拖车状态样本 ${index} 缺少独立 RELEASED 订舱`) + } const value = await buildTruck(context, releasedOcean, status, index) const resourceId = id(value?.id) matrix.pages.truckAppointments.fixtures[status] = { id: resourceId, status: value?.status, detailPath: `/truck-appointments/${resourceId}` } diff --git a/scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs b/scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs index 426576fc7..c61286914 100644 --- a/scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs +++ b/scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs @@ -43,7 +43,7 @@ async function loginMerchant(context) { const password = account?.password || process.env.MERCHANT_TEST_PASSWORD || '' if (!username || !password) throw new Error('缺少客户登录凭据') const response = await context.request('POST', '/api/auth/login', { - username, password, + loginId: username, password, }, '') const token = success(response) ? response.body?.data?.accessToken : null if (!token) throw new Error(`本轮客户登录失败: requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`) @@ -134,6 +134,16 @@ function routes(context, merchantToken) { export async function prepare(context) { const outerMarker = context.marker + const isolatedResourceKeys = ['phase3Loading', 'phase3LoadingCompletedProducer', 'orchestrationFixtures'] + const isolatedResourceSnapshot = new Map(isolatedResourceKeys.map((name) => [name, { + present: Object.hasOwn(context.createdResources, name), value: context.createdResources[name], + }])) + const restoreIsolatedResources = () => { + for (const [name, snapshot] of isolatedResourceSnapshot) { + if (snapshot.present) context.createdResources[name] = snapshot.value + else delete context.createdResources[name] + } + } const restore = await context.preparePhase3LoadingFixture('-CUSTOMS-MIXED', false, { primary: 'BUY_DECLARATION', incremental: 'TAX_REFUND', }) @@ -154,6 +164,7 @@ export async function prepare(context) { if (!source?.loadingPlanId || !source?.containerBookingId || source?.finalStatus !== 'LOADED') { context.marker = outerMarker restore() + restoreIsolatedResources() throw new Error(`混合柜未进入LOADED: planId=${id(source?.loadingPlanId) || 'missing'}, status=${source?.finalStatus || 'missing'}`) } const platform = await context.request('GET', `${PLATFORM}/${encodeURIComponent(id(source.loadingPlanId))}`) @@ -169,6 +180,7 @@ export async function prepare(context) { if (!buy.length || !refund.length) { context.marker = outerMarker restore() + restoreIsolatedResources() throw new Error(`正式装柜API未形成混合柜: buy=${buy.length}, tax=${refund.length}`) } const merchantToken = await loginMerchant(context) @@ -208,5 +220,6 @@ export async function prepare(context) { delete context.context.phase3ContainerCustomsRoutes context.marker = outerMarker restore() + restoreIsolatedResources() } }