From 8bcd23f307d123ef31a0c1ad1d89ad28ea2f6783 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 13 Aug 2026 15:59:05 +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 --- gateway/src/main/resources/application.yml | 4 +- ...merTransitStatisticsRouteContractTest.java | 34 +++++++++++++ .../GatewayFileRouteConfigurationTest.java | 4 +- .../customer-operating-statistics.mjs | 36 ++++++++------ .../ocean-booking-fixture.mjs | 49 +++++++++++++++++++ .../admin-test-env-modules/ocean-bookings.mjs | 10 ++-- 6 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 gateway/src/test/java/com/baiyue/gateway/config/CustomerTransitStatisticsRouteContractTest.java create mode 100644 scripts/admin-test-env-modules/ocean-booking-fixture.mjs diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index 8d7e45403..15ba9c169 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -65,7 +65,7 @@ spring: - id: warehouse-service uri: lb://warehouse-service predicates: - - Path=/api/merchant/v1/warehouses/**,/api/merchant/v1/partners/**,/api/platform/v1/warehouses/**,/api/platform/v1/warehouse-admin/**,/api/platform/v1/partners/**,/api/platform/v1/express-companies/**,/api/platform/v1/pallets/**,/api/platform/v1/locations/**,/api/platform/v1/overseas-storage-fees/**,/api/platform/v1/iam/tenants/{tenantId}/overseas-storage-fees,/api/platform/v1/iam/tenants/{tenantId}/overseas-storage-fees/** + - Path=/api/merchant/v1/warehouses/**,/api/merchant/v1/partners/**,/api/platform/v1/warehouses/**,/api/platform/v1/warehouse-admin/**,/api/platform/v1/partners/**,/api/platform/v1/express-companies/**,/api/platform/v1/pallets/**,/api/platform/v1/locations/**,/api/platform/v1/overseas-storage-fees/**,/api/platform/v1/suppliers/**,/api/platform/v1/domestic-containers/**,/api/platform/v1/iam/tenants/{tenantId}/overseas-storage-fees,/api/platform/v1/iam/tenants/{tenantId}/overseas-storage-fees/** - id: file-service uri: lb://file-service predicates: @@ -77,7 +77,7 @@ spring: - id: firstleg-service uri: lb://firstleg-service predicates: - - Path=/api/merchant/v1/first-leg/**,/api/merchant/v1/customs/**,/api/platform/v1/first-leg/**,/api/platform/v1/customs/**,/api/platform/v1/port-schedules/**,/api/platform/v1/carrier-schedule-queries/**,/api/platform/v1/containers/**,/api/platform/v1/ocean-bookings/**,/api/platform/v1/truck-appointments/**,/api/platform/v1/fba-pricing/**,/api/platform/v1/inbounds/**,/api/platform/v1/fee-items/**,/api/platform/v1/loading-plans/**,/api/platform/inbounds/**,/openapi/v1/inbound-measurements/** + - Path=/api/merchant/v1/first-leg/**,/api/merchant/v1/customs/**,/api/platform/v1/first-leg/**,/api/platform/v1/customs/**,/api/platform/v1/port-schedules/**,/api/platform/v1/carrier-schedule-queries/**,/api/platform/v1/containers/**,/api/platform/v1/customers/{customerId}/container-transit-summary,/api/platform/v1/ocean-bookings/**,/api/platform/v1/truck-appointments/**,/api/platform/v1/fba-pricing/**,/api/platform/v1/inbounds/**,/api/platform/v1/fee-items/**,/api/platform/v1/loading-plans/**,/api/platform/inbounds/**,/openapi/v1/inbound-measurements/** - id: finance-service uri: lb://finance-service predicates: diff --git a/gateway/src/test/java/com/baiyue/gateway/config/CustomerTransitStatisticsRouteContractTest.java b/gateway/src/test/java/com/baiyue/gateway/config/CustomerTransitStatisticsRouteContractTest.java new file mode 100644 index 000000000..a6ab80981 --- /dev/null +++ b/gateway/src/test/java/com/baiyue/gateway/config/CustomerTransitStatisticsRouteContractTest.java @@ -0,0 +1,34 @@ +package com.baiyue.gateway.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.config.GatewayProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = { + "spring.cloud.nacos.discovery.enabled=false", + "spring.cloud.nacos.discovery.register-enabled=false", + "spring.cloud.nacos.config.enabled=false", + "spring.cloud.gateway.discovery.locator.enabled=false", + "baiyue.security.jwt-secret=test-jwt-secret-at-least-32-bytes-long" +}) +class CustomerTransitStatisticsRouteContractTest { + @Autowired private GatewayProperties gatewayProperties; + + @Test + void routesCustomerTransitSummaryToFirstlegService() { + var route = gatewayProperties.getRoutes().stream() + .filter(candidate -> "firstleg-service".equals(candidate.getId())) + .findFirst() + .orElseThrow(); + + assertThat(route.getUri().toString()).isEqualTo("lb://firstleg-service"); + assertThat(route.getPredicates()).singleElement().satisfies(predicate -> { + assertThat(predicate.getName()).isEqualTo("Path"); + assertThat(String.join(",", predicate.getArgs().values())) + .contains("/api/platform/v1/customers/{customerId}/container-transit-summary"); + }); + } +} diff --git a/gateway/src/test/java/com/baiyue/gateway/config/GatewayFileRouteConfigurationTest.java b/gateway/src/test/java/com/baiyue/gateway/config/GatewayFileRouteConfigurationTest.java index 66d5290f6..5dacabec2 100644 --- a/gateway/src/test/java/com/baiyue/gateway/config/GatewayFileRouteConfigurationTest.java +++ b/gateway/src/test/java/com/baiyue/gateway/config/GatewayFileRouteConfigurationTest.java @@ -69,7 +69,9 @@ class GatewayFileRouteConfigurationTest { assertThat(paths) .contains("/api/platform/v1/pallets/**") .contains("/api/platform/v1/locations/**") - .contains("/api/platform/v1/overseas-storage-fees/**"); + .contains("/api/platform/v1/overseas-storage-fees/**") + .contains("/api/platform/v1/suppliers/**") + .contains("/api/platform/v1/domestic-containers/**"); }); } diff --git a/scripts/admin-test-env-modules/customer-operating-statistics.mjs b/scripts/admin-test-env-modules/customer-operating-statistics.mjs index 69a6f98fa..47b70d3d8 100644 --- a/scripts/admin-test-env-modules/customer-operating-statistics.mjs +++ b/scripts/admin-test-env-modules/customer-operating-statistics.mjs @@ -13,27 +13,33 @@ export async function run(context) { const orderPath = `/api/platform/v1/orders/first-leg/tenant-statistics?tenantId=${encodeURIComponent(customerId)}` const orderResponse = await context.request('GET', orderPath) const orders = context.apiSuccess(MODULE, '查询累计与本月已提交主运单统计', orderResponse, `customerId=${customerId}`, OPTIONS) - context.assert(MODULE, '运单统计返回上海时区半开月份边界与客户隔离键', orderResponse, - id(orders?.tenantId) === customerId && nonNegative(orders?.totalSubmittedOrders) - && nonNegative(orders?.currentMonthSubmittedOrders) && orders?.zoneId === 'Asia/Shanghai' - && Boolean(orders?.monthStart) && Boolean(orders?.nextMonthStart) && Boolean(orders?.asOf), - `tenantId=${id(orders?.tenantId) || 'missing'}, zoneId=${orders?.zoneId || 'missing'}`, OPTIONS) + if (orders !== null) { + context.assert(MODULE, '运单统计返回上海时区半开月份边界与客户隔离键', orderResponse, + id(orders?.tenantId) === customerId && nonNegative(orders?.totalSubmittedOrders) + && nonNegative(orders?.currentMonthSubmittedOrders) && orders?.zoneId === 'Asia/Shanghai' + && Boolean(orders?.monthStart) && Boolean(orders?.nextMonthStart) && Boolean(orders?.asOf), + `tenantId=${id(orders?.tenantId) || 'missing'}, zoneId=${orders?.zoneId || 'missing'}`, OPTIONS) + } const financePath = `/api/platform/finance/v1/customer-accounts/${encodeURIComponent(customerId)}/summary` const financeResponse = await context.request('GET', financePath) const finance = context.apiSuccess(MODULE, '查询非VOIDED正式结算应收汇总', financeResponse, `customerId=${customerId}`, OPTIONS) - context.assert(MODULE, '应收汇总保持CNY十进制金额与客户隔离键', financeResponse, - id(finance?.customerId) === customerId && finance?.currency === 'CNY' - && ['totalReceivable', 'receivedAmount', 'outstandingReceivable'].every((field) => nonNegative(finance?.[field])) - && Number(finance?.outstandingReceivable) <= Number(finance?.totalReceivable), - `customerId=${id(finance?.customerId) || 'missing'}, currency=${finance?.currency || 'missing'}`, OPTIONS) + if (finance !== null) { + context.assert(MODULE, '应收汇总保持CNY十进制金额与客户隔离键', financeResponse, + id(finance?.customerId) === customerId && finance?.currency === 'CNY' + && ['totalReceivable', 'receivedAmount', 'outstandingReceivable'].every((field) => nonNegative(finance?.[field])) + && Number(finance?.outstandingReceivable) <= Number(finance?.totalReceivable), + `customerId=${id(finance?.customerId) || 'missing'}, currency=${finance?.currency || 'missing'}`, OPTIONS) + } const transitPath = `/api/platform/v1/customers/${encodeURIComponent(customerId)}/container-transit-summary` const transitResponse = await context.request('GET', transitPath) const transit = context.apiSuccess(MODULE, '查询已开船未到仓在途柜量', transitResponse, `customerId=${customerId}`, OPTIONS) - context.assert(MODULE, '在途柜量保持TEU小数、未知柜型与客户隔离键', transitResponse, - id(transit?.customerId) === customerId && nonNegative(transit?.containerCount) - && nonNegative(transit?.totalTeu) && nonNegative(transit?.unknownContainerCount) - && Number(transit?.unknownContainerCount) <= Number(transit?.containerCount) && Boolean(transit?.definition), - `customerId=${id(transit?.customerId) || 'missing'}, totalTeu=${transit?.totalTeu ?? 'missing'}, unknown=${transit?.unknownContainerCount ?? 'missing'}`, OPTIONS) + if (transit !== null) { + context.assert(MODULE, '在途柜量保持TEU小数、未知柜型与客户隔离键', transitResponse, + id(transit?.customerId) === customerId && nonNegative(transit?.containerCount) + && nonNegative(transit?.totalTeu) && nonNegative(transit?.unknownContainerCount) + && Number(transit?.unknownContainerCount) <= Number(transit?.containerCount) && Boolean(transit?.definition), + `customerId=${id(transit?.customerId) || 'missing'}, totalTeu=${transit?.totalTeu ?? 'missing'}, unknown=${transit?.unknownContainerCount ?? 'missing'}`, OPTIONS) + } } diff --git a/scripts/admin-test-env-modules/ocean-booking-fixture.mjs b/scripts/admin-test-env-modules/ocean-booking-fixture.mjs new file mode 100644 index 000000000..57ac0c44a --- /dev/null +++ b/scripts/admin-test-env-modules/ocean-booking-fixture.mjs @@ -0,0 +1,49 @@ +function text(value) { return value == null ? '' : String(value).trim() } + +export async function resolveOceanBookingMaster(context, module) { + const resources = context.createdResources || {} + const candidates = [resources.phase3Loading, resources.phase3ContainerLoading, + resources.orchestrationFixtures?.phase3Container, resources.inboundMeasurement, + resources.orders].filter(Boolean) + const source = candidates.find((item) => item.marker === context.marker) || candidates[0] + const tenantId = text(source?.tenantId || source?.customerId) + const inboundId = text(source?.inboundId) + let summary = source?.summary || null + if (inboundId && typeof context.request === 'function') { + const response = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(inboundId)}?tenantId=${encodeURIComponent(tenantId)}`) + if (response?.body?.code === '200') summary = response.body.data?.summary || summary + } + const master = { + customerId: text(summary?.tenantId || tenantId), + customerName: text(summary?.customerName || source?.customerName), + channelCode: text(summary?.channelCode || source?.channelCode), + channelName: text(summary?.channelName || source?.channelName), + warehouseId: text(summary?.warehouseId || source?.warehouseId || source?.receivingWarehouseId), + warehouseName: text(summary?.warehouseName || source?.warehouseName), + } + const missing = ['customerId', 'customerName', 'channelCode', 'channelName', 'warehouseId', 'warehouseName'] + .filter((key) => !master[key]) + if (missing.length) { + context.addStep(module, '订舱 fixture 使用本轮正式客户/渠道/仓库主数据', null, false, + `marker=${context.marker}, inboundId=${inboundId || 'missing'}, missing=${missing.join(',')}`, { scenario: 'ocean-booking-master-data', deploymentBlocker: true }) + return null + } + return master +} + +export function oceanBookingDraft(context, suffix, master, labels = {}) { + return { + ...master, + shippingCompanyId: null, shippingCompanyName: labels.shippingCompanyName || `验收船公司-${suffix}`, + bookingAgentId: null, bookingAgentName: labels.bookingAgentName || `验收代理-${suffix}`, + vesselName: labels.vesselName || `验收船-${suffix}`, voyageNo: labels.voyageNo || `V${suffix}`, + departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', + destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY/CY', freightTerm: 'PREPAID', + estimatedSailingAt: labels.estimatedSailingAt, estimatedArrivalAt: labels.estimatedArrivalAt, + remark: context.marker, + parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` }, + { partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` }, + { partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }], + containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 68, maxWeightKg: 28000, quantity: 1 }], + } +} diff --git a/scripts/admin-test-env-modules/ocean-bookings.mjs b/scripts/admin-test-env-modules/ocean-bookings.mjs index 70059ba49..e187b2d26 100644 --- a/scripts/admin-test-env-modules/ocean-bookings.mjs +++ b/scripts/admin-test-env-modules/ocean-bookings.mjs @@ -1,6 +1,7 @@ const MODULE = 'ocean-bookings' const OPTIONS = { scenario: 'ocean-booking-lifecycle', deploymentBlocker: true } const BASE = '/api/platform/v1/ocean-bookings' +import { resolveOceanBookingMaster, oceanBookingDraft } from './ocean-booking-fixture.mjs' function id(value) { return value == null ? '' : String(value) } function codeOf(r) { return r?.body?.code == null ? 'missing' : String(r.body.code) } @@ -15,14 +16,15 @@ async function request(context, method, path, body, headers = {}) { } catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } } } function data(r) { return String(r?.body?.code) === '200' ? r.body.data : null } -function draft(context, suffix) { return { customerId: '10001', customerName: `订舱验收客户-${suffix}`, shippingCompanyId: null, shippingCompanyName: `船公司-${suffix}`, bookingAgentId: null, bookingAgentName: `订舱代理-${suffix}`, vesselName: `验收船-${suffix}`, voyageNo: `V${suffix}`, departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY/CY', freightTerm: 'PREPAID', estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00', billOfLadingNo: `BL-${suffix}`, remark: context.marker, parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` }, { partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` }, { partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }], containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', quantity: 1 }] } } -async function createBooking(context, suffix, key) { return request(context, 'POST', BASE, { expectedVersion: 0, data: draft(context, suffix) }, { 'Idempotency-Key': key }) } +async function createBooking(context, suffix, key, master) { return request(context, 'POST', BASE, { expectedVersion: 0, data: oceanBookingDraft(context, suffix, master, { estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00' }) }, { 'Idempotency-Key': key }) } function step(context, name, r, pass, evidence, options = OPTIONS) { context.addStep(MODULE, name, r, pass, evidence, { ...options, deploymentBlocker: options.deploymentBlocker && !pass }) } export async function run(context) { const containerFlow = context.createdResources?.phase3Loading || context.createdResources?.phase3ContainerLoading const suffix = context.marker.replace(/\D/g, '').slice(-10) + const master = await resolveOceanBookingMaster(context, MODULE) + if (!master) return const list = await request(context, 'GET', `${BASE}?page=1&size=5&keyword=${encodeURIComponent(context.marker)}`) if (missing(list) || list.http === 0) { step(context, '海运订舱服务契约已部署', list, false, `入口 ${BASE} 不可用:HTTP=${list.http}, code=${codeOf(list)}`) @@ -34,7 +36,7 @@ export async function run(context) { for (const status of statuses) { const r = await request(context, 'GET', `${BASE}?page=1&size=5&status=${status}`); const d = data(r); step(context, `订舱 ${status} 状态筛选`, r, r.http === 200 && codeOf(r) === '200' && Array.isArray(d?.records) && d.records.every((x) => x.status === status), `HTTP=${r.http}, code=${codeOf(r)}, records=${d?.records?.length ?? 'n/a'}`) } const key = `${context.marker}-ocean-create` - const createResponse = await createBooking(context, suffix, key) + const createResponse = await createBooking(context, suffix, key, master) const detail = data(createResponse); const bookingId = id(detail?.oceanBooking?.id) context.apiSuccess(MODULE, '创建隔离海运订舱', createResponse, `key=${key}`, OPTIONS) context.assert(MODULE, '订舱 DTO 字段、DRAFT 和9类关联字段完整', createResponse, Boolean(bookingId) && detail?.oceanBooking?.status === 'DRAFT' && detail?.parties?.length === 3 && detail?.containerRequirements?.length === 1, `bookingId=${bookingId || 'missing'}, parties=${detail?.parties?.length ?? 'n/a'}, requirements=${detail?.containerRequirements?.length ?? 'n/a'}`, OPTIONS) @@ -45,7 +47,7 @@ export async function run(context) { context.assert(MODULE, '创建 Idempotency-Key 重放返回同一订舱', replay, String(data(replay)?.oceanBooking?.id) === bookingId, `first=${bookingId}, replay=${id(data(replay)?.oceanBooking?.id)}`, OPTIONS) const path = `${BASE}/${bookingId}` const initialVersion = Number(detail.oceanBooking.version) - const edited = { ...draft(context, suffix), vesselName: `编辑船-${suffix}`, voyageNo: `EV${suffix}` } + const edited = { ...oceanBookingDraft(context, suffix, master, { estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00' }), vesselName: `编辑船-${suffix}`, voyageNo: `EV${suffix}` } const update = await request(context, 'PUT', path, { expectedVersion: initialVersion, data: edited }, { 'Idempotency-Key': `${context.marker}-ocean-edit` }) context.apiSuccess(MODULE, '编辑 DRAFT 订舱', update, `version=${initialVersion}`, OPTIONS) const stale = await request(context, 'PUT', path, { expectedVersion: initialVersion, data: { ...edited, vesselName: `STALE-${suffix}` } }, { 'Idempotency-Key': `${context.marker}-ocean-stale` })