From d51e054def793cea868bed2b65ace38a7e1a114f Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 17 Aug 2026 10:54:08 +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 --- deploy/sql/头程数据库统一增量-MySQL8.sql | 4 +- .../service/CartonMeasurementService.java | 3 +- ...BookingTruckPermissionSqlContractTest.java | 2 + .../service/CartonMeasurementServiceTest.java | 19 + scripts/README.md | 11 +- ...te-pending-review-orders-contract.test.mjs | 52 +++ scripts/create-pending-review-orders.mjs | 364 ++++++++++++++++++ scripts/local-db-full-acceptance.ps1 | 2 +- 8 files changed, 452 insertions(+), 5 deletions(-) create mode 100644 scripts/create-pending-review-orders-contract.test.mjs create mode 100644 scripts/create-pending-review-orders.mjs diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index c21d16675..a00cc5ba8 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -9160,7 +9160,7 @@ BEGIN END IF; IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND id BETWEEN 5102140 AND 5102149 - AND permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 9 + AND permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 6 OR (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND id BETWEEN 5102150 AND 5102154 AND permission_code LIKE 'platform:firstleg:truck-appointment:%') <> 4 THEN @@ -9171,7 +9171,7 @@ BEGIN JOIN baiyue_iam.iam_role r ON r.id=rp.role_id AND r.tenant_id=rp.tenant_id JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE r.tenant_id=0 AND r.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR') - AND p.permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 18 + AND p.permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 12 OR (SELECT COUNT(DISTINCT CONCAT(r.role_code, '|', p.permission_code)) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_role r ON r.id=rp.role_id AND r.tenant_id=rp.tenant_id diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/CartonMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/CartonMeasurementService.java index b82ca598c..6fc197536 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/CartonMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/CartonMeasurementService.java @@ -33,7 +33,8 @@ import java.util.Set; @Service public class CartonMeasurementService { private static final Logger log = LoggerFactory.getLogger(CartonMeasurementService.class); - private static final Set DEVICE_WRITABLE_STATUSES = Set.of("WAIT_MEASURE", "MEASURE_REJECTED"); + private static final Set DEVICE_WRITABLE_STATUSES = Set.of( + "WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED"); private static final Set MANUAL_COUNT_EDITABLE_STATUSES = Set.of( "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED"); private static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3BookingTruckPermissionSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3BookingTruckPermissionSqlContractTest.java index 0dda3408e..2fdc699c7 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3BookingTruckPermissionSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3BookingTruckPermissionSqlContractTest.java @@ -73,6 +73,8 @@ class Phase3BookingTruckPermissionSqlContractTest { .contains("(5102149,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:allocate'") .contains("(5102150,0,'PLATFORM','MENU',5102000,'platform:firstleg:truck-appointment:list'") .contains("(5102154,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:operate'") + .contains("permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 6") + .contains("p.permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 12") .contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')") .doesNotContain("assert_phase3_booking_truck_permission_ids_available") .doesNotContain("/trucking"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java index 3ab81dcce..d2d85a258 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java @@ -114,6 +114,25 @@ class CartonMeasurementServiceTest { assertThat(measurement.getSourceCode()).isEqualTo("PLATFORM"); } + @Test + void measuringInboundAllowsManualEntryBeforeMeasurementSubmission() { + InboundOrderEntity measuring = inbound("MEASURING"); + InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0); + when(repository.findInbound(10001L, 70001L)).thenReturn(measuring); + when(repository.lockInbound(10001L, 70001L)).thenReturn(measuring); + when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(null); + when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement); + when(repository.listItems(10001L, 30001L)).thenAnswer(invocation -> List.of(insertedItem.get())); + when(repository.updateMeasurement(measurement)).thenReturn(true); + + CartonMeasurementResult result = service.upsertManualForInbound( + 10001L, 90001L, 70001L, acceptanceCommand()); + + assertThat(result.created()).isTrue(); + assertThat(measurement.getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_MANUAL); + verify(images).upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any()); + } + @Test void repeatedScanReplacesDeviceValuesButPreservesManualCountAndOldImageIsDeleted() { InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 4); diff --git a/scripts/README.md b/scripts/README.md index 0601af421..ec7368ecd 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -6,12 +6,21 @@ |---|---|---|---| | 平台后台全模块 HTTP 验收 | `scripts/admin-test-env-acceptance.mjs` | 会创建、更新或最终停用测试数据 | `--modules` 默认列表中的平台客户、仓储、订单、入库、分柜、装柜、船期、拖车、报关、财务、通知、统计等模块 | | 客户工作台完整 HTTP 链路验收 | `scripts/client-test-env-acceptance.mjs` | 会创建测试企业、订单、入库、权限和站内消息数据 | 注册/审核、客户隔离、订单、入库计费、文件、财务、客户侧权限,以及我的消息未读/已读/全部已读链路 | +| 固定客户批量创建待审核订单 | `scripts/create-pending-review-orders.mjs` | 只使用固定客户账号创建指定数量订单 | 登录 `17512095155`,选择真实可计价仓库/渠道,创建并提交订单,停在待审核,不创建客户、不审核订单 | | Client 二期专项 HTTP 验收 | `scripts/client-phase2-test-env-acceptance.mjs` | 会写入测试环境业务数据 | 二期客户入库、测量、费用和权限专项场景;不是替代 Client 全量入口 | 执行前必须确认目标环境、账号和数据清理/停用策略,并获得当前对话的测试环境验收授权。`admin-test-env-modules/*contract.test.mjs` 只验证模块脚本结构,不能代替 HTTP 验收。 推荐顺序:部署服务 → Admin 全模块 HTTP 验收 → Client 完整 HTTP 链路验收 → 前端真实页面调用和样式验收 → PDA 页面/设备验收。任何本地单测通过都不能替代 HTTP 全量验收。 +## 固定客户待审核订单 + +需要为人工审核准备固定数量的订单时,使用 `create-pending-review-orders.mjs`。可直接在文件顶部 `CONFIG` 中填写 `GATEWAY`、`USERNAME`、`PASSWORD` 和 `COUNT`;固定账号为 `17512095155`。脚本不会创建客户,也不会调用平台审核、入库或后续履约接口。数量支持 `1-100`,每张订单创建后立即提交并回查客户端状态为 `SUBMITTED`,报告统一记录为 `PENDING_REVIEW`。 + +```powershell +node scripts/create-pending-review-orders.mjs +``` + ## 本地质量检查(非全量验收) 以下命令只用于代码质量和契约回归,不访问测试环境,不代表服务全量可用: @@ -95,7 +104,7 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令 ### 本地 MySQL 全量验收 -正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `635AB29A0ED99A5EA5108C1216F474E5782A9E10ABF6159DFBEDE4C0FEA30897` 和 15215 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。 +正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `1DBA47099AE501B21C4BEC22A50D065571BA46264C15C6C543CBE6F1CC597463` 和 15215 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。 ```powershell .\scripts\local-db-full-acceptance.ps1 -DryRun diff --git a/scripts/create-pending-review-orders-contract.test.mjs b/scripts/create-pending-review-orders-contract.test.mjs new file mode 100644 index 000000000..2d8b413f1 --- /dev/null +++ b/scripts/create-pending-review-orders-contract.test.mjs @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +import { buildOrderPayload, parseArguments, validateCount } from './create-pending-review-orders.mjs' + +const source = readFileSync(new URL('./create-pending-review-orders.mjs', import.meta.url), 'utf8') + +test('uses the fixed merchant account and stops at pending review', () => { + assert.ok(source.includes("USERNAME: '17512095155'"), 'fixed username is missing') + assert.ok(/PASSWORD:\s*'[^']*'/.test(source), 'CONFIG.PASSWORD is missing') + assert.ok(/COUNT:\s*\d+/.test(source), 'CONFIG.COUNT is missing') + assert.match(source, /\/api\/merchant\/v1\/orders\/first-leg\/\$\{encodeURIComponent\(orderId\)\}\/submit/) + assert.match(source, /detail\?\.order\?\.status !== 'SUBMITTED'/) + assert.match(source, /\/api\/merchant\/v1\/orders\/\$\{encodeURIComponent\(orderId\)\}/) + assert.match(source, /unifiedOrder\?\.summary\?\.mainStatus !== 'PENDING_REVIEW'/) + assert.match(source, /mainStatus: unifiedOrder\.summary\.mainStatus/) + assert.doesNotMatch(source, /partners\/candidates\?type=SHIPPER/) + assert.doesNotMatch(source, /\/api\/platform\/v1\/iam\/tenants/) + assert.doesNotMatch(source, /\/approve/) +}) + +test('supports a bounded fixed order count', () => { + assert.equal(validateCount('1'), 1) + assert.equal(validateCount('10'), 10) + assert.throws(() => validateCount('0'), /between 1 and 100/) + assert.throws(() => validateCount('101'), /between 1 and 100/) + assert.deepEqual(parseArguments(['--count', '5', '--gateway=http://localhost:9000']), { + count: '5', gateway: 'http://localhost:9000', + }) +}) + +test('builds an order payload owned by the fixed profile', () => { + const payload = buildOrderPayload({ + marker: 'PENDING-TEST', + index: 2, + profile: { id: '44', tenantName: '固定测试客户' }, + master: { + receiving: { id: '11', warehouseName: '国内仓' }, + destination: { id: '22', warehouseName: 'ONT8', warehouseCode: 'ONT8', countryCode: 'US' }, + channel: { channelCode: 'SEA-US', channelName: '美西海运' }, + pricingTaxMode: 'TAX_INCLUDED', + }, + productImageFileId: '33', + }) + assert.equal(payload.receivingWarehouseId, '11') + assert.equal(payload.destinationWarehouseId, '22') + assert.equal(payload.customerId, '44') + assert.equal(payload.customerName, '固定测试客户') + assert.equal(payload.cargoes.length, 1) + assert.equal(payload.cargoes[0].productImageFileId, '33') +}) diff --git a/scripts/create-pending-review-orders.mjs b/scripts/create-pending-review-orders.mjs new file mode 100644 index 000000000..029338e7e --- /dev/null +++ b/scripts/create-pending-review-orders.mjs @@ -0,0 +1,364 @@ +#!/usr/bin/env node + +import { randomUUID } from 'node:crypto' +import { mkdir, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +const MAX_COUNT = 100 + +// Local test-environment settings. Keep PASSWORD empty in committed code. +const CONFIG = { + GATEWAY: 'http://8.163.30.128:9000', + USERNAME: '17512095155', + PASSWORD: 'd6KW2ZC4vv44WJ4a', + COUNT: 10, +} + +export function parseArguments(values) { + const options = {} + for (let index = 0; index < values.length; index += 1) { + const value = values[index] + if (!value.startsWith('--')) throw new Error(`Unknown argument: ${value}`) + const [rawKey, inlineValue] = value.slice(2).split('=', 2) + if (rawKey === 'help') { + options.help = true + continue + } + const next = inlineValue ?? values[index + 1] + if (inlineValue === undefined) index += 1 + if (next == null || next.startsWith('--')) throw new Error(`--${rawKey} requires a value`) + options[rawKey] = next + } + return options +} + +export function validateCount(value) { + const count = Number(value ?? CONFIG.COUNT) + if (!Number.isInteger(count) || count < 1 || count > MAX_COUNT) { + throw new Error(`--count must be an integer between 1 and ${MAX_COUNT}`) + } + return count +} + +function trimSlash(value) { + return String(value || '').replace(/\/+$/, '') +} + +function compactTimestamp(value = new Date()) { + return value.toISOString().replace(/\D/g, '').slice(0, 14) +} + +function addDays(value, days) { + const date = new Date(value) + date.setUTCDate(date.getUTCDate() + days) + return date.toISOString().slice(0, 10) +} + +function records(value) { + return Array.isArray(value?.records) ? value.records : [] +} + +function items(value) { + return Array.isArray(value) ? value : [] +} + +function requireSuccess(response, action) { + if (response.http !== 200 || String(response.body?.code) !== '200') { + throw new Error(`${action} failed: http=${response.http}, code=${response.body?.code ?? 'missing'}, message=${response.body?.message ?? 'missing'}, requestId=${response.requestId || 'missing'}, traceId=${response.traceId || 'missing'}`) + } + return response.body?.data +} + +async function request(gateway, timeoutMs, method, path, { token, body, headers = {} } = {}) { + const requestHeaders = { Accept: 'application/json', ...headers } + if (token) requestHeaders.Authorization = `Bearer ${token}` + let requestBody + if (body instanceof FormData) { + requestBody = body + } else if (body !== undefined) { + requestHeaders['Content-Type'] = 'application/json' + requestBody = JSON.stringify(body) + } + const response = await fetch(`${gateway}${path}`, { + method, + headers: requestHeaders, + body: requestBody, + signal: AbortSignal.timeout(timeoutMs), + }) + const text = await response.text() + let responseBody = null + try { + responseBody = text ? JSON.parse(text) : null + } catch { + responseBody = { code: 'INVALID_JSON', message: text.slice(0, 500) } + } + return { + http: response.status, + body: responseBody, + requestId: response.headers.get('x-request-id') || responseBody?.requestId, + traceId: response.headers.get('x-trace-id') || responseBody?.traceId, + } +} + +async function loadMasterData(gateway, timeoutMs, token) { + const domesticResponse = await request(gateway, timeoutMs, 'GET', + '/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC', { token }) + const domestic = records(requireSuccess(domesticResponse, 'Query active domestic warehouses')) + const destinationResponse = await request(gateway, timeoutMs, 'GET', + '/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=FBA', { token }) + const destinations = records(requireSuccess(destinationResponse, 'Query active FBA warehouses')) + if (domestic.length === 0 || destinations.length === 0) { + throw new Error('No active domestic receiving warehouse or FBA destination warehouse is available') + } + + for (const receiving of domestic) { + const channelResponse = await request(gateway, timeoutMs, 'GET', + `/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`, { token }) + const channels = items(requireSuccess(channelResponse, 'Query active pricing channels')) + .filter((channel) => channel?.channelCode && items(channel.pricingTaxModes).length > 0) + for (const channel of channels) { + for (const pricingTaxMode of channel.pricingTaxModes) { + for (const destination of destinations) { + const estimateResponse = await request(gateway, timeoutMs, 'POST', + '/api/merchant/v1/first-leg/pricing/estimate', { + token, + body: { + originWarehouseId: receiving.id, + channelCode: channel.channelCode, + destinationCode: destination.warehouseCode, + destinationType: 'FBA', + destinationWarehouseId: destination.id, + destinationName: destination.warehouseName, + pricingTaxMode, + actualWeightKg: 20.5, + actualVolumeCbm: 0.024, + }, + }) + if (estimateResponse.http === 200 && String(estimateResponse.body?.code) === '200') { + return { receiving, destination, channel, pricingTaxMode } + } + } + } + } + } + throw new Error('No priced route is available for the fixed merchant account') +} + +function testPng() { + return Uint8Array.from(Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', + 'base64')) +} + +async function uploadProductImage(gateway, timeoutMs, token, marker) { + const form = new FormData() + form.append('file', new Blob([testPng()], { type: 'image/png' }), `pending-order-${marker}.png`) + const response = await request(gateway, timeoutMs, 'POST', + '/api/merchant/v1/files/product-images', { token, body: form }) + const data = requireSuccess(response, 'Upload product image') + if (!data?.fileId) throw new Error('Product image upload did not return fileId') + return data.fileId +} + +export function buildOrderPayload({ marker, index, profile, master, productImageFileId }) { + const sequence = String(index).padStart(3, '0') + const shipmentId = `SHIP-${sequence}-${marker}`.slice(0, 64) + const referenceId = `REF-${sequence}-${marker}`.slice(0, 64) + const companyName = profile.tenantName || `固定客户-${marker}` + return { + customerId: profile.id, + customerName: companyName, + transportMode: 'SEA', + tradeTerm: 'FOB', + destinationCountryCode: master.destination.countryCode || 'US', + destinationWarehouseId: master.destination.id, + destinationWarehouseName: master.destination.warehouseName, + expectedShipDate: addDays(new Date(), 30), + customerRemark: `待审核订单 ${sequence} ${marker}`, + pickupMethod: 'PICKUP', + receivingWarehouseId: master.receiving.id, + receivingWarehouseName: master.receiving.warehouseName, + channelCode: master.channel.channelCode, + channelName: master.channel.channelName, + carrierId: null, + carrierName: null, + taxMode: 'BUY_DECLARATION', + pricingTaxMode: master.pricingTaxMode, + containerMode: 'LCL', + customsInspection: false, + destinationType: 'FBA', + amazonWarehouseCode: master.destination.warehouseCode, + shipmentId, + referenceId, + sender: { + companyName, + contactName: '订单测试联系人', + phone: '13800138000', + email: 'pending-order@example.com', + countryCode: 'CN', + province: '广东省', + city: '深圳市', + district: '南山区', + addressLine: `${marker} 固定客户测试地址`, + postalCode: '518000', + }, + receiver: null, + cargoes: [{ + skuNo: `SKU-${sequence}-${marker}`.slice(0, 64), + cargoName: '待审核测试产品', + cargoNameEn: 'Pending review test product', + hsCode: '8518210000', + customsHsCode: '8518210000', + packageType: 'CARTON', + lengthCm: 30, + widthCm: 20, + heightCm: 20, + unitsPerPackage: 12, + packageCount: 2, + grossWeightKg: 20.5, + volumeCbm: 0.024, + totalQuantity: 24, + netWeightKg: 18, + unitDeclaredValue: 8.5, + declaredValue: 204, + currency: 'USD', + material: '塑料 / Plastic', + usage: '接口测试 / API testing', + model: `MODEL-${sequence}`, + brand: 'TEST-NO-LOGO', + productLengthCm: 12, + productWidthCm: 8, + productHeightCm: 6, + shipmentId, + referenceId, + upcNo: `${Date.now()}${sequence}`.slice(-12), + productImageFileId, + dangerousGoods: false, + cargoRemark: `待审核订单货物 ${sequence} ${marker}`, + }], + } +} + +function printHelp() { + console.log(`Usage: + node scripts/create-pending-review-orders.mjs --gateway --count <1-${MAX_COUNT}> + +Environment: + CLIENT_TEST_GATEWAY Test environment Gateway URL + MERCHANT_TEST_PASSWORD Password for the configured fixed account + MERCHANT_TEST_USERNAME Optional username override + PENDING_ORDER_REPORT Optional JSON report path + +The GATEWAY, USERNAME, PASSWORD, and COUNT fields can also be filled in CONFIG at the top of this file.`) +} + +export async function main(argv = process.argv.slice(2)) { + const args = parseArguments(argv) + if (args.help) { + printHelp() + return 0 + } + const gateway = trimSlash(args.gateway ?? process.env.CLIENT_TEST_GATEWAY ?? CONFIG.GATEWAY) + if (!gateway) throw new Error('Gateway is required via CONFIG.GATEWAY, --gateway, or CLIENT_TEST_GATEWAY') + const count = validateCount(args.count) + const username = args.username ?? process.env.MERCHANT_TEST_USERNAME ?? CONFIG.USERNAME + const password = args.password ?? process.env.MERCHANT_TEST_PASSWORD ?? CONFIG.PASSWORD + if (!username) throw new Error('Fixed merchant username is required in CONFIG.USERNAME') + if (!password) throw new Error('Merchant password is required in CONFIG.PASSWORD or MERCHANT_TEST_PASSWORD') + const timeoutMs = Number(args['timeout-ms'] ?? process.env.CLIENT_TEST_TIMEOUT_MS ?? 30_000) + if (!Number.isFinite(timeoutMs) || timeoutMs < 1_000) throw new Error('--timeout-ms must be at least 1000') + + const marker = `PENDING-${compactTimestamp()}-${randomUUID().slice(0, 8)}` + const outputPath = resolve(args.output ?? process.env.PENDING_ORDER_REPORT + ?? `reports/pending-review-orders-${marker}.json`) + const startedAt = new Date().toISOString() + const orders = [] + + const loginResponse = await request(gateway, timeoutMs, 'POST', '/api/auth/login', { + body: { username, password }, + }) + const login = requireSuccess(loginResponse, 'Merchant login') + if (!login?.accessToken) throw new Error('Merchant login did not return accessToken') + const token = login.accessToken + + const profileResponse = await request(gateway, timeoutMs, 'GET', + '/api/merchant/v1/iam/tenant/profile', { token }) + const profile = requireSuccess(profileResponse, 'Query merchant profile') + if (!profile?.id || profile.status !== 'ACTIVE') { + throw new Error(`Fixed merchant account is not ready: tenantId=${profile?.id ?? 'missing'}, status=${profile?.status ?? 'missing'}`) + } + + const master = await loadMasterData(gateway, timeoutMs, token) + const productImageFileId = await uploadProductImage(gateway, timeoutMs, token, marker) + + for (let index = 1; index <= count; index += 1) { + const payload = buildOrderPayload({ marker, index, profile, master, productImageFileId }) + const createResponse = await request(gateway, timeoutMs, 'POST', + '/api/merchant/v1/orders/first-leg', { + token, + body: payload, + headers: { 'Idempotency-Key': `${marker}-create-${index}`.slice(0, 128) }, + }) + const orderId = String(requireSuccess(createResponse, `Create order ${index}`) ?? '') + if (!orderId) throw new Error(`Create order ${index} did not return orderId`) + + const submitResponse = await request(gateway, timeoutMs, 'POST', + `/api/merchant/v1/orders/first-leg/${encodeURIComponent(orderId)}/submit`, { + token, + headers: { 'Idempotency-Key': `${marker}-submit-${index}`.slice(0, 128) }, + }) + requireSuccess(submitResponse, `Submit order ${index}`) + + const detailResponse = await request(gateway, timeoutMs, 'GET', + `/api/merchant/v1/orders/first-leg/${encodeURIComponent(orderId)}`, { token }) + const detail = requireSuccess(detailResponse, `Verify order ${index}`) + if (detail?.order?.status !== 'SUBMITTED') { + throw new Error(`Order ${orderId} is not pending review: status=${detail?.order?.status ?? 'missing'}`) + } + const orderNo = String(detail.order.orderNo || '') + if (!orderNo) throw new Error(`Order ${orderId} detail did not return orderNo`) + const unifiedResponse = await request(gateway, timeoutMs, 'GET', + `/api/merchant/v1/orders/${encodeURIComponent(orderId)}`, { token }) + const unifiedOrder = requireSuccess(unifiedResponse, `Verify unified order ${index}`) + if (unifiedOrder?.summary?.mainStatus !== 'PENDING_REVIEW') { + throw new Error(`Order ${orderId} unified status is not pending review: mainStatus=${unifiedOrder?.summary?.mainStatus ?? 'missing'}`) + } + const result = { + index, + orderId, + orderNo, + clientStatus: detail.order.status, + mainStatus: unifiedOrder.summary.mainStatus, + } + orders.push(result) + console.log(`CREATED ${index}/${count} orderId=${result.orderId} orderNo=${result.orderNo} status=PENDING_REVIEW`) + } + + const report = { + reportName: '固定客户待审核订单批量生成', + success: true, + marker, + username, + tenantId: String(profile.id), + requestedCount: count, + createdCount: orders.length, + startedAt, + completedAt: new Date().toISOString(), + productImageFileId: String(productImageFileId), + orders, + securityNote: '报告不保存密码或访问令牌。', + } + await mkdir(dirname(outputPath), { recursive: true }) + await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8') + console.log(`REPORT=${outputPath}`) + console.log(`RESULT=PASS created=${orders.length}`) + return 0 +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + main().then((code) => { process.exitCode = code }).catch((error) => { + console.error(error instanceof Error ? error.message : String(error)) + process.exitCode = 1 + }) +} diff --git a/scripts/local-db-full-acceptance.ps1 b/scripts/local-db-full-acceptance.ps1 index a6066f3a5..553f8830f 100644 --- a/scripts/local-db-full-acceptance.ps1 +++ b/scripts/local-db-full-acceptance.ps1 @@ -11,7 +11,7 @@ $ErrorActionPreference = 'Stop' $Repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $Workspace = (Resolve-Path (Join-Path $Repo '..')).Path $Acceptance = Join-Path $Workspace '.codex-tmp\p23-04-acceptance' -$ExpectedSha256 = '635AB29A0ED99A5EA5108C1216F474E5782A9E10ABF6159DFBEDE4C0FEA30897' +$ExpectedSha256 = '1DBA47099AE501B21C4BEC22A50D065571BA46264C15C6C543CBE6F1CC597463' $ExpectedLines = 15215 if ($TargetHost -notin @('127.0.0.1', 'localhost')) {