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 1/5] =?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() } } From 8da57487c0b6f5058f9bf9e446a69744cffb561a Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 15:04:15 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BB=B6?= =?UTF-8?q?=E4=BB=A3=E5=8F=91=E5=A4=9A=E7=A5=A8=E4=BA=8B=E4=BB=B6=E5=B9=82?= =?UTF-8?q?=E7=AD=89=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/baiyue/lastleg/service/LoadingCompletedConsumer.java | 2 +- .../baiyue/lastleg/service/LoadingCompletedConsumerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java index f5e01bf12..3d3655122 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java @@ -288,7 +288,7 @@ public class LoadingCompletedConsumer { try { warehouseId = Long.parseLong(payload.warehouseId()); } catch (Exception exception) { throw new IllegalArgumentException("One-click dropship warehouseId is invalid", exception); } dropshipOrders.receive(new DropshipRequests.Receive(customerId, order.getId(), warehouseId, - event.eventId() + ":DROPSHIP", dropshipPallets)); + event.eventId() + ":DROPSHIP:" + source.sourceInboundId(), dropshipPallets)); } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java index c2e41acab..cdbeb41c1 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java @@ -26,7 +26,7 @@ class LoadingCompletedConsumerTest { assertTrue(table.contains("customer_id")); assertFalse(table.contains("tenant_id")); } @Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); } - @Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","Customer 100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA"); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","MSCU6639870","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); verify(inventory,never()).registerInTransit(anyString(),anyString(),any()); assertEquals("100",captor.getValue().customerId()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); } + @Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","Customer 100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA"); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","MSCU6639870","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); verify(inventory,never()).registerInTransit(anyString(),anyString(),any()); assertEquals("100",captor.getValue().customerId()); assertEquals("123e4567-e89b-12d3-a456-426614174010:DROPSHIP:in-d",captor.getValue().idempotencyKey()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); } @Test void oneDropshipInboundWithMultipleSkusCreatesOneOrderAndMultipleSkuDetails(){ var itemA=new FirstLegLoadingPlanCompletedV1.Item("d-a","SKU-A","产品A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-A","MARK-A",null,List.of("BOX-A1","BOX-A2")); var itemB=new FirstLegLoadingPlanCompletedV1.Item("d-b","SKU-B","产品B","Product B",3,15,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-B","MARK-B",null,List.of("BOX-B1","BOX-B2","BOX-B3")); From e48e8dca42935fe3f6294b3ab3691bc64e852dc7 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 15:14:24 +0800 Subject: [PATCH 3/5] fix(deploy): eliminate duplicate container log writes --- README.md | 14 +++++ .../src/main/resources/application.yml | 11 ---- auth/src/main/resources/application.yml | 11 ---- config/nacos/cn/baiyue-common.yml | 3 -- config/nacos/local/baiyue-common.yml | 3 -- deploy/docker-compose.overseas.yml | 12 ++--- deploy/docker-compose.yml | 20 ++----- deploy/docker/Dockerfile | 2 +- file/src/main/resources/application.yml | 11 ---- finance/src/main/resources/application.yml | 4 -- firstleg/src/main/resources/application.yml | 11 ---- gateway/src/main/resources/application.yml | 11 ---- iam/src/main/resources/application.yml | 11 ---- .../src/main/resources/application.yml | 11 ---- order/src/main/resources/application.yml | 10 ---- print/src/main/resources/application.yml | 11 ---- scripts/README.md | 1 + scripts/container-logging-contract.test.mjs | 54 +++++++++++++++++++ warehouse/src/main/resources/application.yml | 11 ---- 19 files changed, 78 insertions(+), 144 deletions(-) create mode 100644 scripts/container-logging-contract.test.mjs diff --git a/README.md b/README.md index 0f2913388..de846c17a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,20 @@ GitLab 在独立 Node 镜像中自动执行拓扑合同,随后才允许服务 国内 Nacos 配置位于 `config/nacos/cn`。数据库、Redis、Nacos 和 RocketMQ 的生产密码必须通过环境变量或配置中心密文注入,禁止提交明文密码。 +## 容器日志 + +所有服务只向标准输出和标准错误写日志,不在容器内生成应用日志文件,也不把应用日志目录挂载到宿主机。国内、海外 Compose 统一使用 Docker `local` 日志驱动;仍可通过 `docker logs` 排查。文件服务的 `/data/files` 是业务文件目录,不属于日志目录,必须继续持久化。 + +单容器默认保留 `50m × 3`,可在各环境部署变量中覆盖: + +| 环境 | `DOCKER_LOG_MAX_SIZE` | `DOCKER_LOG_MAX_FILE` | 长期留存 | +|---|---:|---:|---| +| 开发 | `20m` | `2` | 不要求 | +| 测试 | `50m` | `3` | 不要求 | +| 生产 | `100m` | `5` | 必须由集中日志系统负责,Docker 本机日志只作短期缓冲 | + +调整日志策略后必须重新创建容器;只重启既有容器不会改变其日志驱动。历史 `/data/logs/baiyue-firstleg` 文件和旧容器日志不自动删除,确认无需保留后再单独清理。 + ## 网关跨域配置 本地默认允许商户端和后台端从 `localhost/127.0.0.1` 的 `5173/5174` 端口访问网关。部署到测试或生产环境时,必须通过 `CORS_ALLOWED_ORIGINS` 配置两个前端的明确地址,多个地址用英文逗号分隔,例如: diff --git a/audit-service/src/main/resources/application.yml b/audit-service/src/main/resources/application.yml index dfca83c9d..7246521f9 100644 --- a/audit-service/src/main/resources/application.yml +++ b/audit-service/src/main/resources/application.yml @@ -40,14 +40,3 @@ baiyue: rocketmq: name-server: ${ROCKETMQ_NAME_SERVER:127.0.0.1:9876} management.endpoints.web.exposure.include: health,info,prometheus - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/auth/src/main/resources/application.yml b/auth/src/main/resources/application.yml index 33fe66410..fd346398e 100644 --- a/auth/src/main/resources/application.yml +++ b/auth/src/main/resources/application.yml @@ -52,14 +52,3 @@ baiyue.clients: url: ${IAM_SERVICE_URL:} management.endpoints.web.exposure.include: health,info,prometheus - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/config/nacos/cn/baiyue-common.yml b/config/nacos/cn/baiyue-common.yml index 534b74de1..f7318ae43 100644 --- a/config/nacos/cn/baiyue-common.yml +++ b/config/nacos/cn/baiyue-common.yml @@ -50,9 +50,6 @@ spring: client.config.default: connect-timeout: 3000 read-timeout: 5000 -logging: - file: - path: ${LOG_PATH:/app/logs} server: shutdown: graceful max-http-request-header-size: ${SERVER_MAX_HTTP_REQUEST_HEADER_SIZE:32KB} diff --git a/config/nacos/local/baiyue-common.yml b/config/nacos/local/baiyue-common.yml index cf5ab9245..9af5a8d4a 100644 --- a/config/nacos/local/baiyue-common.yml +++ b/config/nacos/local/baiyue-common.yml @@ -47,9 +47,6 @@ spring: client.config.default: connect-timeout: 3000 read-timeout: 5000 -logging: - file: - path: ${LOG_PATH:./logs} server: shutdown: graceful max-http-request-header-size: ${SERVER_MAX_HTTP_REQUEST_HEADER_SIZE:32KB} diff --git a/deploy/docker-compose.overseas.yml b/deploy/docker-compose.overseas.yml index 24729ca6a..85d7534ba 100644 --- a/deploy/docker-compose.overseas.yml +++ b/deploy/docker-compose.overseas.yml @@ -24,13 +24,13 @@ x-overseas-common: &overseas-common DEPLOYMENT_CELL: ${OVERSEAS_DEPLOYMENT_CELL:-test-overseas} DATA_RESIDENCY: OVERSEAS SPRING_SQL_INIT_MODE: never - LOG_PATH: /app/logs TZ: Asia/Shanghai logging: - driver: json-file + # 应用只写 stdout/stderr;local 驱动负责单份本机日志与轮转。 + driver: local options: - max-size: 100m - max-file: "5" + max-size: ${DOCKER_LOG_MAX_SIZE:-50m} + max-file: "${DOCKER_LOG_MAX_FILE:-3}" services: gateway-overseas: @@ -38,7 +38,6 @@ services: image: ${IMAGE_REPOSITORY}/gateway-overseas:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-gateway-overseas ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${GATEWAY_OVERSEAS_PORT:-9300}:9300"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/gateway-overseas:/app/logs"] environment: <<: *overseas-environment SERVER_PORT: 9300 @@ -55,7 +54,6 @@ services: image: ${IMAGE_REPOSITORY}/lastleg-service:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-lastleg ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${LASTLEG_PORT:-9303}:9303"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/lastleg-service:/app/logs"] environment: <<: *overseas-environment SERVER_PORT: 9303 @@ -73,7 +71,6 @@ services: image: ${IMAGE_REPOSITORY}/warehouse-overseas-service:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-warehouse-overseas ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${WAREHOUSE_OVERSEAS_PORT:-9305}:9305"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/warehouse-overseas-service:/app/logs"] environment: <<: *overseas-environment SERVER_PORT: 9305 @@ -87,7 +84,6 @@ services: image: ${IMAGE_REPOSITORY}/event-bridge-service:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-event-bridge-overseas ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${EVENT_BRIDGE_OVERSEAS_PORT:-9313}:9312"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/event-bridge-service-overseas:/app/logs"] environment: <<: *overseas-environment SERVER_PORT: 9312 diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 8e61b9ec7..0caf03dec 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -26,13 +26,13 @@ x-common: &common REGION_CODE: ${REGION_CODE:-CN} DEPLOYMENT_CELL: ${DEPLOYMENT_CELL:-test-cn} DATA_RESIDENCY: ${DATA_RESIDENCY:-CN} - LOG_PATH: /app/logs TZ: Asia/Shanghai logging: - driver: json-file + # 应用只写 stdout/stderr;local 驱动负责单份本机日志与轮转。 + driver: local options: - max-size: 100m - max-file: "5" + max-size: ${DOCKER_LOG_MAX_SIZE:-50m} + max-file: "${DOCKER_LOG_MAX_FILE:-3}" services: gateway: @@ -40,7 +40,6 @@ services: image: ${IMAGE_REPOSITORY}/gateway:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-gateway ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${GATEWAY_PORT:-9000}:9000"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/gateway:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9000 @@ -53,7 +52,6 @@ services: image: ${IMAGE_REPOSITORY}/auth:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-auth ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${AUTH_PORT:-9100}:9100"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/auth:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9100 @@ -72,7 +70,6 @@ services: image: ${IMAGE_REPOSITORY}/iam:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-iam ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${IAM_PORT:-9201}:9201"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/iam:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9201 @@ -86,7 +83,6 @@ services: image: ${IMAGE_REPOSITORY}/firstleg:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-business ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${FIRSTLEG_PORT:-9202}:9202"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/firstleg:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9202 @@ -112,7 +108,6 @@ services: image: ${IMAGE_REPOSITORY}/finance:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-finance ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${FINANCE_PORT:-9203}:9203"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/finance:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9203 @@ -125,7 +120,6 @@ services: image: ${IMAGE_REPOSITORY}/order:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-order ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${ORDER_PORT:-9204}:9204"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/order:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9204 @@ -138,7 +132,6 @@ services: image: ${IMAGE_REPOSITORY}/warehouse:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-warehouse ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${WAREHOUSE_PORT:-9205}:9205"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/warehouse:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9205 @@ -153,7 +146,6 @@ services: ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${FILE_PORT:-9206}:9206"] volumes: - "${FILE_UPLOADS_HOST_DIR:-/data/firstleg/uploads}:/data/files" - - "${LOG_ROOT:-/data/logs/baiyue-firstleg}/file:/app/logs" environment: <<: *common-environment SERVER_PORT: 9206 @@ -175,7 +167,6 @@ services: image: ${IMAGE_REPOSITORY}/print:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-print ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${PRINT_PORT:-9207}:9207"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/print:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9207 @@ -186,7 +177,6 @@ services: image: ${IMAGE_REPOSITORY}/integration-service:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-integration ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${INTEGRATION_PORT:-9210}:9210"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/integration-service:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9210 @@ -236,7 +226,6 @@ services: image: ${IMAGE_REPOSITORY}/event-bridge-service:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-event-bridge ports: ["${PUBLISHED_BIND_ADDRESS:-0.0.0.0}:${EVENT_BRIDGE_PORT:-9312}:9312"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/event-bridge-service:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9312 @@ -256,7 +245,6 @@ services: image: ${IMAGE_REPOSITORY}/audit-service:${IMAGE_TAG} container_name: firstleg-${DEPLOY_ENV:-test}-audit expose: ["9700"] - volumes: ["${LOG_ROOT:-/data/logs/baiyue-firstleg}/audit-service:/app/logs"] environment: <<: *common-environment SERVER_PORT: 9700 diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 4e7c1d79a..ad239b1bf 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -15,7 +15,7 @@ RUN addgroup -S app \ WORKDIR /app # 预建镜像内运行时数据和日志目录并归属 app 用户;宿主机 bind mount 的属主需在部署前单独设置为 UID 10001。 -RUN mkdir -p /data/files /app/logs && chown -R app:app /data /app/logs +RUN mkdir -p /data/files && chown -R app:app /data # 依赖层稳定、业务代码层高频变化;Registry 只需传输实际变化的 Layer。 COPY --from=extractor --chown=app:app /workspace/dependencies/ ./ diff --git a/file/src/main/resources/application.yml b/file/src/main/resources/application.yml index d142be68e..2d9fa2132 100644 --- a/file/src/main/resources/application.yml +++ b/file/src/main/resources/application.yml @@ -201,14 +201,3 @@ baiyue: max-attempts: 3 batch-size: 500 management.endpoints.web.exposure.include: health,info,prometheus - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/finance/src/main/resources/application.yml b/finance/src/main/resources/application.yml index 9ccb4b825..0614b2707 100644 --- a/finance/src/main/resources/application.yml +++ b/finance/src/main/resources/application.yml @@ -26,7 +26,3 @@ spring: namespace: ${NACOS_NAMESPACE:public} management.endpoints.web.exposure.include: health,info,prometheus - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" diff --git a/firstleg/src/main/resources/application.yml b/firstleg/src/main/resources/application.yml index e31ea3e38..46cbdd8f3 100644 --- a/firstleg/src/main/resources/application.yml +++ b/firstleg/src/main/resources/application.yml @@ -61,14 +61,3 @@ firstleg: reservation-ttl: ${PALLET_RESERVATION_TTL:30m} warehouse-resource-tenant-id: ${WAREHOUSE_RESOURCE_TENANT_ID:0} management.endpoints.web.exposure.include: health,info,prometheus - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index 15ba9c169..141cf31ef 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -230,14 +230,3 @@ springdoc: url: /event-bridge-service/v3/api-docs - name: finance url: /finance-service/v3/api-docs - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/iam/src/main/resources/application.yml b/iam/src/main/resources/application.yml index 5faf2522f..a23c9fe33 100644 --- a/iam/src/main/resources/application.yml +++ b/iam/src/main/resources/application.yml @@ -39,14 +39,3 @@ baiyue: jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start} internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long} management.endpoints.web.exposure.include: health,info,prometheus - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/integration-service/src/main/resources/application.yml b/integration-service/src/main/resources/application.yml index cca1f3168..f6d42fe07 100644 --- a/integration-service/src/main/resources/application.yml +++ b/integration-service/src/main/resources/application.yml @@ -104,14 +104,3 @@ management: health: probes: enabled: true - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/order/src/main/resources/application.yml b/order/src/main/resources/application.yml index 5674c27d8..a0883c3c1 100644 --- a/order/src/main/resources/application.yml +++ b/order/src/main/resources/application.yml @@ -37,13 +37,3 @@ baiyue: url: ${FILE_SERVICE_URL:} order: inbound-code-mode: ${ORDER_INBOUND_CODE_MODE:AUTO} -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/print/src/main/resources/application.yml b/print/src/main/resources/application.yml index 5761abcbf..e01f7b2b9 100644 --- a/print/src/main/resources/application.yml +++ b/print/src/main/resources/application.yml @@ -36,14 +36,3 @@ print: supported-height-mm: 100 font-resource: fonts/NotoSansSC-Bold.ttf management.endpoints.web.exposure.include: health,info,prometheus - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" diff --git a/scripts/README.md b/scripts/README.md index 2c5a2dd2f..407d39846 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -131,6 +131,7 @@ node scripts/create-pending-review-orders.mjs | Mapper 注册静态合同 | `node scripts/mapper-scan-contract.mjs`、`node --test scripts/mapper-scan-contract.test.mjs`;自动核对全部持久化模块的 `@Mapper`、服务根包注解过滤扫描和唯一扫描入口,不启动 Spring、不连接数据库;`backend-full-regression.ps1` 在 Maven 前强制执行 | | Java 质量聚合摘要 | `node --test scripts/java-quality-summary.test.mjs`、`node scripts/java-quality-summary.mjs --output target/java-quality-summary.json`;聚合 Maven/服务拓扑、OpenAPI 覆盖、JaCoCo 和 SpotBugs 报告,默认只报告存量缺口,CI 用显式 `--require-jacoco --require-spotbugs` 检查报告完整性且当前允许失败 | | 单服务部署失败回滚合同 | `scripts/deploy-built-service-rollback.test.mjs`;模拟成功、健康失败和启动失败,确认仅恢复同服务先前健康镜像且部署任务仍失败 | +| 容器日志单写合同 | `scripts/container-logging-contract.test.mjs`;离线确认应用只写标准输出、Compose 使用有界 `local` 驱动且不挂载应用日志目录 | Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块边界,不访问测试环境: diff --git a/scripts/container-logging-contract.test.mjs b/scripts/container-logging-contract.test.mjs new file mode 100644 index 000000000..440849fa7 --- /dev/null +++ b/scripts/container-logging-contract.test.mjs @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; + +const applicationConfigs = [ + 'auth/src/main/resources/application.yml', + 'audit-service/src/main/resources/application.yml', + 'event-bridge-service/src/main/resources/application.yml', + 'file/src/main/resources/application.yml', + 'finance/src/main/resources/application.yml', + 'firstleg/src/main/resources/application.yml', + 'gateway/src/main/resources/application.yml', + 'gateway-overseas/src/main/resources/application.yml', + 'iam/src/main/resources/application.yml', + 'integration-service/src/main/resources/application.yml', + 'lastleg-service/src/main/resources/application.yml', + 'order/src/main/resources/application.yml', + 'print/src/main/resources/application.yml', + 'warehouse/src/main/resources/application.yml', + 'warehouse-overseas-service/src/main/resources/application.yml', + 'config/nacos/cn/baiyue-common.yml', + 'config/nacos/local/baiyue-common.yml', + 'config/nacos/overseas/baiyue-common.yml', +]; + +const composeFiles = [ + 'deploy/docker-compose.yml', + 'deploy/docker-compose.overseas.yml', +]; + +test('deployable applications log only to stdout and stderr', () => { + for (const path of applicationConfigs) { + const source = readFileSync(path, 'utf8'); + assert.doesNotMatch(source, /LOG_PATH|LOGGING_FILE_NAME|LOGGING_ROLLING/, path); + } +}); + +test('Compose uses one bounded local log stream without application log mounts', () => { + for (const path of composeFiles) { + const source = readFileSync(path, 'utf8'); + assert.match(source, /driver:\s+local/, path); + assert.match(source, /DOCKER_LOG_MAX_SIZE/, path); + assert.match(source, /DOCKER_LOG_MAX_FILE/, path); + assert.doesNotMatch(source, /LOG_PATH|LOG_ROOT|\/app\/logs/, path); + } + + const domesticCompose = readFileSync(composeFiles[0], 'utf8'); + assert.match(domesticCompose, /FILE_UPLOADS_HOST_DIR[^\n]+:\/data\/files/); +}); + +test('runtime image does not prepare an application log directory', () => { + const dockerfile = readFileSync('deploy/docker/Dockerfile', 'utf8'); + assert.doesNotMatch(dockerfile, /\/app\/logs/); +}); diff --git a/warehouse/src/main/resources/application.yml b/warehouse/src/main/resources/application.yml index 0d0085386..55de853bf 100644 --- a/warehouse/src/main/resources/application.yml +++ b/warehouse/src/main/resources/application.yml @@ -27,14 +27,3 @@ baiyue: jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start} internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long} management.endpoints.web.exposure.include: health,info,prometheus - -logging: - file: - name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}" - logback: - rollingpolicy: - file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}" - max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}" - max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}" - total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}" - clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}" From b65f98451dd26d3210364ce36d3b1237214779ea Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 15:15:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=B5=B7=E5=A4=96PDA?= =?UTF-8?q?=E6=9F=9C=E7=BA=A7=E8=BD=AC=E6=8D=A2=E9=AA=8C=E6=94=B6=E5=A5=91?= =?UTF-8?q?=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...hase3-phase4-departure-pickup-chain-contract.test.mjs | 2 ++ scripts/admin-test-env-modules/phase4-overseas-pda.mjs | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/admin-test-env-modules/phase3-phase4-departure-pickup-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-phase4-departure-pickup-chain-contract.test.mjs index b9c9d1c20..1e5348a68 100644 --- a/scripts/admin-test-env-modules/phase3-phase4-departure-pickup-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-phase4-departure-pickup-chain-contract.test.mjs @@ -44,6 +44,8 @@ test('outbound-created query supports automated or manual carrier then reaches P assert.match(pda, /convertReceivingContainers\(context, producers\)/) assert.match(pda, /last-leg\/orders\/container-summaries/) assert.match(pda, /last-leg\/inbound-plans\/convert-container/) + assert.match(pda, /Number\(summary\?\.missingInboundCodeCount\) === 0/) + assert.doesNotMatch(pda, /summary\?\.eligible/) assert.match(pda, /item\?\.shippingReady === true/) assert.match(pda, /nonBlank\(item\?\.carrierScheduleQueryId\)/) assert.match(pda, /targetCorrelated/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs index 7ce5fb9f3..753ed739e 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs @@ -279,11 +279,12 @@ async function convertReceivingContainers(context, producers) { const summaries = success(summaryResponse) && Array.isArray(summaryResponse.body?.data?.records) ? summaryResponse.body.data.records : [] const summary = summaries.find((item) => id(item?.containerNo) === containerNo) + const convertible = Boolean(nonBlank(summary?.sourceContainerBookingId) + && Number(summary?.inboundCodeCount) > 0 && Number(summary?.missingInboundCodeCount) === 0) context.addStep(MODULE, `读取待分配柜级摘要 ${containerNo}`, summaryResponse, - Boolean(summary?.eligible && nonBlank(summary?.sourceContainerBookingId) - && Number(summary?.inboundCodeCount) > 0), - `sourceContainerBookingId=${id(summary?.sourceContainerBookingId) || 'missing'}, eligible=${summary?.eligible === true}, inboundCodes=${summary?.inboundCodeCount ?? 'missing'}; ${trace(summaryResponse)}`, OPTIONS) - if (!summary?.eligible || !nonBlank(summary?.sourceContainerBookingId)) return false + convertible, + `sourceContainerBookingId=${id(summary?.sourceContainerBookingId) || 'missing'}, inboundCodes=${summary?.inboundCodeCount ?? 'missing'}, missingInboundCodes=${summary?.missingInboundCodeCount ?? 'missing'}; ${trace(summaryResponse)}`, OPTIONS) + if (!convertible) return false const body = { sourceContainerBookingId: id(summary.sourceContainerBookingId) } const key = `${context.marker}-pda-convert-${containerNo}`.slice(0, 128) const convertedResponse = await context.requestOverseas('POST', From 70d66c2fae99f6004abc8cb68ee5e26a5023b35e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 15:15:41 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 6 +++--- file/AGENTS.md | 3 ++- firstleg/AGENTS.md | 2 +- order/AGENTS.md | 2 +- scripts/agents-index-contract.ps1 | 28 ++++++++++++++++++++++++++-- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8a60a4f8a..526e62f64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,13 +22,13 @@ | 客户档案、成员、审核、RBAC、授权快照 | `iam/` | `iam/AGENTS.md` | | 客户建档默认尺寸初始化与补偿 | `iam/`、`firstleg/` | IAM `CustomerDimensionRuleInitializationService` 持久化意图、`CustomerDimensionInitializationRelay` 后台投递;Firstleg 内部入口 `/internal/firstleg/v1/customer-dimension-configs/defaults`,未 `READY` 不得审核通过 | | 消息中心、站内通知、消息模板、收件箱 | `iam/` | `iam/AGENTS.md`,再按 `src/main/java/com/baiyue/iam/notification/` 定位;业务事件生产者回到所属服务 | -| 订单、货物、快照、状态、幂等、Outbox | `order/` | `order/AGENTS.md` | +| 订单、货物、快照、状态、幂等、Outbox | `order/` | `order/AGENTS.md`;统一列表由 `UnifiedOrderQueryService` 组合交易与履约投影,Order 为 `SUBMITTED` 时必须优先返回 `PENDING_REVIEW/APPROVE_ORDER`,不得被 Firstleg 准备中状态覆盖 | | 平台修改客户原始申报、履约渠道变更与跨服务同步 | `order/`、`firstleg/` | Order `OrderDeclarationUpdateService.java`、`OrderInboundOutboxPublisher.java`、`order-v1.yaml`;Firstleg `OrderDeclarationSyncService.java`、`OrderDeclarationSyncMapper.java`、`firstleg-v1.yaml`。只更新申报快照;渠道变化先失败关闭校验履约可逆性与重新计价,成功后由版本化 `ORDER_DECLARATION_UPDATED_V1` 幂等同步,不覆盖实测、箱码或已确认费用 | | 履约分配、入库、测量、审核、FBA 计费 | `firstleg/` | `firstleg/AGENTS.md` | | Firstleg 内部模块化与依赖基线 | `firstleg/` | `firstleg/docs/firstleg-internal-modularization-baseline.md`、`firstleg/scripts/analyze-firstleg-dependencies.mjs`;价格簿查询和客户统计按端口/适配器切片,不改变服务与数据库边界 | | Mapper 新增、移动、Bean 缺失与扫描遗漏 | 目标持久化服务 | 所有 Mapper 显式 `@Mapper`,启动类按服务根包注解过滤扫描;先运行 `node scripts/mapper-scan-contract.mjs`,再按目标模块执行 `test-compile/test`,不得等部署启动发现 | | 脚本管理、全量验证、测试造数与诊断 | 仓库根 `scripts/`;服务专属脚本留在 `/scripts/` | 先读 `scripts/AGENTS.md` 和 `scripts/README.md`;Admin HTTP 内部模块继续读 `scripts/admin-test-env-modules/README.md`。新增入口必须登记用途、目标环境、数据副作用和报告位置,并通过 `scripts/script-documentation-contract.test.mjs` | -| 分柜、柜次、智能排柜、预装/装柜清单、真实柜号后补、PDA | `firstleg/` | `firstleg/AGENTS.md`,再按 `split/`、`loading/`、`outbound/` 定位;创建柜次时原子生成唯一预装计划,加入入仓码时同步建立关联,不设推送步骤;预装/正式详情由 `/loading-plans/{planId}/lifecycle` 解析。真实集装箱号不阻塞订舱关联、计划确认或 PDA task 创建,可在 `EDITING/LOADING` 首次补录,但必须在 PDA 最终完成前存在 | +| 分柜、柜次、智能排柜、预装/装柜清单、真实柜号后补、PDA | `firstleg/` | `firstleg/AGENTS.md`,再按 `split/`、`loading/`、`outbound/` 定位;创建柜次时原子生成唯一预装计划,加入入仓码时同步建立关联,不设推送步骤;预装/正式详情由 `/loading-plans/{planId}/lifecycle` 解析。真实集装箱号不阻塞订舱关联、计划确认或 PDA task 创建,可在 `EDITING/LOADING` 首次补录,但必须在 PDA 最终完成前存在。装柜证据上传重试按幂等键与包含文件内容的请求哈希识别,完全重放不得再次调用 File | | 合柜报关 / 同柜不可拆组、跨客户合柜、整组分配/移除 | `firstleg/` | `loading/coload/controller/PlatformColoadGroupController.java`、`service/ColoadGroupService.java`、`persistence/ColoadGroupMapper.java`、`src/main/resources/openapi/coload-groups-v1.yaml`;按订单建组并冻结全部可用入仓片段,仅要求同国内仓、同渠道 | | 智能装柜入仓码候选与箱数分配 | `firstleg/` | `controller/PlatformSmartLoadingController.java` 的 `/inbound-candidates`、`/plans/{planId}/inbound-assignments`,业务编排 `service/SmartLoadingService.java` | | 订舱资料/打印、船期、订舱级拖车、报关 | `firstleg/` | `oceanbooking/`、`portschedule/`、`TruckAppointmentService.java`、`outbound/dispatch/`、`customs/`;拖车只绑定 `RELEASED` 订舱,不携带客户、柜次、装柜计划或真实柜号,每订舱最多一张非终态预约,订舱文件联查 `file/AGENTS.md` | @@ -44,7 +44,7 @@ | 海外 API 路由、海外 Token、Audience/JWKS | `gateway-overseas/` | `gateway-overseas/AGENTS.md` | | 海外 IAM 状态与权限版本投影 | `lastleg-service/security/` | `lastleg-service/AGENTS.md`;内部入口 `/internal/lastleg/security/v1/events`、`/validate`;消费三类 IAM 安全事件并为海外 Gateway 提供 fail-closed 校验,投影表归 `baiyue_lastleg` | | 尾程订单、海外入仓计划、换标、库存、采购、本土卡车 | `lastleg-service/` | `lastleg-service/AGENTS.md`,再按 `inboundplan/`、`relabel/`、`inventory/`、`procurement/`、`trucking/` 定位 | -| 柜级入仓计划进度与整柜转换 | `lastleg-service/` | `orders/container-summaries` 按 `sourceContainerBookingId` 跨客户聚合全柜进度与 `allPlansReady`,详情返回柜内全部订单/入仓码/计划且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结事实;`inbound-plans/convert-container` 锁定同柜订单,每票生成独立计划,整批原子且幂等 | +| 柜级入仓计划进度与整柜转换 | `lastleg-service/` | `orders/container-summaries` 按 `sourceContainerBookingId` 跨客户聚合,分页前排除全部入仓码均已生成有效独立计划的柜,只返回仍有待生成计划入仓码的柜;详情返回柜内全部订单/入仓码/计划且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结事实并升序、空值最后;`inbound-plans/convert-container` 锁定同柜订单,每票生成独立计划,整批原子且幂等 | | 海外入仓分页、FBA 预约、PDA 柜号签收与入仓 | `lastleg-service/` | 平台入口 `inboundplan/controller/InboundPlanController.java`、`InboundPlanQueryMapper.java`、`InboundFbaAppointmentService.java`;PDA 公网入口 `inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,旧 planNo/allocationId Controller 已移除 | | 海外仓排车查询、候选与智能预排 | `lastleg-service/` | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`;草稿柜、托盘锁和预排归 `dispatch/`,只读排车和预排创建独立开关,不自动开放编辑、取消、发车、PDA 出库或派送 | | 客户海外仓贴标、转运、废弃与退货换标 | `lastleg-service/` | `relabel/`、`transfer/`、`discard/` 的 Merchant Controller/Service;Client 消费端 `../apps/client-web/src/modules/overseas-warehouse/`,统一经 `gateway-overseas/` | diff --git a/file/AGENTS.md b/file/AGENTS.md index 032d85031..903c56b15 100644 --- a/file/AGENTS.md +++ b/file/AGENTS.md @@ -10,7 +10,7 @@ | 目标 | 入口 | |---|---| -| 商户/平台资质 | `controller/QualificationFileController.java`、`PlatformQualificationFileController.java` | +| 商户/平台资质 | `controller/QualificationFileController.java`、`PlatformQualificationFileController.java`、`controller/QualificationTypes.java`;契约 `src/main/resources/openapi/qualification-required-package-v1.yaml`。营业执照必须与 18 位统一社会信用代码走 `required-package`;法人身份证选填,客户可在已有营业执照后通过 `/api/merchant/v1/files/qualifications` 单独新增或替换 | | 导出中心 | `controller/PlatformExportTaskController.java`、`service/ExportTaskService.java`、`service/ExportStateService.java`、`service/GenericXlsxExportRunner.java`、`config/ExportDefinitionRegistry.java`、`client/ExportProviderClient.java`、`mq/` | | 测量图片/证据 | `controller/PlatformMeasurementImageController.java`、`InternalMeasurementImageController.java`、`service/MeasurementImageService.java` | | 平台订单商品图片读取 | `controller/PlatformProductImageController.java` 的 `/api/platform/v1/files/product-images/{tenantId}/{fileId}`、`service/ProductImageService.java`;仅平台 `platform:order:list` 可按明确客户与文件 ID 内联读取,响应 `no-store` | @@ -27,6 +27,7 @@ ## 修改约束 - 上传先写真实文件再登记 IAM 元数据;异常结果未知时保留文件供重试,禁止伪造 URL。 +- 客户单文件资质入口只接受 `LEGAL_PERSON_ID/FREIGHT_FORWARDER_CERTIFICATE/BANK_ACCOUNT_LICENSE`,客户范围只取登录 Token;营业执照不得绕过 `required-package`。同类当前资质已存在时必须显式 `replace=true`,成功后旧元数据标记为已替代;上传不会自动提交审核。 - 客户业务文件按客户、业务 ID 和文件类型校验归属;重量图片可 OCR,第三方尺寸证据不得触发重量 OCR。装柜四步照片与铅封是跨客户物理柜证据,所有者固定为平台(`tenantId=0`),业务 ID 固定为 `containerBookingId`,File 按柜与五类 `LOADING_*` 精确校验且不得跨柜复用;PDA `taskId` 只由 Firstleg 保存为来源/审计,不进入 File 所有权。国内 PDA 禁止直连 File 平台接口,必须经 Firstleg 校验任务、仓库和操作者后,由 Firstleg 使用内部服务令牌访问 File。 - 商品图片平台读取必须同时校验平台身份、`platform:order:list`、客户范围、文件业务类型与正整数 ID;不得提供匿名 URL、跨客户读取或把内部存储路径返回页面。 - 公共导出只接受 `ExportDefinitionRegistry` 明确登记的 `exportType`,由 File 调用所属业务服务的内部 Provider 分批取数并生成文件;业务页面不得直连 Provider。新增或修改导出类型时同步检查业务服务 Provider、File Feign 客户端/响应上限、权限、Admin 下载中心标签与重试映射及契约测试。 diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index 5eb092492..86bc3622a 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -33,7 +33,7 @@ | 同柜不可拆组 | `loading/coload/controller/PlatformColoadGroupController.java`、`service/ColoadGroupService.java`、`persistence/ColoadGroupMapper.java`;契约 `src/main/resources/openapi/coload-groups-v1.yaml`。请求按订单 ID 建组,服务端冻结每票全部 `WAIT_SPLIT` 可用入仓片段与满箱数;仅要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均保持整组原子 | | 货柜与智能排柜 | `controller/PlatformContainerController.java`、`PlatformSmartLoadingController.java`、`service/SmartLoadingService.java`、`repository/SmartLoadingRepository.java`、`loading/`;按入仓码候选与箱数分配分别从 `/inbound-candidates`、`/plans/{planId}/inbound-assignments` 进入,明细规格拆分留在服务端 | | 货柜资料、PDA 证据与业务总览 | `controller/PlatformContainerController.java` 的 `/{id}/overview`、`/{id}/documents/{documentType}`,聚合 `mapper/ContainerOverviewMapper.java`;契约 `src/main/resources/openapi/phase3-container-overview-v1.yaml`,四类资料固定为目的港清关、国内出口报关、DO、PU 的当前唯一版本 | -| PDA 拣货与装柜 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java` | +| PDA 拣货与装柜、证据幂等上传 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java`、`loading/service/CabinetLoadingEvidenceService.java`;证据写入和 multipart 上传均先以任务内幂等键匹配请求哈希,上传哈希覆盖仓库、版本、证据类型、锁码、备注、文件名、媒体类型、大小与文件内容;完全重放返回当前视图且不得再次上传 File,相同键不同内容返回冲突 | | 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType=FBA|OVERSEAS`,OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` | | 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` | | 三期海运订舱、资料/打印、关联候选与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`OceanBookingDocumentService.java`、`OceanBookingPrintService.java`、`oceanbooking/mapper/OceanBookingMapper.java`、`portschedule/controller/PlatformPortScheduleController.java`;文件经 `client/OceanBookingFileClient.java` 进入 File,关联候选 `/api/platform/v1/ocean-bookings/link-candidates`,契约 `src/main/resources/openapi/ocean-bookings-v1.yaml` | diff --git a/order/AGENTS.md b/order/AGENTS.md index 5aecc7efe..71982d58b 100644 --- a/order/AGENTS.md +++ b/order/AGENTS.md @@ -12,7 +12,7 @@ | 目标 | 入口 | |---|---| | 平台订单 API | `controller/PlatformFirstLegOrderController.java` | -| 统一订单查询与稳定渠道编码 | `service/UnifiedOrderQueryService.java`、`dto/UnifiedOrderResponse.java`、`src/main/resources/openapi/order-v1.yaml`;`channelCode` 直接来自 Order/Firstleg 权威快照,只用于业务校验,不得由 `channelName` 推导 | +| 统一订单查询、审核优先状态与稳定渠道编码 | `service/UnifiedOrderQueryService.java`、`dto/UnifiedOrderResponse.java`、`src/main/resources/openapi/order-v1.yaml`;`channelCode` 直接来自 Order/Firstleg 权威快照,只用于业务校验,不得由 `channelName` 推导。Order 为 `SUBMITTED` 时即使已有 Firstleg 入库投影,统一主状态/下一动作仍固定为 `PENDING_REVIEW/APPROVE_ORDER` | | 平台版本化修改客户原始申报 | `controller/PlatformUnifiedOrderController.java` 的 `PUT /api/platform/v1/orders/{orderId}/declaration`、`service/OrderDeclarationUpdateService.java`、`dto/OrderDeclarationDtos.java`;权限 `platform:order:declaration:edit`,货物以订单内 `itemNo` 定位并保持行集合完整,契约 `src/main/resources/openapi/order-v1.yaml` | | 申报同步、渠道履约/计价门禁 | `service/OrderFulfillmentChannelChangeGuard.java`、`OrderPricingChannelGuard.java`、`OrderInboundOutboxPublisher.java`,内部客户端 `FirstlegInboundClient.java`、`FirstlegPricingClient.java`;审核后通过版本化 `ORDER_DECLARATION_UPDATED_V1` 同步 Firstleg | | 入仓码业务关系摘要 | `service/InboundCodeAssociationService.java`、`dto/InboundCodeAssociationSummary.java`、平台/客户 `FirstLegOrderController` 的 `/inbound-code-associations/{inboundCode}`;客户入口必须按认证客户隔离 | diff --git a/scripts/agents-index-contract.ps1 b/scripts/agents-index-contract.ps1 index 8bf773c4a..272e6ccb2 100644 --- a/scripts/agents-index-contract.ps1 +++ b/scripts/agents-index-contract.ps1 @@ -145,6 +145,7 @@ Assert-Contains $firstlegAgents '/loading-plans/{planId}/lifecycle' 'Firstleg $orderOpenApi = Read-RequiredText (Join-Path $backendRoot 'order/src/main/resources/openapi/order-v1.yaml') $firstlegOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/firstleg-v1.yaml') $orderDeclarationService = Read-RequiredText (Join-Path $backendRoot 'order/src/main/java/com/baiyue/order/service/OrderDeclarationUpdateService.java') +$unifiedOrderQueryService = Read-RequiredText (Join-Path $backendRoot 'order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java') $firstlegDeclarationService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/service/OrderDeclarationSyncService.java') $adminDeclarationEditor = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/unified-orders/components/OrderDeclarationEditorDialog.vue') Assert-Contains $rootAgents '平台修改客户原始申报与履约渠道' '根订单申报快速路由' @@ -160,6 +161,11 @@ Assert-Contains $orderDeclarationService 'ORDER_DECLARATION_UPDATED_V1' 'Order Assert-Contains $firstlegDeclarationService 'mapper.byEvent(event.tenantId(), event.eventId())' 'Firstleg 申报 eventId 幂等' Assert-Contains $firstlegDeclarationService 'event.version() < current.orderVersion()' 'Firstleg 申报版本单调性' Assert-Contains $adminDeclarationEditor '仓库/PDA 实测数据不会被覆盖' 'Admin 申报事实边界提示' +Assert-Contains $rootAgents '统一列表仍固定显示 `PENDING_REVIEW` 并提供审核动作' '根待审核订单优先状态' +Assert-Contains $orderAgents '统一主状态/下一动作仍固定为 `PENDING_REVIEW/APPROVE_ORDER`' 'Order 待审核优先状态' +Assert-Contains $unifiedOrderQueryService 'order.getStatus() == OrderStatus.SUBMITTED' 'Order 待审核状态判断' +Assert-Contains $unifiedOrderQueryService 'pendingReview ? "PENDING_REVIEW" : summary.mainStatus()' 'Order 待审核主状态优先' +Assert-Contains $unifiedOrderQueryService 'pendingReview ? "APPROVE_ORDER" : summary.nextAction()' 'Order 待审核动作优先' # 企业联系信息与账号登录标识已经解耦,资质审核不得重新把法人身份证设为必填。 $authAgents = Read-RequiredText (Join-Path $backendRoot 'auth/AGENTS.md') @@ -183,9 +189,18 @@ Assert-Contains $adminCustomersView '不会修改企业管理员登录手机号' Assert-Contains $clientCompanyProfile '不会修改任何成员的登录手机号' 'Client 公司联系电话提示' Assert-Contains $clientMembersView '用于该成员手机号登录;修改后会同步认证账号并退出该成员现有会话' 'Client 成员登录手机号提示' $fileAgents = Read-RequiredText (Join-Path $backendRoot 'file/AGENTS.md') +$qualificationOpenApi = Read-RequiredText (Join-Path $backendRoot 'file/src/main/resources/openapi/qualification-required-package-v1.yaml') +Assert-Contains $rootAgents '法人身份证选填,可在已有营业执照后单独补传或替换' '根法人身份证单独上传语义' +Assert-Contains $fileAgents '法人身份证选填,客户可在已有营业执照后通过 `/api/merchant/v1/files/qualifications` 单独新增或替换' 'File 法人身份证单独上传入口' +Assert-Contains $qualificationOpenApi '/api/merchant/v1/files/qualifications:' '客户单文件资质 OpenAPI' +Assert-Contains $qualificationOpenApi 'LEGAL_PERSON_ID 可在已有营业执照后单独新增或替换' '客户法人身份证单独上传契约' +Assert-Contains $qualificationOpenApi 'BUSINESS_LICENSE 不允许走此入口' '营业执照整包上传边界' +Assert-Contains $clientCompanyProfile '法人身份证已保存,尚未提交审核' 'Client 法人身份证单独上传反馈' +Assert-NotContains $clientCompanyProfile '当前文件服务尚未开放法人身份证单独上传或替换' 'Client 已移除法人身份证旧阻断' $truckOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/truck-appointments-v1.yaml') $loadingListsOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/loading-lists-v1.yaml') $pdaLoadingApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/api/pda-loading.js') +$cabinetEvidenceService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceService.java') Assert-Contains $rootAgents '拖车预约由 `RELEASED` 订舱拥有,不按客户、柜次、装柜计划或真实柜号归属' '根订舱级拖车语义' Assert-Contains $rootAgents '证据以 `taskId + warehouseId` 进入任务专用 API' '根柜级证据入口' Assert-Contains $adminAgents '订舱级拖车预约' 'Admin 拖车快速入口' @@ -210,6 +225,11 @@ Assert-Contains $adminAgents 'PDA 最终完成前必须补齐' 'Admin 真实柜 Assert-Contains $pdaAgents '当前 `taskId + warehouseId` 走任务专用 API' '国内 PDA 柜级证据入口' Assert-Contains $pdaLoadingApi '/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/evidence?warehouseId=' '国内 PDA 证据查询 API' Assert-NotContains $pdaLoadingApi '/inbounds/${encodeURIComponent(inboundId)}/loading/evidence' '国内 PDA 旧入仓证据 API' +Assert-Contains $rootAgents '上传以幂等键绑定包含文件内容的请求哈希' '根装柜证据上传重放边界' +Assert-Contains $firstlegAgents '完全重放返回当前视图且不得再次上传 File' 'Firstleg 装柜证据重放边界' +Assert-Contains $cabinetEvidenceService 'uploadHash(warehouseId, expectedVersion, evidenceType, sealNo, remark, multipart)' '装柜证据上传请求哈希' +Assert-Contains $cabinetEvidenceService 'digest.update(multipart.getBytes())' '装柜证据文件内容哈希' +Assert-Matches $cabinetEvidenceService '(?s)mapper\.byOperation\(taskId, key\).{0,500}?return view\(scope\);.{0,500}?files\.upload' '装柜证据上传先查重放再调用 File' $mapperModuleAgents = @( @{ Module = 'audit-service'; Application = 'BaiyueAuditApplication.java' }, @{ Module = 'auth'; Application = 'BaiyueAuthApplication.java' }, @@ -468,7 +488,11 @@ Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/orders/contai Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/inbound-plans/convert-container:' 'Lastleg 整柜计划转换契约' Assert-Contains $lastlegBusinessOpenApi 'sourceContainerBookingId' 'Lastleg 稳定来源柜身份' Assert-Contains $adminAgents '/orders/container-summaries' 'Admin 柜级摘要目标入口' -Assert-Contains $adminAgents 'allPlansReady' 'Admin 柜级计划就绪事实' +Assert-Contains $rootAgents '只分页返回仍有入仓码待生成有效独立计划的柜' '根柜级计划待处理筛选' +Assert-Contains $backendAgents '分页前排除全部入仓码均已生成有效独立计划的柜' '后端柜级计划待处理筛选' +Assert-Contains $adminAgents '返回仍有待生成计划入仓码的柜' 'Admin 柜级计划待处理筛选' +Assert-Contains $lastlegAgents '分页前排除全部入仓码均已生成有效独立计划的柜' 'Lastleg 柜级计划待处理筛选' +Assert-Contains $lastlegBusinessOpenApi '仅返回仍有入仓码待生成有效独立计划的柜' 'Lastleg 柜级计划待处理 OpenAPI' Assert-Contains $adminAgents 'ACTIVE + FBA' 'Admin FBA 权威仓候选' Assert-Contains $adminAgents '/inbound-plans/convert-container' 'Admin 整柜转换目标入口' Assert-Contains $adminLastMileApi '/orders/container-summaries' 'Admin 柜级摘要真实调用' @@ -786,5 +810,5 @@ if ($failures.Count -gt 0) { exit 1 } -Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、前端模块、Client active 路由、脚本目录治理、企业联系信息与登录标识解耦、订单申报版本同步与渠道门禁、业务编号与内部 ID 隔离、装柜清单生命周期与真实柜号后补、订舱级拖车与柜级证据、四期范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green +Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、前端模块、Client active 路由、脚本目录治理、企业联系信息与登录标识解耦、选填法人身份证独立上传、待审核订单状态优先、订单申报版本同步与渠道门禁、装柜证据内容哈希重放、业务编号与内部 ID 隔离、装柜清单生命周期与真实柜号后补、订舱级拖车与柜级证据、四期柜级待处理计划筛选与范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green exit 0