From 9f025edc969724a3c560bd1d50ca7bc5409ba9f5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 27 Sep 2026 01:01:40 +0800 Subject: [PATCH] fix(file): allow customs broker receipt files --- file/AGENTS.md | 2 +- .../file/service/BusinessFileCategory.java | 1 + .../openapi/customs-file-and-export-v1.yaml | 6 +++--- .../config/CustomsFileExportContractTest.java | 2 +- .../service/InternalBusinessFileServiceTest.java | 3 +++ scripts/admin-test-env-modules/README.md | 1 + .../domestic-pda-operator-contract.test.mjs | 16 ++++++++++++++++ .../domestic-pda-operator.mjs | 14 ++++++++++++++ .../manual-state-loading-pda-contract.test.mjs | 2 +- .../manual-state-loading-pda.mjs | 3 ++- .../phase3-container-customs-fixture.mjs | 2 +- .../phase3-container-loading.mjs | 3 ++- .../admin-test-env-modules/phase3-loading.mjs | 3 ++- .../phase3-order-splitting-contract.test.mjs | 2 +- .../phase3-order-splitting.mjs | 3 ++- 15 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 scripts/admin-test-env-modules/domestic-pda-operator-contract.test.mjs create mode 100644 scripts/admin-test-env-modules/domestic-pda-operator.mjs diff --git a/file/AGENTS.md b/file/AGENTS.md index 7d16583d5..c4a01d206 100644 --- a/file/AGENTS.md +++ b/file/AGENTS.md @@ -19,7 +19,7 @@ | 海外异常订单证据 | 通用内部业务文件入口;固定 `businessType=OVERSEAS_EXCEPTION_EVIDENCE`、`tenantId=异常订单所属客户ID`、`businessId=异常订单ID`,File 只校验文件复合归属,异常状态、完成门禁和证据引用归 Lastleg | | 海外换标 PDF 资料 | 通用内部业务文件入口,契约 `src/main/resources/openapi/overseas-relabel-document-v1.yaml`;`OVERSEAS_RELABEL_TOTAL_DOCUMENT` 按 `application.id`、`OVERSEAS_RELABEL_ITEM_DOCUMENT` 按 `item.id` 归属,必须指定明确客户与正数业务 ID;仅非空 PDF、最大 10MiB,校验 MIME 和文件签名。File 按客户、类型、业务 ID、文件 ID 复合校验,Lastleg 校验申请/明细关系、客户归属、权限和业务状态 | | 入库扣货凭证 | 通用内部业务文件入口;固定 `businessType=INBOUND_HOLD_EVIDENCE`、`tenantId=客户ID`、`businessId=入库单ID`,客户作用域单文件最多 10MiB;可见性与历史引用由 Firstleg 校验 | -| 报关源文件、完成文件与买单明细导出 | `src/main/resources/openapi/customs-file-and-export-v1.yaml`;文件类别 `LOADING_LIST_CUSTOMS_SOURCE`、`LOADING_LIST_CUSTOMS_RESULT` 均为客户作用域且最多 50MB,异步导出类型 `LOADING_LIST_BUY_DECLARATION_ITEMS` 由 Firstleg Provider 按固定十三列供数 | +| 报关源文件、报关行回执、完成文件与买单明细导出 | `src/main/resources/openapi/customs-file-and-export-v1.yaml`;文件类别 `LOADING_LIST_CUSTOMS_SOURCE`、`LOADING_LIST_CUSTOMS_RECEIPT`、`LOADING_LIST_CUSTOMS_RESULT` 均为客户作用域且最多 50MB,异步导出类型 `LOADING_LIST_BUY_DECLARATION_ITEMS` 由 Firstleg Provider 按固定十三列供数 | | 订舱文件 | 通用内部业务文件入口,契约 `src/main/resources/openapi/ocean-booking-file-v1.yaml`;类型固定 `OCEAN_BOOKING_DOCUMENT`、`tenantId=0`(平台专属,客户可选快照不改变文件归属,拒绝客户作用域)、`businessId=oceanBookingId`,任意格式且最多 50MiB,不得绑定拖车;`BOOKING_CONFIRMATION/BILL_OF_LADING` 文档类型及有效状态由 Firstleg 保存 | | 拖车预约附件 | 通用内部业务文件入口;固定 `tenantId=0`、`businessType=TRUCK_APPOINTMENT_ATTACHMENT`、`businessId=appointmentId`,只允许派车单、司机/车辆资料和其他平台附件;提单文件归订舱 `OCEAN_BOOKING_DOCUMENT`,不得复制为拖车附件 | | 存储与实体 | `service/LocalQualificationStorage.java`、`MeasurementImageStorage.java`、`entity/FileObjectEntity.java` | diff --git a/file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java b/file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java index 10c944048..42add2447 100644 --- a/file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java +++ b/file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java @@ -31,6 +31,7 @@ enum BusinessFileCategory { OVERSEAS_RELABEL_ITEM_DOCUMENT(true, FormatPolicy.PDF, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG), OVERSEAS_EXCEPTION_EVIDENCE(true, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG), LOADING_LIST_CUSTOMS_SOURCE(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), + LOADING_LIST_CUSTOMS_RECEIPT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), LOADING_LIST_CUSTOMS_RESULT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), ORDER_BATCH_IMPORT_SOURCE(true, FormatPolicy.XLSX, Limits.ORDER_IMPORT, BusinessIdPolicy.ORDER_BATCH_NO), ORDER_BATCH_IMPORT_ERROR_REPORT(true, FormatPolicy.XLSX, Limits.ORDER_IMPORT, BusinessIdPolicy.ORDER_BATCH_NO); diff --git a/file/src/main/resources/openapi/customs-file-and-export-v1.yaml b/file/src/main/resources/openapi/customs-file-and-export-v1.yaml index f7fa002a8..88e895f73 100644 --- a/file/src/main/resources/openapi/customs-file-and-export-v1.yaml +++ b/file/src/main/resources/openapi/customs-file-and-export-v1.yaml @@ -5,7 +5,7 @@ info: description: | File 只保存不可变文件对象并生成异步 XLSX,不保存报关状态或业务版本。 Firstleg 持有 loadingPlanId、文件版本、完成状态和可见性,并在调用本内部接口前完成 Platform/Client 鉴权。 - LOADING_LIST_CUSTOMS_SOURCE 与 LOADING_LIST_CUSTOMS_RESULT 均为 CUSTOMER owner scope,tenantId 必须大于 0; + LOADING_LIST_CUSTOMS_SOURCE、LOADING_LIST_CUSTOMS_RECEIPT 与 LOADING_LIST_CUSTOMS_RESULT 均为 CUSTOMER owner scope,tenantId 必须大于 0; 客户源文件上传/下载由 Firstleg 校验当前客户,结果文件下载还须由 Firstleg 校验整柜完成和结果可见性。 用户侧权限冻结为:Platform 上传结果 platform:firstleg:customs:manage,Platform 下载 platform:firstleg:customs:list; Client 上传源文件 merchant:order:edit,Client 下载自身源文件或获准结果 merchant:order:view。 @@ -15,7 +15,7 @@ paths: post: summary: 保存报关业务文件对象 description: | - 仅服务令牌可调用。fileCategory 取 LOADING_LIST_CUSTOMS_SOURCE 或 LOADING_LIST_CUSTOMS_RESULT。 + 仅服务令牌可调用。fileCategory 取 LOADING_LIST_CUSTOMS_SOURCE、LOADING_LIST_CUSTOMS_RECEIPT 或 LOADING_LIST_CUSTOMS_RESULT。 支持任意文件格式,单文件最多 50MB。operatorId 为已通过 Firstleg 鉴权的实际操作人。 parameters: - $ref: '#/components/parameters/InternalToken' @@ -94,7 +94,7 @@ components: InternalToken: {name: X-Internal-Service-Token, in: header, required: true, schema: {type: string}} OperatorId: {name: X-Operator-Id, in: header, required: true, schema: {type: integer, format: int64, minimum: 1}} TenantId: {name: tenantId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}, description: 客户ID;不得使用平台0作用域} - FileCategory: {name: fileCategory, in: path, required: true, schema: {type: string, enum: [LOADING_LIST_CUSTOMS_SOURCE, LOADING_LIST_CUSTOMS_RESULT]}} + FileCategory: {name: fileCategory, in: path, required: true, schema: {type: string, enum: [LOADING_LIST_CUSTOMS_SOURCE, LOADING_LIST_CUSTOMS_RECEIPT, LOADING_LIST_CUSTOMS_RESULT]}} LoadingPlanId: {name: loadingPlanId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}} FileId: {name: fileId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}} schemas: diff --git a/file/src/test/java/com/baiyue/file/config/CustomsFileExportContractTest.java b/file/src/test/java/com/baiyue/file/config/CustomsFileExportContractTest.java index 1252a954f..29fef5e9e 100644 --- a/file/src/test/java/com/baiyue/file/config/CustomsFileExportContractTest.java +++ b/file/src/test/java/com/baiyue/file/config/CustomsFileExportContractTest.java @@ -29,7 +29,7 @@ class CustomsFileExportContractTest { String source = Files.readString(repositoryRoot() .resolve("file/src/main/resources/openapi/customs-file-and-export-v1.yaml")); - assertThat(source).contains("LOADING_LIST_CUSTOMS_SOURCE", "LOADING_LIST_CUSTOMS_RESULT") + assertThat(source).contains("LOADING_LIST_CUSTOMS_SOURCE", "LOADING_LIST_CUSTOMS_RECEIPT", "LOADING_LIST_CUSTOMS_RESULT") .contains("maximum: 52428800", "no-store", "CUSTOMER owner scope", "loadingPlanId") .contains("platform:firstleg:customs:manage", "platform:firstleg:customs:list", "merchant:order:edit", "merchant:order:view") diff --git a/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java b/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java index 3f1c1fe34..d0d9790b1 100644 --- a/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java +++ b/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java @@ -169,14 +169,17 @@ class InternalBusinessFileServiceTest { }); var source = service.upload(10001L, "LOADING_LIST_CUSTOMS_SOURCE", "51", 7L, file); + var receipt = service.upload(10001L, "LOADING_LIST_CUSTOMS_RECEIPT", "51", 7L, file); var result = service.upload(10001L, "LOADING_LIST_CUSTOMS_RESULT", "51", 7L, file); assertThat(source).extracting(InternalBusinessFileService.Metadata::tenantId, InternalBusinessFileService.Metadata::businessType, InternalBusinessFileService.Metadata::businessId) .containsExactly(10001L, "LOADING_LIST_CUSTOMS_SOURCE", "51"); + assertThat(receipt.businessType()).isEqualTo("LOADING_LIST_CUSTOMS_RECEIPT"); assertThat(result.businessType()).isEqualTo("LOADING_LIST_CUSTOMS_RESULT"); verify(storage).store(10001L, "51", file, BusinessFileCategory.LOADING_LIST_CUSTOMS_SOURCE); + verify(storage).store(10001L, "51", file, BusinessFileCategory.LOADING_LIST_CUSTOMS_RECEIPT); verify(storage).store(10001L, "51", file, BusinessFileCategory.LOADING_LIST_CUSTOMS_RESULT); } diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index 8f2193cab..3178c1d80 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -168,6 +168,7 @@ export async function run(context) { | 入仓自动头程费用、派生详情、人工发布与 MANUAL 其他费用版本 | `inbound-fees.mjs`(前置:`merchant-qualification-fixture,orders,inbound-measurement`) | `inbound-fees-contract.test.mjs` | | 国内货柜履约模拟柜关联、柜次概览、渠道隔离、装柜模型桥接、PDA、动态增量、国内报关证据、出仓及离仓后目的港资料 | `phase3-container-loading.mjs`、`ocean-booking-fixture.mjs`、`phase3-container-overview.mjs`、`phase3-loading.mjs`、`phase3-loading-model-bridge.mjs`、`phase3-pda-convergence.mjs`、`phase3-evidence-gate.mjs`、`phase3-loading-finalize.mjs`、`phase3-customs.mjs`、`phase3-outbound-dispatch.mjs`、`phase3-container-documents.mjs` | `container-simulation-association-contract.test.mjs`、`container-specification-fixture-contract.test.mjs`、`ocean-booking-channel-contract.test.mjs`、`phase3-container-regression-contract.test.mjs`、`phase3-loading-model-bridge-contract.test.mjs`、`phase3-loading-dynamic-final-contract.test.mjs`、`phase3-container-documents-contract.test.mjs`、`phase3-phase4-departure-pickup-chain-contract.test.mjs`、`phase3-producer-chain-contract.test.mjs`、`manual-state-loading-pda-contract.test.mjs` | | 头程整柜报关 | `phase3-container-customs-fixture.mjs`、`phase3-container-customs.mjs`;定向入口通过正式订单、测量、同柜装载和最终完成 API 生成独立 `BUY_DECLARATION + TAX_REFUND` 混合柜,客户资料只覆盖退税票,买单行由下载中心 XLSX 验证;不复用历史柜、不执行 SQL | `container-customs-acceptance-contract.test.mjs`、`container-customs-acceptance-matrix.mjs` | +| 国内装柜验收作业员选择 | `domestic-pda-operator.mjs`;按本次 PDA 登录 Token 的字符串 subject 匹配目标仓库候选 userId,缺少匹配即停止,不使用首位候选或成员编号 | `domestic-pda-operator-contract.test.mjs` | | 分柜与父子入库单数量守恒 | `phase3-order-splitting.mjs` | `phase3-order-splitting-contract.test.mjs` | | 同柜且不可拆组 | `coload-groups.mjs`(可选;前置正式生成至少7票同线路 `WAIT_SPLIT`,另造不同线路反向票;覆盖创建/取消幂等、原子分配/移除、目标组隔离超容拒绝与零漂移、推送防绕过、PDA短拣完成拒绝且无余量子单、足量整组消费、普通单拆分不回归及已有父子单建组拒绝) | `coload-groups-contract.test.mjs` | | 人工计划分柜准备与只读核验 | `manual-order-splitting-fixture.mjs`、`manual-order-splitting-verify.mjs`(可选,不进入默认全量) | `manual-order-splitting-contract.test.mjs` | diff --git a/scripts/admin-test-env-modules/domestic-pda-operator-contract.test.mjs b/scripts/admin-test-env-modules/domestic-pda-operator-contract.test.mjs new file mode 100644 index 000000000..a5a416d1d --- /dev/null +++ b/scripts/admin-test-env-modules/domestic-pda-operator-contract.test.mjs @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { selectSessionPdaOperator } from './domestic-pda-operator.mjs' + +const subject = '2103886047128588290' +const token = `header.${Buffer.from(JSON.stringify({ sub: subject })).toString('base64url')}.signature` +test('task assignee matches authenticated user even when another operator is first', () => { + const current = { userId: subject, membershipId: '2103886047128588291' } + const operators = [{ userId: '194000000000000101' }, current] + assert.equal(selectSessionPdaOperator(operators, token), current) + assert.equal(selectSessionPdaOperator({ records: operators }, token), current) +}) +test('missing warehouse candidate does not fall back to another operator or membership ID', () => { + assert.throws(() => selectSessionPdaOperator([{ userId: '2103886047128588291' }], token), /不在目标仓库候选/) + assert.throws(() => selectSessionPdaOperator([], ''), /有效登录身份/) +}) diff --git a/scripts/admin-test-env-modules/domestic-pda-operator.mjs b/scripts/admin-test-env-modules/domestic-pda-operator.mjs new file mode 100644 index 000000000..1b7ecb345 --- /dev/null +++ b/scripts/admin-test-env-modules/domestic-pda-operator.mjs @@ -0,0 +1,14 @@ +// Use the subject of the authenticated test session, never the first candidate +// or IAM membership ID. Server-side warehouse and assignment checks remain authoritative. +export function selectSessionPdaOperator(operators, token) { + let subject + try { subject = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')).sub } + catch { throw new Error('国内 PDA 验收缺少有效登录身份') } + if (typeof subject !== 'string' || !/^[1-9]\d*$/.test(subject)) { + throw new Error('国内 PDA 登录身份必须包含字符串用户编号') + } + const rows = Array.isArray(operators) ? operators : operators?.records || [] + const operator = rows.find((item) => String(item.userId) === subject) + if (!operator) throw new Error('当前登录 PDA 作业员不在目标仓库候选中,停止分配任务') + return operator +} diff --git a/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs b/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs index 314b60a0d..e4734a05f 100644 --- a/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs +++ b/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs @@ -30,7 +30,7 @@ test('retains all clickable container, loading-list and PDA workflow states thro assert.doesNotMatch(source, /push-preload/) assert.match(source, /计划确认前不生成 PDA 任务/) assert.match(source, /summary\?\.readiness\?\.canConfirm === true/) - assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion \}\)/) + assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion, assigneeId: id\(pdaOperator.userId\) \}\)/) assert.match(source, /confirmationStatus === 'CONFIRMED'/) assert.match(source, /readiness\?\.readyForPda === true/) assert.match(source, /id\(task\?\.id\) !== confirmedTaskId/) diff --git a/scripts/admin-test-env-modules/manual-state-loading-pda.mjs b/scripts/admin-test-env-modules/manual-state-loading-pda.mjs index 858ece9f2..cc72bc3e2 100644 --- a/scripts/admin-test-env-modules/manual-state-loading-pda.mjs +++ b/scripts/admin-test-env-modules/manual-state-loading-pda.mjs @@ -1,3 +1,4 @@ +import { selectSessionPdaOperator } from './domestic-pda-operator.mjs' const MODULE = 'manual-state-loading-pda' const OPTIONS = { scenario: 'manual-loading-pda-state-matrix', deploymentBlocker: true } const CONTAINERS = '/api/platform/v1/containers' @@ -216,7 +217,7 @@ async function createPlanTask(context, label) { const pdaOperatorsResponse = await context.request('GET', `/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${encodeURIComponent(source.warehouseId)}`) const pdaOperators = context.apiSuccess(MODULE, `${label} 查询国内头程 PDA 作业员候选`, pdaOperatorsResponse, '', OPTIONS) - const pdaOperator = (Array.isArray(pdaOperators) ? pdaOperators : pdaOperators?.records || [])[0] + const pdaOperator = selectSessionPdaOperator(pdaOperators, context.pdaToken) context.assert(MODULE, `${label} 存在可用 PDA 作业员`, pdaOperatorsResponse, Boolean(pdaOperator?.userId), `operatorId=${id(pdaOperator?.userId) || 'missing'}`, OPTIONS) if (!pdaOperator?.userId) throw new Error(`${label} 缺少可用 PDA 作业员`) 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 8b3bda756..51785dbd0 100644 --- a/scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs +++ b/scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs @@ -83,7 +83,7 @@ function routes(context, merchantToken) { undefined, customer.customerToken || merchantToken, { 'Idempotency-Key': idempotencyKey }), merchantConfirmReceipt: (ctx, fixture, customer, request, idempotencyKey) => ctx.request('POST', `${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/receipt/${request.confirm === false ? 'reject' : 'confirm'}`, - { expectedVersion: Number(request.expectedVersion), remark: request.remark || '' }, + { expectedVersion: Number(request.expectedVersion), confirm: request.confirm !== false, remark: request.remark || '' }, customer.customerToken || merchantToken, { 'Idempotency-Key': idempotencyKey }), platformStart: (ctx, fixture, request, idempotencyKey) => ctx.request('POST', `${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/start`, request, ctx.token, diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index aaee70b9f..4d91841ed 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -1,3 +1,4 @@ +import { selectSessionPdaOperator } from './domestic-pda-operator.mjs' import { publishPlatformLogisticsChannels, resolveOceanBookingAgent } from './ocean-booking-fixture.mjs' import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs' import { containerFieldGaps, loadingListFieldGaps, preloadFieldGaps } from './fulfillment-list-field-contract.mjs' @@ -563,7 +564,7 @@ export async function run(context) { const pdaOperatorsResponse = await context.request('GET', `/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${encodeURIComponent(warehouseId)}`) const pdaOperators = context.apiSuccess(MODULE, '查询当前国内头程 PDA 作业员候选', pdaOperatorsResponse, '', OPTIONS) - const pdaOperator = (Array.isArray(pdaOperators) ? pdaOperators : records(pdaOperators))[0] + const pdaOperator = selectSessionPdaOperator(pdaOperators, context.pdaToken) context.assert(MODULE, '存在可用于计划确认的国内头程 PDA 作业员', pdaOperatorsResponse, Boolean(pdaOperator?.userId), `operatorId=${id(pdaOperator?.userId) || 'missing'}`, OPTIONS) if (!pdaOperator?.userId) return diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index 93e61f0ba..024401f5c 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -1,3 +1,4 @@ +import { selectSessionPdaOperator } from './domestic-pda-operator.mjs' import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs' import { isPureStatusNoop, visibleLoadingHistories } from './loading-history-noop.mjs' import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs' @@ -672,7 +673,7 @@ export async function run(context) { const pdaOperatorsResponse = await context.request('GET', `/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${encodeURIComponent(warehouseId)}`) const pdaOperators = context.apiSuccess(MODULE, '查询当前国内头程 PDA 作业员候选', pdaOperatorsResponse, '', OPTIONS) - const pdaOperator = (Array.isArray(pdaOperators) ? pdaOperators : pdaOperators?.records || [])[0] + const pdaOperator = selectSessionPdaOperator(pdaOperators, context.pdaToken) context.assert(MODULE, '存在可用于计划确认的国内头程 PDA 作业员', pdaOperatorsResponse, Boolean(pdaOperator?.userId), `operatorId=${id(pdaOperator?.userId) || 'missing'}`, OPTIONS) if (!pdaOperator?.userId) return diff --git a/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs b/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs index 7858d015b..b160be16c 100644 --- a/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs @@ -52,7 +52,7 @@ test('PDA opens only after linking a released requirement and uses stable plan i assert.match(source, /if \(!transportStable\) return null/) assert.match(source, /计划确认前不生成 PDA 任务/) assert.match(source, /summary\?\.readiness\?\.canConfirm === true/) - assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion \}\)/) + assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion, assigneeId: id\(pdaOperator.userId\) \}\)/) assert.match(source, /confirmationStatus === 'CONFIRMED'/) assert.match(source, /readiness\?\.readyForPda === true/) assert.match(source, /taskId === confirmedTaskId/) diff --git a/scripts/admin-test-env-modules/phase3-order-splitting.mjs b/scripts/admin-test-env-modules/phase3-order-splitting.mjs index fd6228711..2e774186b 100644 --- a/scripts/admin-test-env-modules/phase3-order-splitting.mjs +++ b/scripts/admin-test-env-modules/phase3-order-splitting.mjs @@ -1,3 +1,4 @@ +import { selectSessionPdaOperator } from './domestic-pda-operator.mjs' const MODULE = 'phase3-order-splitting' const SCENARIO = 'source-loaded-single-remainder-chain' const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true } @@ -339,7 +340,7 @@ async function loadOneFragment(context, state) { const pdaOperatorsResponse = await context.request('GET', `/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${encodeURIComponent(warehouseId)}`) const pdaOperators = context.apiSuccess(MODULE, `查询 ${sourceOrderNo} 国内头程 PDA 作业员候选`, pdaOperatorsResponse, '', OPTIONS) - const pdaOperator = (Array.isArray(pdaOperators) ? pdaOperators : pdaOperators?.records || [])[0] + const pdaOperator = selectSessionPdaOperator(pdaOperators, context.pdaToken) context.assert(MODULE, `${sourceOrderNo} 存在可用 PDA 作业员`, pdaOperatorsResponse, Boolean(pdaOperator?.userId), `operatorId=${id(pdaOperator?.userId) || 'missing'}`, OPTIONS) if (!pdaOperator?.userId) return null