贴标申请,仓储转运
This commit is contained in:
+10
-1
@@ -1,5 +1,9 @@
|
||||
# 后端脚本管理与全量 HTTP 验收索引
|
||||
|
||||
`db/relabel-wait-status-incremental.sql`:存量换标主单状态修正,显式使用 `baiyue_lastleg`。由操作者选择已授权数据库连接执行,无内置凭据;只更新未生成入库单、仍为待分配的 `RELABEL_APPLICATION` 主单为待换标/待排车/待出库,不修改库存或删除单据。事务执行、可重复运行,执行后按换标号回查订单状态;无单独报告文件。
|
||||
|
||||
`db/warehouse-transfer-outbound-incremental.sql`:固定指向 `baiyue_lastleg`,一次补齐仓储转运审核、提货、PDA 测量打托、现场照片、换标、FBA/私人仓目的地、入库计划、智能排车和派送所需表、租户字段及索引,并将换标新 SKU 和转运托盘生命周期字段修正为本地要求的可空结构。无内置凭据;表使用 `CREATE TABLE IF NOT EXISTS`,字段和索引通过 `information_schema` 独立判断,可重复执行,不删除或改写业务单据。完整成功时返回 `WAREHOUSE_TRANSFER_LATEST_SCHEMA_READY`;若历史数据存在同客户重复托盘号,则返回 `WAREHOUSE_TRANSFER_SCHEMA_APPLIED_WITH_INDEX_BLOCKER`,且 `inbound_pallet_unique_index` 为 `BLOCKED_BY_DUPLICATE_CUSTOMER_PALLET_NO`。其余结构仍正常补齐,脚本不会擅自修改历史托盘号。
|
||||
|
||||
`mixed-container-outbound-test-env.mjs` 是经明确授权的测试环境留存混柜出库续跑入口:参数为 `source-report containerNo MIXED-FBA|MIXED-DROPSHIP output-report`,凭据由显式 `node --env-file` 注入。FBA 分支实时校验既有收货数据,创建排车、出库、派送及临时 PDA 账号,执行拣货、发车、测试签收文件保存和国内状态回查;一件代发分支只走专用候选、两托四箱出库、PDA 幂等拣货和完成回查,并明确记录派送未覆盖,不返回全流程成功退出码。finally 停用临时账号,不删除业务数据。输出报告作为检查点,失败后须先核对其中业务单号和实时状态,再用同一输出文件续跑;不得删除报告后盲目重试。它不代表普通海外仓目的地转换或所有分支签收验收通过。
|
||||
|
||||
全量验收只认测试环境 HTTP 脚本。它们通过 Gateway 调用已部署服务,按业务模块执行真实登录、权限、状态、幂等、跨服务和数据链路检查;脚本通过后,剩余工作是 Admin/Client 的真实接口调用与浏览器样式验收,以及 PDA 页面/设备验收。
|
||||
@@ -71,6 +75,7 @@ Client 全量、Client 入库测量与计费、待审核订单和批量待分柜
|
||||
| 测试环境 HTTP | `client-phase2-test-env-acceptance.mjs` | Client 入库、测量、费用和权限专项;写测试数据 |
|
||||
| 测试环境 HTTP | `create-pending-review-orders.mjs` | 固定客户批量创建并提交待审核订单,不做平台审核和后续履约 |
|
||||
| 测试环境 HTTP | `it-test-wait-split-bulk.mjs` | 固定“IT测试”与“义乌仓”,渠道等随机,逐票推进到 `WAIT_SPLIT`;失败继续并落报告。只复用 `TP-001`/`YW-TP-002`,不新增托盘或库位,库位不足使用公共位 `A0` |
|
||||
| 本地真实 API | `relabel-closure-local-acceptance.mjs` | 仅调用本机海外 Gateway/Print/File/Lastleg,创建合成换标单并验证重复下单、三类 PDF、PDA 换标、拆托/合托、返工和换标出库;会写入本地业务数据,不代表现场照片或财务验收 |
|
||||
| 测试环境 Excel 造数 | `real-order-xlsx-test-env.mjs` | 从外部源 XLSX 按入仓码归并,严格匹配客户可用渠道、税制、目的仓;`preflight` 只核对,`create` 创建草稿,`pending-review` 批量提交后停在待审核且不使用托盘/库位,`wait-split` 只供少量完整测量验收。参数、限制及重跑说明见 `real-order-xlsx-import.md` |
|
||||
| 测试环境 HTTP | `dimension-ocr-pricing-test-env.ps1` | 尺寸、OCR 和渠道计价专项,上传本地图片并写测试业务数据;包含历史默认参数,只能显式传入当前目标参数后使用 |
|
||||
| 测试环境 HTTP | `export-center-test-env-acceptance.ps1` | 导出中心测试环境验收,会创建导出任务和相关测试数据 |
|
||||
@@ -276,6 +281,10 @@ Dry-run 只校验两个统一 SQL 的入口标记,打印当前 SHA-256、行
|
||||
|
||||
## 海外换标定向验收补充
|
||||
|
||||
- `scripts/current-session-overseas-relabel-acceptance.mjs`:本地开发环境定向入口,从当前 Codex 内置浏览器本地存储读取最新平台/客户 refresh token,在内存中刷新双区域会话,自动选择当前客户具备足量库存的海外仓并临时创建海外 PDA 操作员;随后串行运行换标拆/合托、智能预排、PDA 出库与派送完成。报告写入 `reports/current-session-overseas-relabel/`,不记录 Token/密码;会消耗当前浏览器 refresh token 并保留业务验收单据,临时 PDA 在 `finally` 中停用。只允许本机开发环境且须有当前轮真实写入授权。
|
||||
- `db/seed-local-relabel-ready-orders.sql`:仅供明确授权的本机测试库存准备。调用者在同一 MySQL 会话提供客户、共享海外仓、有效虚拟库位、FBA目的仓、唯一批次及ID基数;凭据由本机进程环境提供。以事务插入三单共54箱、六托及原入仓快照、计划、明细、余额、仓位和明确标记 LOCAL_TEST_FIXTURE 的库存流水;不伪造PDA照片、签收操作或HTTP验收通过,不修改原数据。重复批次失败并回滚,不用upsert重置已消耗库存;成功数据保留,结果表输出到调用者日志,不自动清理。
|
||||
|
||||
- `scripts/current-session-overseas-relabel-acceptance.mjs`:本地开发环境定向入口,从当前 Codex 内置浏览器本地存储读取最新平台/客户 refresh token,在内存中刷新双区域会话,自动选择当前客户具备足量库存的海外仓并临时创建海外 PDA 操作员;随后串行运行换标拆/合托、智能预排、PDA 出库与派送完成。报告写入 `reports/current-session-overseas-relabel/`,不记录 Token/密码;会消耗当前浏览器 refresh token 并保留业务验收单据,临时 PDA 在 `finally` 中停用。履约阶段中断后可显式设置 `OVERSEAS_RESUME_FULFILLMENT=true`,复用目录内已通过的 `relabel.json` 且只续跑出库派送。只允许本机开发环境且须有当前轮真实写入授权。
|
||||
- `scripts/overseas-relabel-real-acceptance.mjs`:定向真实 HTTP 换标作业,会创建换标、库存及排车记录;从显式环境文件读取平台/PDA账号、网关及客户参数,输出脚本配置的报告,不自动清理。失败保留证据与已创建资源,重跑前核对报告,必须另获指定环境授权。
|
||||
- 该脚本现按“扫码→组托→完工→客户确认”执行,`OVERSEAS_RELABEL_CHARGE_AMOUNT=0` 表示省略费用字段、只验仓库链路(不代表财务通过);默认 0.01 验钱包冻结。照片为有效的合成测试 PNG,不是真实现场照片。
|
||||
- `scripts/relabel-pdf-local-acceptance.mjs`:仅本机 Print→File 真实 HTTP 验收,显式提供 `INTERNAL_SERVICE_TOKEN`、`OVERSEAS_TEST_CUSTOMER_ID`,可配置 `PRINT_SERVICE_URL`、`FILE_SERVICE_URL`。创建三类测试 PDF 文件元数据,验证下载哈希及跨客户拒绝,报告 `reports/relabel-closure-20260916/pdf.json`;保留文件,不清理,失败非零退出,可重跑生成新 businessId。不代表换标下单/PDA全流程通过。
|
||||
- `scripts/overseas-relabel-fulfillment-acceptance.mjs`:读取 `OVERSEAS_ACCEPTANCE_SOURCE` 或指定排车/换标编号,执行 PDA 拣货、出库、派送后续真实 HTTP,会推进业务状态;凭据来自显式环境文件,报告默认 `reports/overseas-relabel-fulfillment-acceptance.json`(`OVERSEAS_FULFILLMENT_OUTPUT` 可覆盖)。失败保留资源,重跑前核对业务状态,不自动清理。两个定向脚本不替代 Admin 全量入口,本次合并仅做离线合同。
|
||||
|
||||
@@ -9,7 +9,7 @@ const domesticSchemas = ['baiyue_auth', 'baiyue_iam', 'baiyue_order', 'baiyue_wa
|
||||
const overseasSchemas = ['baiyue_event_bridge_overseas', 'baiyue_lastleg', 'baiyue_lastlag_warehouse']
|
||||
|
||||
test('两个入口分别只操作国内九库与海外三库,包括限定名引用', () => {
|
||||
for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 208], [overseas, overseasSchemas, domesticSchemas, 136]]) {
|
||||
for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 208], [overseas, overseasSchemas, domesticSchemas, 138]]) {
|
||||
assert.deepEqual([...sql.matchAll(/^CREATE DATABASE IF NOT EXISTS (\w+)/gm)].map(m => m[1]), allowed)
|
||||
assert.deepEqual([...new Set([...sql.matchAll(/^USE (\w+);/gm)].map(m => m[1]))], allowed)
|
||||
const body = sql.replace(/^--.*$/gm, '')
|
||||
@@ -28,7 +28,7 @@ test('纯初始化禁止迁移、数据回填、清理、重复建表和提前
|
||||
assert.doesNotMatch(body.replace(/^CREATE TRIGGER [\s\S]*?\$\$/gm, ''), /^\s*(?:UPDATE|DELETE FROM|REPLACE INTO)\b/gmi)
|
||||
assert.doesNotMatch(body, /CREATE TABLE IF NOT EXISTS/i)
|
||||
assert.equal((body.match(/AS initialization_status/g) ?? []).length, 1)
|
||||
assert.match(body.trim(), /SELECT 'PASS' AS initialization_status, '(CN|OVERSEAS)' AS region, (208|136) AS initialized_tables;$/)
|
||||
assert.match(body.trim(), /SELECT 'PASS' AS initialization_status, '(CN|OVERSEAS)' AS region, (208|138) AS initialized_tables;$/)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -113,6 +113,43 @@ async function createPda(adminToken, warehouseId, onCreated) {
|
||||
throw new Error('Temporary PDA security projection did not converge')
|
||||
}
|
||||
|
||||
async function merchantSessionForTenant(adminToken, tenantId) {
|
||||
const ticket = await api(domestic, adminToken, 'POST',
|
||||
`/api/platform/v1/iam/tenants/${tenantId}/impersonation`, { reason: `${marker} 海外换标真实验收` })
|
||||
return api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
|
||||
}
|
||||
|
||||
async function findRelabelScope(adminToken, preferredMerchant = null) {
|
||||
const sessions = []
|
||||
if (preferredMerchant?.accessToken && preferredMerchant?.overseasAccessToken) sessions.push(preferredMerchant)
|
||||
if (!sessions.length) {
|
||||
const tenants = await api(domestic, adminToken, 'GET', '/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE')
|
||||
for (const tenant of tenants.records || []) {
|
||||
try { sessions.push(await merchantSessionForTenant(adminToken, id(tenant.id))) } catch {}
|
||||
}
|
||||
}
|
||||
for (const session of sessions) {
|
||||
const customerId = jwtId(session.overseasAccessToken, 'tenant_id', 'tenantId')
|
||||
if (!customerId) continue
|
||||
let warehouses
|
||||
try { warehouses = await api(domestic, session.accessToken, 'GET', '/api/merchant/v1/warehouses?warehouseType=OVERSEAS&page=1&size=200') }
|
||||
catch { continue }
|
||||
for (const warehouse of warehouses.records || []) {
|
||||
if (warehouse.status !== 'ACTIVE') continue
|
||||
let candidates
|
||||
try { candidates = await api(overseas, session.overseasAccessToken, 'GET', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${encodeURIComponent(id(warehouse.id))}`) }
|
||||
catch { continue }
|
||||
if (candidates.some((item) => String(item.inboundOrderNo || '').startsWith('DS-')
|
||||
&& Number(item.availablePackages) >= 5 && item.destinationFbaWarehouseCode
|
||||
&& item.positions?.some((position) => position.palletNo && position.palletNo !== 'UNBOUND'
|
||||
&& Number(position.availablePackages) >= 5))) {
|
||||
return { merchant: session, customerId, warehouse }
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('No active customer has a destination warehouse and five cartons on a physical source pallet')
|
||||
}
|
||||
|
||||
await mkdir(outputDir, { recursive: true })
|
||||
let admin
|
||||
let pda
|
||||
@@ -124,29 +161,22 @@ try {
|
||||
}
|
||||
const storedMerchantToken = storedJwt('cc-client-overseas-access-token')
|
||||
const storedCustomerId = jwtId(storedMerchantToken, 'tenant_id', 'tenantId')
|
||||
let merchant
|
||||
let preferredMerchant = null
|
||||
try {
|
||||
merchant = await refreshStoredSession('cc-client-refresh-token', 'merchant')
|
||||
preferredMerchant = await refreshStoredSession('cc-client-refresh-token', 'merchant')
|
||||
} catch {
|
||||
assert(storedCustomerId, 'Current Codex merchant session does not contain a customer id')
|
||||
const ticket = await api(domestic, admin.accessToken, 'POST',
|
||||
`/api/platform/v1/iam/tenants/${storedCustomerId}/impersonation`, { reason: `${marker} 当前客户海外API验收` })
|
||||
merchant = await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
|
||||
if (storedCustomerId) preferredMerchant = await merchantSessionForTenant(admin.accessToken, storedCustomerId)
|
||||
}
|
||||
assert(admin.accessToken && admin.overseasAccessToken && merchant.accessToken && merchant.overseasAccessToken, 'Refresh response did not contain both regional tokens')
|
||||
const customerId = jwtId(merchant.overseasAccessToken, 'tenant_id', 'tenantId')
|
||||
assert(customerId, 'Current merchant customer id is missing from the overseas token')
|
||||
const warehouses = await api(domestic, merchant.accessToken, 'GET', '/api/merchant/v1/warehouses?warehouseType=OVERSEAS&page=1&size=200')
|
||||
let selected = null
|
||||
for (const warehouse of warehouses.records || []) {
|
||||
if (warehouse.status !== 'ACTIVE') continue
|
||||
const candidates = await api(overseas, merchant.overseasAccessToken, 'GET', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${encodeURIComponent(id(warehouse.id))}`)
|
||||
if (candidates.some((item) => String(item.inboundOrderNo || '').startsWith('DS-') && Number(item.availablePackages) >= 5 && item.destinationFbaWarehouseCode && item.positions?.some((position) => Number(position.availablePackages) >= 5))) {
|
||||
selected = warehouse; break
|
||||
}
|
||||
}
|
||||
assert(selected, 'Current merchant has no active overseas warehouse with a five-carton DS inventory source')
|
||||
await disableStalePdas(admin.accessToken)
|
||||
assert(admin.accessToken && admin.overseasAccessToken, 'Platform refresh response did not contain both regional tokens')
|
||||
const resume = process.env.OVERSEAS_RESUME_FULFILLMENT === 'true'
|
||||
if (resume && (!process.env.OVERSEAS_TEST_CUSTOMER_ID || !process.env.OVERSEAS_TEST_WAREHOUSE_ID)) throw new Error('Resume requires explicit customer and warehouse from the saved run')
|
||||
const scope = resume ? {
|
||||
merchant: await merchantSessionForTenant(admin.accessToken, process.env.OVERSEAS_TEST_CUSTOMER_ID),
|
||||
customerId: process.env.OVERSEAS_TEST_CUSTOMER_ID,
|
||||
warehouse: { id: process.env.OVERSEAS_TEST_WAREHOUSE_ID, warehouseCode: process.env.OVERSEAS_TEST_WAREHOUSE_CODE || null },
|
||||
} : await findRelabelScope(admin.accessToken, preferredMerchant)
|
||||
const { merchant, customerId, warehouse: selected } = scope
|
||||
// Do not disable accounts from another acceptance run; cleanup owns only this run's account.
|
||||
pda = await createPda(admin.accessToken, id(selected.id), (created) => { pda = created })
|
||||
|
||||
const relabelReport = join(outputDir, 'relabel.json')
|
||||
@@ -156,7 +186,10 @@ try {
|
||||
PDA_OVERSEAS_ACCESS_TOKEN: pda.token, OVERSEAS_TEST_CUSTOMER_ID: customerId,
|
||||
OVERSEAS_TEST_WAREHOUSE_ID: id(selected.id), OVERSEAS_ACCEPTANCE_OUTPUT: relabelReport,
|
||||
OVERSEAS_ACCEPTANCE_SOURCE: relabelReport, OVERSEAS_FULFILLMENT_OUTPUT: fulfillmentReport }
|
||||
for (const script of ['overseas-relabel-real-acceptance.mjs', 'overseas-relabel-fulfillment-acceptance.mjs']) {
|
||||
const scripts = process.env.OVERSEAS_CLOSURE_EXTRA === 'true' ? ['relabel-closure-local-acceptance.mjs'] : process.env.OVERSEAS_RESUME_FULFILLMENT === 'true'
|
||||
? ['overseas-relabel-fulfillment-acceptance.mjs']
|
||||
: ['overseas-relabel-real-acceptance.mjs', 'overseas-relabel-fulfillment-acceptance.mjs']
|
||||
for (const script of scripts) {
|
||||
const result = spawnSync(process.execPath, [resolve('scripts', script)], { cwd: resolve('.'), env: childEnv, stdio: 'inherit' })
|
||||
if (result.error) throw result.error
|
||||
if (result.status !== 0) throw new Error(`${script} failed with exit code ${result.status}`)
|
||||
|
||||
@@ -17,6 +17,33 @@ BEGIN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='缺少基础表 ll_relabel_item,请先执行海外尾程基线';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order') THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='pickup_contact_name') THEN
|
||||
ALTER TABLE ll_warehouse_transfer_order ADD COLUMN pickup_contact_name VARCHAR(128) NULL COMMENT '提货联系人' AFTER delivery_address;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='pickup_contact_phone') THEN
|
||||
ALTER TABLE ll_warehouse_transfer_order ADD COLUMN pickup_contact_phone VARCHAR(64) NULL COMMENT '提货联系电话' AFTER pickup_contact_name;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_measurement_photo (
|
||||
id BIGINT NOT NULL COMMENT '测量照片引用ID', transfer_order_id BIGINT NOT NULL COMMENT '仓储转运单ID',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', file_id BIGINT NOT NULL COMMENT '文件服务文件ID',
|
||||
file_name VARCHAR(255) NULL COMMENT '文件名快照', content_type VARCHAR(128) NULL COMMENT '文件媒体类型快照',
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数快照', uploaded_by VARCHAR(128) NULL COMMENT 'PDA上传操作人',
|
||||
uploaded_at DATETIME(3) NOT NULL COMMENT '上传时间', submitted_at DATETIME(3) NULL COMMENT '随测量打托提交时间', PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_transfer_measurement_photo_file(customer_id,transfer_order_id,file_id),
|
||||
KEY idx_ll_transfer_measurement_photo_order(customer_id,transfer_order_id,uploaded_at)
|
||||
) ENGINE=InnoDB COMMENT='仓储转运PDA测量现场照片引用';
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_measurement_photo' AND column_name='submitted_at') THEN
|
||||
ALTER TABLE ll_warehouse_transfer_measurement_photo ADD COLUMN submitted_at DATETIME(3) NULL COMMENT '随测量打托提交时间' AFTER uploaded_at;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='ll_outbound_order')
|
||||
AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='request_hash') THEN
|
||||
ALTER TABLE ll_outbound_order ADD COLUMN request_hash CHAR(64) NULL COMMENT '一件代发创建请求摘要,其他出库类型为空' AFTER recipient_info;
|
||||
END IF;
|
||||
|
||||
-- 贴标申请:FBA目的仓、换标模式、SKU组合明细及原库存快照。
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='destination_fba_warehouse_code') THEN
|
||||
ALTER TABLE ll_relabel_item ADD COLUMN destination_fba_warehouse_code VARCHAR(128) NULL COMMENT 'FBA目的仓编码(与海外操作仓分离)';
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
USE baiyue_lastleg;
|
||||
|
||||
SET @ddl = (SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_warehouse_transfer_pallet'
|
||||
AND column_name = 'draft_idempotency_key'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE ll_warehouse_transfer_pallet
|
||||
MODIFY COLUMN warehouse_pallet_id BIGINT NULL COMMENT ''生成后的海外仓真实托盘ID'',
|
||||
MODIFY COLUMN pallet_no VARCHAR(128) NULL COMMENT ''生成后的真实托盘码'',
|
||||
MODIFY COLUMN warehouse_location_id BIGINT NULL COMMENT ''生成后的真实库位ID'',
|
||||
MODIFY COLUMN warehouse_resource_lock_id BIGINT NULL COMMENT ''生成后的仓储资源锁ID'',
|
||||
MODIFY COLUMN completed_at DATETIME(3) NULL COMMENT ''整单打托完成时间'',
|
||||
ADD COLUMN draft_idempotency_key VARCHAR(255) NULL COMMENT ''草稿保存幂等键'' AFTER status,
|
||||
ADD COLUMN draft_request_hash CHAR(64) NULL COMMENT ''草稿请求摘要'' AFTER draft_idempotency_key,
|
||||
ADD COLUMN generate_idempotency_key VARCHAR(255) NULL COMMENT ''新托盘生成幂等键'' AFTER draft_request_hash,
|
||||
ADD COLUMN bind_idempotency_key VARCHAR(255) NULL COMMENT ''扫码绑定幂等键'' AFTER generate_idempotency_key,
|
||||
ADD COLUMN bound_at DATETIME(3) NULL COMMENT ''扫码确认绑定时间'' AFTER bind_idempotency_key'
|
||||
));
|
||||
PREPARE transfer_pallet_lifecycle_stmt FROM @ddl;
|
||||
EXECUTE transfer_pallet_lifecycle_stmt;
|
||||
DEALLOCATE PREPARE transfer_pallet_lifecycle_stmt;
|
||||
@@ -1173,7 +1173,7 @@ CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_order (
|
||||
transfer_no VARCHAR(64) NOT NULL COMMENT '转仓单号', inbound_order_id BIGINT NOT NULL COMMENT '入仓单ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓单号快照',
|
||||
lastleg_order_id BIGINT NOT NULL COMMENT '尾程订单ID', lastleg_order_no VARCHAR(32) NOT NULL COMMENT '尾程订单号',
|
||||
operating_warehouse_id VARCHAR(128) NOT NULL COMMENT '入库中转仓ID', fba_warehouse_code VARCHAR(128) NULL COMMENT '可选FBA目的仓编码',
|
||||
delivery_address VARCHAR(1000) NOT NULL COMMENT '提货地址或中转仓收货地址', package_count INT NOT NULL COMMENT '箱数', delivery_method VARCHAR(64) NOT NULL COMMENT '入仓方式',
|
||||
delivery_address VARCHAR(1000) NOT NULL COMMENT '提货地址或中转仓收货地址', pickup_contact_name VARCHAR(128) NULL COMMENT '提货联系人', pickup_contact_phone VARCHAR(64) NULL COMMENT '提货联系电话', package_count INT NOT NULL COMMENT '箱数', delivery_method VARCHAR(64) NOT NULL COMMENT '入仓方式',
|
||||
estimated_arrival_at DATETIME(3) NOT NULL COMMENT '预计入仓时间', fba_shipment_no VARCHAR(128) NULL COMMENT '可选FBA货件编号',
|
||||
tracking_no VARCHAR(255) NULL COMMENT '物流跟踪号', fba_box_nos VARCHAR(2000) NULL COMMENT 'FBA箱号集合', remark VARCHAR(1000) NULL COMMENT '备注',
|
||||
need_relabel TINYINT NOT NULL DEFAULT 0 COMMENT '是否需要换标', relabel_application_id BIGINT NULL COMMENT '换标申请单ID', relabel_no VARCHAR(64) NULL COMMENT '换标申请单号',
|
||||
@@ -3963,6 +3963,8 @@ CREATE TABLE IF NOT EXISTS wh_pallet_batch_item (
|
||||
USE baiyue_lastleg;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='lx_codes'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN lx_codes VARCHAR(1000) NULL COMMENT ''关联一件代发领星入库单代码汇总'' AFTER outbound_type'));
|
||||
PREPARE ll_outbound_lx_codes_stmt FROM @ddl; EXECUTE ll_outbound_lx_codes_stmt; DEALLOCATE PREPARE ll_outbound_lx_codes_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='request_hash'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN request_hash CHAR(64) NULL COMMENT ''一件代发创建请求摘要,其他出库类型为空'' AFTER recipient_info'));
|
||||
PREPARE ll_outbound_request_hash_stmt FROM @ddl; EXECUTE ll_outbound_request_hash_stmt; DEALLOCATE PREPARE ll_outbound_request_hash_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_order' AND column_name='lx_code'), 'SELECT 1', 'ALTER TABLE ll_inbound_order ADD COLUMN lx_code VARCHAR(128) NULL COMMENT ''一件代发领星入库单代码'''));
|
||||
PREPARE ll_inbound_lx_code_stmt FROM @ddl; EXECUTE ll_inbound_lx_code_stmt; DEALLOCATE PREPARE ll_inbound_lx_code_stmt;
|
||||
|
||||
@@ -4201,14 +4203,16 @@ PREPARE return_order_lastleg_no_stmt FROM @ddl; EXECUTE return_order_lastleg_no_
|
||||
USE baiyue_lastleg;
|
||||
CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_pallet (
|
||||
id BIGINT NOT NULL COMMENT '打托事实ID', transfer_order_id BIGINT NOT NULL COMMENT '转运单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
sequence_no INT NOT NULL COMMENT '托序号', warehouse_pallet_id BIGINT NOT NULL COMMENT '海外仓真实托盘ID', pallet_no VARCHAR(128) NOT NULL COMMENT '真实托盘码',
|
||||
warehouse_location_id BIGINT NOT NULL COMMENT '真实库位ID', warehouse_location_code VARCHAR(128) NULL COMMENT '真实库位码', warehouse_resource_lock_id BIGINT NOT NULL COMMENT '仓储资源锁ID',
|
||||
sequence_no INT NOT NULL COMMENT '托序号', warehouse_pallet_id BIGINT NULL COMMENT '生成后的海外仓真实托盘ID', pallet_no VARCHAR(128) NULL COMMENT '生成后的真实托盘码',
|
||||
warehouse_location_id BIGINT NULL COMMENT '生成后的真实库位ID', warehouse_location_code VARCHAR(128) NULL COMMENT '真实库位码', warehouse_resource_lock_id BIGINT NULL COMMENT '生成后的仓储资源锁ID',
|
||||
carton_count INT NOT NULL COMMENT '本托实收箱数', length_cm DECIMAL(12,2) NOT NULL COMMENT '实测长cm', width_cm DECIMAL(12,2) NOT NULL COMMENT '实测宽cm',
|
||||
height_cm DECIMAL(12,2) NOT NULL COMMENT '实测高cm', gross_weight_kg DECIMAL(12,3) NOT NULL COMMENT '实测毛重kg', volume_cbm DECIMAL(18,6) NOT NULL COMMENT '实测体积CBM',
|
||||
status VARCHAR(32) NOT NULL COMMENT '打托状态', completed_at DATETIME(3) NOT NULL COMMENT 'PDA打托完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
status VARCHAR(32) NOT NULL COMMENT '逐托状态DRAFT/GENERATED/BOUND/COMPLETED', draft_idempotency_key VARCHAR(255) NULL COMMENT '草稿保存幂等键', draft_request_hash CHAR(64) NULL COMMENT '草稿请求摘要', generate_idempotency_key VARCHAR(255) NULL COMMENT '新托盘生成幂等键', bind_idempotency_key VARCHAR(255) NULL COMMENT '扫码绑定幂等键', bound_at DATETIME(3) NULL COMMENT '扫码确认绑定时间', completed_at DATETIME(3) NULL COMMENT '整单打托完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_transfer_pallet_seq(transfer_order_id,sequence_no), UNIQUE KEY uk_ll_transfer_pallet_physical(customer_id,warehouse_pallet_id), KEY idx_ll_transfer_pallet_order(customer_id,transfer_order_id,status)
|
||||
) ENGINE=InnoDB COMMENT='仓储转运PDA实测打托事实';
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_pallet' AND column_name='draft_idempotency_key'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_pallet MODIFY COLUMN warehouse_pallet_id BIGINT NULL COMMENT ''生成后的海外仓真实托盘ID'', MODIFY COLUMN pallet_no VARCHAR(128) NULL COMMENT ''生成后的真实托盘码'', MODIFY COLUMN warehouse_location_id BIGINT NULL COMMENT ''生成后的真实库位ID'', MODIFY COLUMN warehouse_resource_lock_id BIGINT NULL COMMENT ''生成后的仓储资源锁ID'', MODIFY COLUMN completed_at DATETIME(3) NULL COMMENT ''整单打托完成时间'', ADD COLUMN draft_idempotency_key VARCHAR(255) NULL COMMENT ''草稿保存幂等键'' AFTER status, ADD COLUMN draft_request_hash CHAR(64) NULL COMMENT ''草稿请求摘要'' AFTER draft_idempotency_key, ADD COLUMN generate_idempotency_key VARCHAR(255) NULL COMMENT ''新托盘生成幂等键'' AFTER draft_request_hash, ADD COLUMN bind_idempotency_key VARCHAR(255) NULL COMMENT ''扫码绑定幂等键'' AFTER generate_idempotency_key, ADD COLUMN bound_at DATETIME(3) NULL COMMENT ''扫码确认绑定时间'' AFTER bind_idempotency_key'));
|
||||
PREPARE transfer_pallet_lifecycle_stmt FROM @ddl; EXECUTE transfer_pallet_lifecycle_stmt; DEALLOCATE PREPARE transfer_pallet_lifecycle_stmt;
|
||||
CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_pallet_item (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', transfer_order_id BIGINT NOT NULL COMMENT '转运单ID', transfer_pallet_id BIGINT NOT NULL COMMENT '转运托盘事实ID',
|
||||
transfer_item_id BIGINT NOT NULL COMMENT '转运申报明细ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', package_count INT NOT NULL COMMENT '本托该SKU实收箱数',
|
||||
@@ -4220,3 +4224,80 @@ CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_relabel_binding (
|
||||
fba_carton_mark VARCHAR(255) NULL COMMENT '新FBA箱唛', destination_warehouse_code VARCHAR(128) NULL COMMENT 'FBA目的仓', remark VARCHAR(1000) NULL COMMENT '备注',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id), UNIQUE KEY uk_ll_transfer_relabel_mark(transfer_order_id,customer_carton_mark), KEY idx_ll_transfer_relabel_order(customer_id,transfer_order_id)
|
||||
) ENGINE=InnoDB COMMENT='仓储转运换标预绑定明细';
|
||||
|
||||
-- 2026-09-16 / 039 仓储转运提货联系人独立于换标联系人。
|
||||
USE baiyue_lastleg;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='pickup_contact_name'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN pickup_contact_name VARCHAR(128) NULL COMMENT ''提货联系人'' AFTER delivery_address'));
|
||||
PREPARE transfer_pickup_contact_name_stmt FROM @ddl; EXECUTE transfer_pickup_contact_name_stmt; DEALLOCATE PREPARE transfer_pickup_contact_name_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='pickup_contact_phone'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN pickup_contact_phone VARCHAR(64) NULL COMMENT ''提货联系电话'' AFTER pickup_contact_name'));
|
||||
PREPARE transfer_pickup_contact_phone_stmt FROM @ddl; EXECUTE transfer_pickup_contact_phone_stmt; DEALLOCATE PREPARE transfer_pickup_contact_phone_stmt;
|
||||
|
||||
-- 2026-09-16 / 040 仓储转运PDA测量现场照片引用。
|
||||
CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_measurement_photo (
|
||||
id BIGINT NOT NULL COMMENT '测量照片引用ID', transfer_order_id BIGINT NOT NULL COMMENT '仓储转运单ID',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', file_id BIGINT NOT NULL COMMENT '文件服务文件ID',
|
||||
file_name VARCHAR(255) NULL COMMENT '文件名快照', content_type VARCHAR(128) NULL COMMENT '文件媒体类型快照',
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数快照', uploaded_by VARCHAR(128) NULL COMMENT 'PDA上传操作人',
|
||||
uploaded_at DATETIME(3) NOT NULL COMMENT '上传时间', submitted_at DATETIME(3) NULL COMMENT '随测量打托提交时间', PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_transfer_measurement_photo_file(customer_id,transfer_order_id,file_id),
|
||||
KEY idx_ll_transfer_measurement_photo_order(customer_id,transfer_order_id,uploaded_at)
|
||||
) ENGINE=InnoDB COMMENT='仓储转运PDA测量现场照片引用';
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_measurement_photo' AND column_name='submitted_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_measurement_photo ADD COLUMN submitted_at DATETIME(3) NULL COMMENT ''随测量打托提交时间'' AFTER uploaded_at'));
|
||||
PREPARE transfer_measurement_photo_submitted_stmt FROM @ddl; EXECUTE transfer_measurement_photo_submitted_stmt; DEALLOCATE PREPARE transfer_measurement_photo_submitted_stmt;
|
||||
|
||||
-- 2026-09-16 / 041 仓储转运PDA实收箱数及历史完成状态回填。
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='received_package_count'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN received_package_count INT NULL COMMENT ''PDA实际入库箱数'' AFTER package_count'));
|
||||
PREPARE transfer_received_package_count_stmt FROM @ddl; EXECUTE transfer_received_package_count_stmt; DEALLOCATE PREPARE transfer_received_package_count_stmt;
|
||||
UPDATE ll_order o
|
||||
JOIN ll_warehouse_transfer_order t
|
||||
ON t.customer_id=o.customer_id AND t.lastleg_order_id=o.id AND t.transfer_no=o.source_order_no
|
||||
JOIN (
|
||||
SELECT customer_id,transfer_order_id,SUM(carton_count) AS received_package_count
|
||||
FROM ll_warehouse_transfer_pallet
|
||||
WHERE status='COMPLETED'
|
||||
GROUP BY customer_id,transfer_order_id
|
||||
) p ON p.customer_id=t.customer_id AND p.transfer_order_id=t.id
|
||||
SET o.received_package_count=p.received_package_count,
|
||||
o.current_status='INBOUND_COMPLETED',
|
||||
o.updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE o.order_type='WAREHOUSE_TRANSFER'
|
||||
AND t.status IN ('WAIT_CUSTOMER_CONFIRMATION','WAIT_FBA_DESTINATION','WAIT_RELABEL','READY_FOR_DISPATCH','ALLOCATED','OUTBOUNDED','PARTIALLY_RECEIVED','RECEIVED');
|
||||
UPDATE ll_inbound_order i
|
||||
JOIN ll_warehouse_transfer_order t
|
||||
ON t.customer_id=i.customer_id AND t.inbound_order_id=i.id AND t.inbound_code=t.transfer_no
|
||||
JOIN (
|
||||
SELECT customer_id,transfer_order_id,COUNT(*) AS pallet_count,SUM(carton_count) AS package_count,
|
||||
SUM(gross_weight_kg) AS gross_weight_kg,SUM(volume_cbm) AS volume_cbm
|
||||
FROM ll_warehouse_transfer_pallet
|
||||
WHERE status='COMPLETED'
|
||||
GROUP BY customer_id,transfer_order_id
|
||||
) p ON p.customer_id=t.customer_id AND p.transfer_order_id=t.id
|
||||
SET i.status='RECEIVED',i.package_count=p.package_count,i.pallet_count=p.pallet_count,
|
||||
i.gross_weight_kg=p.gross_weight_kg,i.volume_cbm=p.volume_cbm,i.updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE t.status IN ('WAIT_CUSTOMER_CONFIRMATION','WAIT_FBA_DESTINATION','WAIT_RELABEL','READY_FOR_DISPATCH','ALLOCATED','OUTBOUNDED','PARTIALLY_RECEIVED','RECEIVED');
|
||||
|
||||
-- 2026-09-16 / 042 仓储转运直接出库的FBA/私人仓目的地快照。
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='destination_type'), 'SELECT 1', 'ALTER TABLE ll_delivery_order ADD COLUMN destination_type VARCHAR(16) NULL COMMENT ''目的地类型:FBA或PRIVATE'' AFTER destination_warehouse_name, ADD COLUMN delivery_address VARCHAR(1000) NULL COMMENT ''私人仓派送地址'' AFTER destination_type, ADD COLUMN contact_name VARCHAR(128) NULL COMMENT ''私人仓联系人'' AFTER delivery_address, ADD COLUMN contact_phone VARCHAR(64) NULL COMMENT ''私人仓联系电话'' AFTER contact_name'));
|
||||
PREPARE transfer_outbound_destination_stmt FROM @ddl; EXECUTE transfer_outbound_destination_stmt; DEALLOCATE PREPARE transfer_outbound_destination_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='dispatch_order_id' AND is_nullable='YES'), 'SELECT 1', 'ALTER TABLE ll_delivery_order MODIFY COLUMN dispatch_order_id BIGINT NULL COMMENT ''关联派车单ID,仓储转运直接出库时为空'', MODIFY COLUMN dispatch_no VARCHAR(64) NULL COMMENT ''关联派车单号,仓储转运直接出库时为空'', MODIFY COLUMN destination_warehouse_code VARCHAR(128) NULL COMMENT ''本派送单目的FBA仓编码快照,私人仓为空'''));
|
||||
PREPARE transfer_outbound_nullable_stmt FROM @ddl; EXECUTE transfer_outbound_nullable_stmt; DEALLOCATE PREPARE transfer_outbound_nullable_stmt;
|
||||
|
||||
-- 2026-09-17 / 043 仓储转运目的地绑定及私人仓邮编快照。
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='destination_type'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN destination_type VARCHAR(16) NULL COMMENT ''目的类型:FBA或PRIVATE'' AFTER fba_warehouse_code, ADD COLUMN destination_warehouse_id VARCHAR(128) NULL COMMENT ''目的仓ID快照'' AFTER destination_type, ADD COLUMN destination_warehouse_code VARCHAR(128) NULL COMMENT ''目的仓编码快照'' AFTER destination_warehouse_id, ADD COLUMN destination_warehouse_name VARCHAR(255) NULL COMMENT ''目的仓名称快照'' AFTER destination_warehouse_code, ADD COLUMN private_destination_address VARCHAR(1000) NULL COMMENT ''私人仓完整地址'' AFTER destination_warehouse_name, ADD COLUMN private_destination_postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编'' AFTER private_destination_address, ADD COLUMN private_destination_contact_name VARCHAR(128) NULL COMMENT ''私人仓联系人'' AFTER private_destination_postal_code, ADD COLUMN private_destination_contact_phone VARCHAR(64) NULL COMMENT ''私人仓联系电话'' AFTER private_destination_contact_name, ADD COLUMN destination_binding_idempotency_key VARCHAR(255) NULL COMMENT ''目的地绑定幂等键'', ADD COLUMN destination_binding_request_hash CHAR(64) NULL COMMENT ''目的地绑定请求哈希'''));
|
||||
PREPARE transfer_destination_stmt FROM @ddl; EXECUTE transfer_destination_stmt; DEALLOCATE PREPARE transfer_destination_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='private_destination_postal_code'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN private_destination_postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编'' AFTER private_destination_address'));
|
||||
PREPARE transfer_postal_stmt FROM @ddl; EXECUTE transfer_postal_stmt; DEALLOCATE PREPARE transfer_postal_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan_line' AND column_name='delivery_address'), 'SELECT 1', 'ALTER TABLE ll_inbound_plan_line ADD COLUMN delivery_address VARCHAR(1000) NULL COMMENT ''私人仓目的地址快照'', ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编快照'', ADD COLUMN contact_name VARCHAR(128) NULL COMMENT ''私人仓联系人快照'', ADD COLUMN contact_phone VARCHAR(64) NULL COMMENT ''私人仓联系电话快照'''));
|
||||
PREPARE transfer_line_destination_stmt FROM @ddl; EXECUTE transfer_line_destination_stmt; DEALLOCATE PREPARE transfer_line_destination_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan_line' AND column_name='postal_code'), 'SELECT 1', 'ALTER TABLE ll_inbound_plan_line ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编快照'' AFTER delivery_address'));
|
||||
PREPARE transfer_line_postal_stmt FROM @ddl; EXECUTE transfer_line_postal_stmt; DEALLOCATE PREPARE transfer_line_postal_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan_pallet' AND column_name='destination_type'), 'SELECT 1', 'ALTER TABLE ll_inbound_plan_pallet ADD COLUMN destination_type VARCHAR(32) NULL COMMENT ''目的类型:FBA或PRIVATE'', ADD COLUMN delivery_address VARCHAR(1000) NULL COMMENT ''私人仓目的地址快照'', ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编快照'', ADD COLUMN contact_name VARCHAR(128) NULL COMMENT ''私人仓联系人快照'', ADD COLUMN contact_phone VARCHAR(64) NULL COMMENT ''私人仓联系电话快照'''));
|
||||
PREPARE transfer_pallet_destination_stmt FROM @ddl; EXECUTE transfer_pallet_destination_stmt; DEALLOCATE PREPARE transfer_pallet_destination_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan_pallet' AND column_name='postal_code'), 'SELECT 1', 'ALTER TABLE ll_inbound_plan_pallet ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编快照'' AFTER delivery_address'));
|
||||
PREPARE transfer_pallet_postal_stmt FROM @ddl; EXECUTE transfer_pallet_postal_stmt; DEALLOCATE PREPARE transfer_pallet_postal_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='destination_type'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN destination_type VARCHAR(32) NULL COMMENT ''目的类型:FBA或PRIVATE'' AFTER fba_warehouse_code, ADD COLUMN delivery_address VARCHAR(1000) NULL COMMENT ''私人仓目的地址快照'' AFTER destination_type, ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编快照'' AFTER delivery_address, ADD COLUMN contact_name VARCHAR(128) NULL COMMENT ''私人仓联系人快照'' AFTER postal_code, ADD COLUMN contact_phone VARCHAR(64) NULL COMMENT ''私人仓联系电话快照'' AFTER contact_name'));
|
||||
PREPARE dispatch_destination_stmt FROM @ddl; EXECUTE dispatch_destination_stmt; DEALLOCATE PREPARE dispatch_destination_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='postal_code'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编快照'' AFTER delivery_address'));
|
||||
PREPARE dispatch_postal_stmt FROM @ddl; EXECUTE dispatch_postal_stmt; DEALLOCATE PREPARE dispatch_postal_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='postal_code'), 'SELECT 1', 'ALTER TABLE ll_delivery_order ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编'' AFTER delivery_address'));
|
||||
PREPARE delivery_postal_stmt FROM @ddl; EXECUTE delivery_postal_stmt; DEALLOCATE PREPARE delivery_postal_stmt;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
USE baiyue_lastleg;
|
||||
|
||||
-- 当前文件服务数据库;不删除或重建文件元数据。
|
||||
ALTER TABLE baiyue_file.file_object MODIFY COLUMN business_type VARCHAR(64) NULL COMMENT '关联业务类型';
|
||||
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_id VARCHAR(128) NULL COMMENT ''箱唛总文件ID'' AFTER total_file_url'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_name VARCHAR(255) NULL COMMENT ''箱唛总文件名'' AFTER carton_total_file_id'));
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 存量换标尾程主单状态修正:不创建入库单,不增减库存;可重复执行。
|
||||
USE baiyue_lastleg;
|
||||
START TRANSACTION;
|
||||
UPDATE ll_order o
|
||||
JOIN ll_relabel_application a ON a.relabel_no=o.relabel_order_no AND a.customer_id=o.customer_id
|
||||
SET o.current_status=CASE
|
||||
WHEN a.status IN ('PENDING_PROCESSING','PROCESSING','CUSTOMER_CONFIRM_PENDING') THEN 'WAIT_RELABEL'
|
||||
WHEN a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' THEN 'WAIT_OUTBOUND'
|
||||
WHEN a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status IN ('WAIT_OUTBOUND_CREATION','OUTBOUND_CREATED') THEN 'WAIT_OUTBOUND'
|
||||
ELSE o.current_status END,
|
||||
o.updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE o.order_type='RELABEL_APPLICATION' AND o.current_status='PENDING_ASSIGNMENT'
|
||||
AND o.inbound_order_no IS NULL
|
||||
AND (a.status IN ('PENDING_PROCESSING','PROCESSING','CUSTOMER_CONFIRM_PENDING')
|
||||
OR (a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status IN ('WAIT_PRE_DISPATCH','WAIT_OUTBOUND_CREATION','OUTBOUND_CREATED')));
|
||||
COMMIT;
|
||||
@@ -0,0 +1,114 @@
|
||||
-- LOCAL ONLY: explicit test fixtures, NOT evidence of real PDA receiving.
|
||||
-- Caller must set @fixture_customer_id, @fixture_warehouse_id, @fixture_location_id,
|
||||
-- @fixture_destination_id, @fixture_base and @fixture_batch in the same connection.
|
||||
-- Insert-only: duplicate IDs/numbers fail; never restore or overwrite consumed stock.
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_lastleg;
|
||||
CREATE TEMPORARY TABLE fixture_guard (ok INT NOT NULL CHECK (ok=1));
|
||||
INSERT INTO fixture_guard SELECT IF(@fixture_customer_id>0 AND @fixture_base>0
|
||||
AND @fixture_batch REGEXP '^[A-Z0-9-]{1,16}$',1,0);
|
||||
SET @fixture_customer_name=(SELECT tenant_name FROM baiyue_iam.iam_tenant
|
||||
WHERE id=@fixture_customer_id AND deleted=0 AND status='ACTIVE');
|
||||
SET @fixture_warehouse_code=(SELECT warehouse_code FROM baiyue_warehouse.wh_warehouse
|
||||
WHERE id=@fixture_warehouse_id AND warehouse_type='OVERSEAS' AND status='ACTIVE' AND deleted=0);
|
||||
SET @fixture_warehouse_name=(SELECT warehouse_name FROM baiyue_warehouse.wh_warehouse WHERE id=@fixture_warehouse_id);
|
||||
SET @fixture_location_code=(SELECT location_code FROM baiyue_lastlag_warehouse.wh_location
|
||||
WHERE id=@fixture_location_id AND warehouse_id=@fixture_warehouse_id AND location_type='VIRTUAL' AND status='ACTIVE');
|
||||
SET @fixture_destination_code=(SELECT warehouse_code FROM baiyue_warehouse.wh_warehouse
|
||||
WHERE id=@fixture_destination_id AND warehouse_type='FBA' AND status='ACTIVE' AND deleted=0);
|
||||
INSERT INTO fixture_guard SELECT IF(@fixture_customer_name IS NOT NULL AND @fixture_warehouse_code IS NOT NULL
|
||||
AND @fixture_location_code IS NOT NULL AND @fixture_destination_code IS NOT NULL,1,0);
|
||||
CREATE TEMPORARY TABLE fixture_orders (seq INT PRIMARY KEY, boxes INT, order_id BIGINT, plan_id BIGINT,
|
||||
balance_id BIGINT, inbound_id BIGINT, order_no VARCHAR(32), plan_no VARCHAR(32), inbound_code VARCHAR(128), sku VARCHAR(128));
|
||||
INSERT INTO fixture_orders
|
||||
SELECT n,6+n*6,@fixture_base+n,@fixture_base+100+n,@fixture_base+200+n,@fixture_base+300+n,
|
||||
CONCAT('TL-',@fixture_batch,'-',n),CONCAT('IP-',@fixture_batch,'-',n),
|
||||
CONCAT('IN-',@fixture_batch,'-',n),CONCAT('SKU-',@fixture_batch,'-',n)
|
||||
FROM (SELECT 1 n UNION ALL SELECT 2 UNION ALL SELECT 3) numbers;
|
||||
CREATE TEMPORARY TABLE fixture_pallets (seq INT, part INT, pallet_id BIGINT, position_id BIGINT,
|
||||
pallet_code VARCHAR(64), boxes INT, PRIMARY KEY(seq,part));
|
||||
INSERT INTO fixture_pallets SELECT f.seq,p.n,@fixture_base+400+f.seq*10+p.n,
|
||||
@fixture_base+500+f.seq*10+p.n,CONCAT('PAL-',@fixture_batch,'-',f.seq,'-',p.n),f.boxes/2
|
||||
FROM fixture_orders f CROSS JOIN (SELECT 1 n UNION ALL SELECT 2) p;
|
||||
CREATE TEMPORARY TABLE fixture_numbers (n INT PRIMARY KEY);
|
||||
INSERT INTO fixture_numbers SELECT a.n+b.n*10+1 FROM
|
||||
(SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
|
||||
UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
|
||||
CROSS JOIN (SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2) b;
|
||||
START TRANSACTION;
|
||||
INSERT INTO baiyue_lastlag_warehouse.wh_pallet
|
||||
(id,customer_id,warehouse_id,location_id,pallet_code,pallet_type,capacity_cbm,status,remark,fba_warehouse_code,version)
|
||||
SELECT pallet_id,0,@fixture_warehouse_id,@fixture_location_id,pallet_code,'STANDARD',2,'OCCUPIED',
|
||||
'LOCAL_TEST_FIXTURE: relabel source inventory; not real PDA acceptance',@fixture_destination_code,0 FROM fixture_pallets;
|
||||
INSERT INTO ll_inbound_order
|
||||
(id,customer_id,source_region,source_inbound_id,source_order_id,source_order_no,inbound_code,
|
||||
destination_warehouse_id,destination_warehouse_code,destination_type,package_count,product_quantity,pallet_count,
|
||||
source_event_id,source_loading_plan_id,source_loading_plan_version,handover_version,payload_hash,status)
|
||||
SELECT inbound_id,@fixture_customer_id,'FIRST_LEG',CONCAT('FIXTURE-',inbound_code),CAST(order_id AS CHAR),order_no,inbound_code,
|
||||
@fixture_destination_id,@fixture_destination_code,'FBA',boxes,boxes*10,2,
|
||||
CONCAT('FIXTURE-',order_no),CONCAT('FIXTURE-LOAD-',order_no),1,1,SHA2(order_no,256),'RECEIVED' FROM fixture_orders;
|
||||
INSERT INTO ll_order
|
||||
(id,order_no,customer_id,order_type,lx_code,destination_type,inbound_code,source_region,source_inbound_id,
|
||||
source_order_id,source_order_no,customer_name,overseas_warehouse_id,overseas_warehouse_code,overseas_warehouse_name,
|
||||
fba_warehouse_code,container_no,container_status,pallet_count,package_count,product_quantity,
|
||||
inbound_order_no,current_status,source_event_id,source_loading_plan_id,source_payload_hash)
|
||||
SELECT order_id,order_no,@fixture_customer_id,'FIRST_LEG',CONCAT('LX-',order_no),'FBA',inbound_code,'FIRST_LEG',
|
||||
CONCAT('FIXTURE-',inbound_code),CAST(order_id AS CHAR),order_no,@fixture_customer_name,
|
||||
@fixture_warehouse_id,@fixture_warehouse_code,@fixture_warehouse_name,@fixture_destination_code,
|
||||
CONCAT('FIXTURE-',@fixture_batch),'LOCAL_TEST_FIXTURE',2,boxes,boxes*10,plan_no,'INBOUND_COMPLETED',
|
||||
CONCAT('FIXTURE-',order_no),CONCAT('FIXTURE-LOAD-',order_no),SHA2(order_no,256) FROM fixture_orders;
|
||||
INSERT INTO ll_order_detail
|
||||
(id,customer_id,order_id,source_inbound_detail_id,sku_code,product_name_zh,package_count,product_quantity,pallet_count,
|
||||
destination_warehouse_id,destination_warehouse_code,shipping_mark)
|
||||
SELECT @fixture_base+10000+f.seq*100+n.n,@fixture_customer_id,f.order_id,
|
||||
CONCAT('FIXTURE-',f.inbound_code,'-',n.n),f.sku,CONCAT('贴标测试商品-',f.seq),1,10,0,
|
||||
@fixture_destination_id,@fixture_destination_code,CONCAT('CM-',@fixture_batch,'-',f.seq,'-',LPAD(n.n,3,'0'))
|
||||
FROM fixture_orders f JOIN fixture_numbers n ON n.n<=f.boxes;
|
||||
INSERT INTO ll_inbound_plan
|
||||
(id,plan_no,customer_id,source_order_id,container_no,overseas_warehouse_id,planned_arrival_date,
|
||||
pallet_type,assigned_flag,creation_mode,plan_remark,status,line_count,pallet_count,total_volume_cbm,version)
|
||||
SELECT plan_id,plan_no,@fixture_customer_id,order_id,CONCAT('FIXTURE-',@fixture_batch),@fixture_warehouse_id,
|
||||
CURRENT_DATE,'STANDARD',1,'MANUAL','LOCAL_TEST_FIXTURE: 已入库测试准备,非真实PDA签收验收','COMPLETED',1,2,2,0 FROM fixture_orders;
|
||||
INSERT INTO ll_inbound_plan_line (id,customer_id,plan_id,order_id,inbound_code,required_package_count,
|
||||
planned_pallet_count,destination_type,destination_warehouse_code,fba_warehouse_code)
|
||||
SELECT @fixture_base+700+seq,@fixture_customer_id,plan_id,order_id,inbound_code,boxes,2,'FBA',
|
||||
@fixture_destination_code,@fixture_destination_code FROM fixture_orders;
|
||||
INSERT INTO ll_inbound_plan_pallet
|
||||
(id,customer_id,plan_id,pallet_no,pallet_type,destination_warehouse_code,capacity_cbm,allocated_volume_cbm,
|
||||
sequence_no,warehouse_pallet_id,assignment_status,actual_volume_cbm,actual_package_count)
|
||||
SELECT p.pallet_id+1000,@fixture_customer_id,f.plan_id,p.pallet_code,'STANDARD',@fixture_destination_code,
|
||||
2,1,p.part,p.pallet_id,'ASSIGNED',1,p.boxes FROM fixture_pallets p JOIN fixture_orders f ON f.seq=p.seq;
|
||||
INSERT INTO ll_inventory_balance
|
||||
(id,customer_id,customer_name,overseas_warehouse_id,overseas_warehouse_code,overseas_warehouse_name,
|
||||
destination_warehouse_code,inbound_order_id,inbound_order_no,inbound_code,sku_code,product_name_zh,
|
||||
available_packages,available_quantity,version)
|
||||
SELECT balance_id,@fixture_customer_id,@fixture_customer_name,@fixture_warehouse_id,@fixture_warehouse_code,
|
||||
@fixture_warehouse_name,@fixture_destination_code,order_id,plan_no,inbound_code,sku,CONCAT('贴标测试商品-',seq),boxes,boxes*10,0 FROM fixture_orders;
|
||||
INSERT INTO ll_inventory_position
|
||||
(id,customer_id,balance_id,overseas_warehouse_id,destination_warehouse_code,sku_code,pallet_id,pallet_no,batch_no,
|
||||
warehouse_location_id,warehouse_location_code,available_packages,product_quantity,status,version)
|
||||
SELECT p.position_id,@fixture_customer_id,f.balance_id,@fixture_warehouse_id,@fixture_destination_code,f.sku,p.pallet_id,
|
||||
p.pallet_code,CONCAT('FIXTURE-',@fixture_batch),@fixture_location_id,@fixture_location_code,p.boxes,p.boxes*10,'OCCUPIED',0
|
||||
FROM fixture_pallets p JOIN fixture_orders f ON f.seq=p.seq;
|
||||
INSERT INTO ll_inbound_plan_pallet_allocation
|
||||
(id,customer_id,plan_id,line_id,pallet_id,allocated_volume_cbm,planned_package_count,
|
||||
counted_package_count,picked_package_count,actual_volume_cbm,status,version)
|
||||
SELECT p.pallet_id+3000,@fixture_customer_id,f.plan_id,@fixture_base+700+f.seq,p.pallet_id+1000,
|
||||
1,p.boxes,p.boxes,p.boxes,1,'PICKED',0
|
||||
FROM fixture_pallets p JOIN fixture_orders f ON f.seq=p.seq;
|
||||
INSERT INTO ll_inventory_ledger
|
||||
(id,customer_id,customer_name,balance_id,position_id,overseas_warehouse_id,destination_warehouse_code,
|
||||
inbound_order_id,sku_code,pallet_id,pallet_no,batch_no,warehouse_location_code,business_type,business_order_no,
|
||||
inbound_code,inbound_order_no,idempotency_key,status_from,status_to,before_packages,delta_packages,after_packages,remark,occurred_at)
|
||||
SELECT p.position_id+2000,@fixture_customer_id,@fixture_customer_name,f.balance_id,p.position_id,@fixture_warehouse_id,
|
||||
@fixture_destination_code,f.order_id,f.sku,p.pallet_id,p.pallet_code,CONCAT('FIXTURE-',@fixture_batch),@fixture_location_code,
|
||||
'LOCAL_TEST_FIXTURE',f.order_no,f.inbound_code,f.plan_no,CONCAT('LOCAL_TEST_FIXTURE:',p.pallet_code),'NONE','AVAILABLE',
|
||||
0,p.boxes,p.boxes,'Explicit local test fixture; not a real PDA stock-in event',NOW(3)
|
||||
FROM fixture_pallets p JOIN fixture_orders f ON f.seq=p.seq;
|
||||
INSERT INTO fixture_guard SELECT IF(COUNT(*)=6 AND SUM(p.available_packages)=54,1,0)
|
||||
FROM ll_inventory_position p JOIN fixture_pallets f ON f.position_id=p.id;
|
||||
COMMIT;
|
||||
SELECT f.order_no,f.plan_no,f.inbound_code,f.sku,p.pallet_code,p.boxes,
|
||||
CONCAT('CM-',@fixture_batch,'-',f.seq,'-001') first_carton_mark,
|
||||
CONCAT('CM-',@fixture_batch,'-',f.seq,'-',LPAD(f.boxes,3,'0')) last_carton_mark
|
||||
FROM fixture_orders f JOIN fixture_pallets p ON p.seq=f.seq ORDER BY f.seq,p.part;
|
||||
@@ -0,0 +1,326 @@
|
||||
-- 仓储转运审核、PDA测量打托、换标、目的地绑定、智能排车与派送字段。
|
||||
-- 目标数据库:baiyue_lastleg
|
||||
-- 可重复执行:字段和索引独立检查,补充缺失结构并修正必要的可空性;不更新业务数据。
|
||||
|
||||
USE `baiyue_lastleg`;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `ensure_transfer_column`;
|
||||
DROP PROCEDURE IF EXISTS `ensure_transfer_nullable_column`;
|
||||
DROP PROCEDURE IF EXISTS `ensure_transfer_index`;
|
||||
DROP PROCEDURE IF EXISTS `ensure_transfer_pallet_unique_index`;
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
CREATE PROCEDURE `ensure_transfer_column`(
|
||||
IN p_table_name VARCHAR(64),
|
||||
IN p_column_name VARCHAR(64),
|
||||
IN p_column_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = p_table_name
|
||||
AND column_name = p_column_name
|
||||
) THEN
|
||||
SET @ddl = CONCAT(
|
||||
'ALTER TABLE `', REPLACE(p_table_name, '`', '``'),
|
||||
'` ADD COLUMN `', REPLACE(p_column_name, '`', '``'),
|
||||
'` ', p_column_definition
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
CREATE PROCEDURE `ensure_transfer_nullable_column`(
|
||||
IN p_table_name VARCHAR(64),
|
||||
IN p_column_name VARCHAR(64),
|
||||
IN p_column_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = p_table_name
|
||||
AND column_name = p_column_name
|
||||
AND is_nullable = 'NO'
|
||||
) THEN
|
||||
SET @ddl = CONCAT(
|
||||
'ALTER TABLE `', REPLACE(p_table_name, '`', '``'),
|
||||
'` MODIFY COLUMN `', REPLACE(p_column_name, '`', '``'),
|
||||
'` ', p_column_definition
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
CREATE PROCEDURE `ensure_transfer_index`(
|
||||
IN p_table_name VARCHAR(64),
|
||||
IN p_index_name VARCHAR(64),
|
||||
IN p_index_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = p_table_name
|
||||
AND index_name = p_index_name
|
||||
) THEN
|
||||
SET @ddl = CONCAT(
|
||||
'ALTER TABLE `', REPLACE(p_table_name, '`', '``'),
|
||||
'` ADD ', p_index_definition
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
CREATE PROCEDURE `ensure_transfer_pallet_unique_index`()
|
||||
BEGIN
|
||||
DECLARE v_duplicate_groups BIGINT DEFAULT 0;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_inbound_plan_pallet'
|
||||
AND index_name = 'uk_ll_inbound_pallet_no'
|
||||
) THEN
|
||||
SELECT COUNT(*)
|
||||
INTO v_duplicate_groups
|
||||
FROM (
|
||||
SELECT 1
|
||||
FROM `ll_inbound_plan_pallet`
|
||||
GROUP BY `customer_id`, `pallet_no`
|
||||
HAVING COUNT(*) > 1
|
||||
) duplicate_groups;
|
||||
|
||||
IF v_duplicate_groups = 0 THEN
|
||||
ALTER TABLE `ll_inbound_plan_pallet`
|
||||
ADD UNIQUE KEY `uk_ll_inbound_pallet_no` (`customer_id`,`pallet_no`);
|
||||
END IF;
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 相关业务表统一补齐 SaaS 租户字段。默认值只用于兼容历史数据和单租户海外环境。
|
||||
CALL ensure_transfer_column('ll_delivery_order', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_dispatch_item', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_line', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_pallet', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_order', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_relabel_application', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_relabel_item', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_relabel_status_history', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_inventory_allocation', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_item', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_receipt', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_source_order', 'tenant_id', 'VARCHAR(128) NOT NULL DEFAULT ''BAIYUE'' COMMENT ''SaaS租户标识,当前海外租户固定为BAIYUE''');
|
||||
|
||||
-- 退货换标允许只更换箱唛,不强制生成新 SKU。
|
||||
CALL ensure_transfer_nullable_column('ll_relabel_item', 'new_sku_code', 'VARCHAR(128) NULL COMMENT ''换标后SKU编码''');
|
||||
|
||||
-- 主单审核、提货、发货、换标和客户确认事实。
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'pickup_contact_name', 'VARCHAR(128) NULL COMMENT ''提货联系人''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'pickup_contact_phone', 'VARCHAR(64) NULL COMMENT ''提货联系电话''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'estimated_ship_at', 'DATETIME(3) NULL COMMENT ''预计发货时间''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'review_decision', 'VARCHAR(32) NULL COMMENT ''审核决定''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'review_remark', 'VARCHAR(1000) NULL COMMENT ''审核备注或驳回原因''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'reviewed_by', 'VARCHAR(128) NULL COMMENT ''审核人ID''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'reviewed_at', 'DATETIME(3) NULL COMMENT ''审核时间''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'label_deadline', 'DATETIME(3) NULL COMMENT ''换标截止时间''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'relabel_contact_name', 'VARCHAR(128) NULL COMMENT ''换标联系人''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'relabel_contact_phone', 'VARCHAR(64) NULL COMMENT ''换标联系电话''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'relabel_charge_amount', 'DECIMAL(20,2) NULL COMMENT ''换标费用CNY''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'relabel_delivery_method', 'VARCHAR(32) NULL COMMENT ''换标后派送方式''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'relabel_completed_at', 'DATETIME(3) NULL COMMENT ''关联贴标完成时间''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'palletize_idempotency_key', 'VARCHAR(255) NULL COMMENT ''PDA打托幂等键''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'palletize_request_hash', 'CHAR(64) NULL COMMENT ''PDA打托请求哈希''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'confirmation_idempotency_key', 'VARCHAR(255) NULL COMMENT ''客户测量确认幂等键''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'confirmation_request_hash', 'CHAR(64) NULL COMMENT ''客户测量确认请求哈希''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'customer_confirmed_at', 'DATETIME(3) NULL COMMENT ''客户确认测量时间''');
|
||||
|
||||
-- 仓储转运主单目的地。
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'destination_type', 'VARCHAR(16) NULL COMMENT ''目的类型:FBA或PRIVATE''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'destination_warehouse_id', 'VARCHAR(128) NULL COMMENT ''目的仓ID快照''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'destination_warehouse_code', 'VARCHAR(128) NULL COMMENT ''目的仓编码快照''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'destination_warehouse_name', 'VARCHAR(255) NULL COMMENT ''目的仓名称快照''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'private_destination_address', 'VARCHAR(1000) NULL COMMENT ''私人仓完整地址''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'private_destination_postal_code', 'VARCHAR(32) NULL COMMENT ''私人仓邮编''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'private_destination_contact_name', 'VARCHAR(128) NULL COMMENT ''私人仓联系人''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'private_destination_contact_phone', 'VARCHAR(64) NULL COMMENT ''私人仓联系电话''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'destination_binding_idempotency_key', 'VARCHAR(255) NULL COMMENT ''目的地绑定幂等键''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_order', 'destination_binding_request_hash', 'CHAR(64) NULL COMMENT ''目的地绑定请求哈希''');
|
||||
|
||||
-- 转仓尾程主单记录PDA实收箱数。
|
||||
CALL ensure_transfer_column('ll_order', 'received_package_count', 'INT NULL COMMENT ''PDA实际入库箱数''');
|
||||
|
||||
-- PDA逐托草稿、生成、扫码绑定和整单完成事实。
|
||||
CREATE TABLE IF NOT EXISTS `ll_warehouse_transfer_pallet` (
|
||||
`id` BIGINT NOT NULL COMMENT '打托事实ID',
|
||||
`transfer_order_id` BIGINT NOT NULL COMMENT '转运单ID',
|
||||
`customer_id` VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
`sequence_no` INT NOT NULL COMMENT '托序号',
|
||||
`warehouse_pallet_id` BIGINT NULL COMMENT '生成后的海外仓真实托盘ID',
|
||||
`pallet_no` VARCHAR(128) NULL COMMENT '生成后的真实托盘码',
|
||||
`warehouse_location_id` BIGINT NULL COMMENT '生成后的真实库位ID',
|
||||
`warehouse_location_code` VARCHAR(128) NULL COMMENT '真实库位码',
|
||||
`warehouse_resource_lock_id` BIGINT NULL COMMENT '生成后的仓储资源锁ID',
|
||||
`carton_count` INT NOT NULL COMMENT '本托实收箱数',
|
||||
`length_cm` DECIMAL(12,2) NOT NULL COMMENT '实测长cm',
|
||||
`width_cm` DECIMAL(12,2) NOT NULL COMMENT '实测宽cm',
|
||||
`height_cm` DECIMAL(12,2) NOT NULL COMMENT '实测高cm',
|
||||
`gross_weight_kg` DECIMAL(12,3) NOT NULL COMMENT '实测毛重kg',
|
||||
`volume_cbm` DECIMAL(18,6) NOT NULL COMMENT '实测体积CBM',
|
||||
`status` VARCHAR(32) NOT NULL COMMENT '逐托状态DRAFT/GENERATED/BOUND/COMPLETED',
|
||||
`draft_idempotency_key` VARCHAR(255) NULL COMMENT '草稿保存幂等键',
|
||||
`draft_request_hash` CHAR(64) NULL COMMENT '草稿请求摘要',
|
||||
`generate_idempotency_key` VARCHAR(255) NULL COMMENT '新托盘生成幂等键',
|
||||
`bind_idempotency_key` VARCHAR(255) NULL COMMENT '扫码绑定幂等键',
|
||||
`bound_at` DATETIME(3) NULL COMMENT '扫码确认绑定时间',
|
||||
`completed_at` DATETIME(3) NULL COMMENT '整单打托完成时间',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ll_transfer_pallet_seq` (`transfer_order_id`,`sequence_no`),
|
||||
UNIQUE KEY `uk_ll_transfer_pallet_physical` (`customer_id`,`warehouse_pallet_id`),
|
||||
KEY `idx_ll_transfer_pallet_order` (`customer_id`,`transfer_order_id`,`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='仓储转运PDA实测打托事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ll_warehouse_transfer_pallet_item` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键ID',
|
||||
`transfer_order_id` BIGINT NOT NULL COMMENT '转运单ID',
|
||||
`transfer_pallet_id` BIGINT NOT NULL COMMENT '转运托盘事实ID',
|
||||
`transfer_item_id` BIGINT NOT NULL COMMENT '转运申报明细ID',
|
||||
`customer_id` VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
`package_count` INT NOT NULL COMMENT '本托该SKU实收箱数',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ll_transfer_pallet_item` (`transfer_pallet_id`,`transfer_item_id`),
|
||||
KEY `idx_ll_transfer_pallet_item_order` (`customer_id`,`transfer_order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='仓储转运每托SKU箱数回填';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ll_warehouse_transfer_relabel_binding` (
|
||||
`id` BIGINT NOT NULL COMMENT '主键ID',
|
||||
`transfer_order_id` BIGINT NOT NULL COMMENT '转运单ID',
|
||||
`customer_id` VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
`operation_type` VARCHAR(32) NOT NULL COMMENT '换标类型',
|
||||
`customer_carton_mark` VARCHAR(255) NOT NULL COMMENT '客户原箱唛',
|
||||
`original_sku` VARCHAR(128) NOT NULL COMMENT '旧SKU',
|
||||
`new_sku` VARCHAR(128) NULL COMMENT '新SKU',
|
||||
`quantity` INT NOT NULL COMMENT '换标件数',
|
||||
`fba_carton_mark` VARCHAR(255) NULL COMMENT '新FBA箱唛',
|
||||
`destination_warehouse_code` VARCHAR(128) NULL COMMENT 'FBA目的仓',
|
||||
`remark` VARCHAR(1000) NULL COMMENT '备注',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ll_transfer_relabel_mark` (`transfer_order_id`,`customer_carton_mark`),
|
||||
KEY `idx_ll_transfer_relabel_order` (`customer_id`,`transfer_order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='仓储转运换标预绑定明细';
|
||||
|
||||
CALL ensure_transfer_nullable_column('ll_warehouse_transfer_pallet', 'warehouse_pallet_id', 'BIGINT NULL COMMENT ''生成后的海外仓真实托盘ID''');
|
||||
CALL ensure_transfer_nullable_column('ll_warehouse_transfer_pallet', 'pallet_no', 'VARCHAR(128) NULL COMMENT ''生成后的真实托盘码''');
|
||||
CALL ensure_transfer_nullable_column('ll_warehouse_transfer_pallet', 'warehouse_location_id', 'BIGINT NULL COMMENT ''生成后的真实库位ID''');
|
||||
CALL ensure_transfer_nullable_column('ll_warehouse_transfer_pallet', 'warehouse_resource_lock_id', 'BIGINT NULL COMMENT ''生成后的仓储资源锁ID''');
|
||||
CALL ensure_transfer_nullable_column('ll_warehouse_transfer_pallet', 'completed_at', 'DATETIME(3) NULL COMMENT ''整单打托完成时间''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_pallet', 'draft_idempotency_key', 'VARCHAR(255) NULL COMMENT ''草稿保存幂等键''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_pallet', 'draft_request_hash', 'CHAR(64) NULL COMMENT ''草稿请求摘要''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_pallet', 'generate_idempotency_key', 'VARCHAR(255) NULL COMMENT ''新托盘生成幂等键''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_pallet', 'bind_idempotency_key', 'VARCHAR(255) NULL COMMENT ''扫码绑定幂等键''');
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_pallet', 'bound_at', 'DATETIME(3) NULL COMMENT ''扫码确认绑定时间''');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ll_warehouse_transfer_measurement_photo` (
|
||||
`id` BIGINT NOT NULL COMMENT '测量照片引用ID',
|
||||
`transfer_order_id` BIGINT NOT NULL COMMENT '仓储转运单ID',
|
||||
`customer_id` VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
`file_id` BIGINT NOT NULL COMMENT '文件服务文件ID',
|
||||
`file_name` VARCHAR(255) NULL COMMENT '文件名快照',
|
||||
`content_type` VARCHAR(128) NULL COMMENT '文件媒体类型快照',
|
||||
`size_bytes` BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数快照',
|
||||
`uploaded_by` VARCHAR(128) NULL COMMENT 'PDA上传操作人',
|
||||
`uploaded_at` DATETIME(3) NOT NULL COMMENT '上传时间',
|
||||
`submitted_at` DATETIME(3) NULL COMMENT '随测量打托提交时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ll_transfer_measurement_photo_file` (`customer_id`,`transfer_order_id`,`file_id`),
|
||||
KEY `idx_ll_transfer_measurement_photo_order` (`customer_id`,`transfer_order_id`,`uploaded_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='仓储转运PDA测量现场照片引用';
|
||||
CALL ensure_transfer_column('ll_warehouse_transfer_measurement_photo', 'submitted_at', 'DATETIME(3) NULL COMMENT ''随测量打托提交时间''');
|
||||
CALL ensure_transfer_index('ll_warehouse_transfer_measurement_photo', 'uk_ll_transfer_measurement_photo_file',
|
||||
'UNIQUE KEY `uk_ll_transfer_measurement_photo_file` (`customer_id`,`transfer_order_id`,`file_id`)');
|
||||
CALL ensure_transfer_index('ll_warehouse_transfer_measurement_photo', 'idx_ll_transfer_measurement_photo_order',
|
||||
'KEY `idx_ll_transfer_measurement_photo_order` (`customer_id`,`transfer_order_id`,`uploaded_at`)');
|
||||
|
||||
-- 入库计划行和托盘保存私人仓目的地快照。
|
||||
CALL ensure_transfer_column('ll_inbound_plan_line', 'delivery_address', 'VARCHAR(1000) NULL COMMENT ''私人仓目的地址快照''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_line', 'postal_code', 'VARCHAR(32) NULL COMMENT ''私人仓邮编快照''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_line', 'contact_name', 'VARCHAR(128) NULL COMMENT ''私人仓联系人快照''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_line', 'contact_phone', 'VARCHAR(64) NULL COMMENT ''私人仓联系电话快照''');
|
||||
|
||||
CALL ensure_transfer_column('ll_inbound_plan_pallet', 'destination_type', 'VARCHAR(32) NULL COMMENT ''目的类型:FBA或PRIVATE''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_pallet', 'delivery_address', 'VARCHAR(1000) NULL COMMENT ''私人仓目的地址快照''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_pallet', 'postal_code', 'VARCHAR(32) NULL COMMENT ''私人仓邮编快照''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_pallet', 'contact_name', 'VARCHAR(128) NULL COMMENT ''私人仓联系人快照''');
|
||||
CALL ensure_transfer_column('ll_inbound_plan_pallet', 'contact_phone', 'VARCHAR(64) NULL COMMENT ''私人仓联系电话快照''');
|
||||
CALL ensure_transfer_index('ll_inbound_plan_line', 'idx_ll_inbound_line_tenant_plan',
|
||||
'KEY `idx_ll_inbound_line_tenant_plan` (`customer_id`,`plan_id`)');
|
||||
CALL ensure_transfer_index('ll_inbound_plan_pallet', 'idx_ll_inbound_pallet_tenant_plan',
|
||||
'KEY `idx_ll_inbound_pallet_tenant_plan` (`customer_id`,`plan_id`)');
|
||||
CALL ensure_transfer_pallet_unique_index();
|
||||
|
||||
-- 智能排车候选和排车明细快照。
|
||||
CALL ensure_transfer_column('ll_dispatch_item', 'destination_type', 'VARCHAR(32) NULL COMMENT ''目的类型:FBA或PRIVATE''');
|
||||
CALL ensure_transfer_column('ll_dispatch_item', 'delivery_address', 'VARCHAR(1000) NULL COMMENT ''私人仓目的地址快照''');
|
||||
CALL ensure_transfer_column('ll_dispatch_item', 'postal_code', 'VARCHAR(32) NULL COMMENT ''私人仓邮编快照''');
|
||||
CALL ensure_transfer_column('ll_dispatch_item', 'contact_name', 'VARCHAR(128) NULL COMMENT ''私人仓联系人快照''');
|
||||
CALL ensure_transfer_column('ll_dispatch_item', 'contact_phone', 'VARCHAR(64) NULL COMMENT ''私人仓联系电话快照''');
|
||||
|
||||
-- 派送单允许私人仓目的地,并保留地址、邮编及联系人快照。
|
||||
CALL ensure_transfer_column('ll_delivery_order', 'destination_type', 'VARCHAR(16) NULL COMMENT ''目的地类型:FBA或PRIVATE''');
|
||||
CALL ensure_transfer_column('ll_delivery_order', 'delivery_address', 'VARCHAR(1000) NULL COMMENT ''私人仓派送地址''');
|
||||
CALL ensure_transfer_column('ll_delivery_order', 'postal_code', 'VARCHAR(32) NULL COMMENT ''私人仓邮编''');
|
||||
CALL ensure_transfer_column('ll_delivery_order', 'contact_name', 'VARCHAR(128) NULL COMMENT ''私人仓联系人''');
|
||||
CALL ensure_transfer_column('ll_delivery_order', 'contact_phone', 'VARCHAR(64) NULL COMMENT ''私人仓联系电话''');
|
||||
|
||||
-- 仓储转运直接出库历史兼容:这三列允许为空。
|
||||
CALL ensure_transfer_nullable_column('ll_delivery_order', 'dispatch_order_id', 'BIGINT NULL COMMENT ''关联派车单ID,仓储转运直接出库时为空''');
|
||||
CALL ensure_transfer_nullable_column('ll_delivery_order', 'dispatch_no', 'VARCHAR(64) NULL COMMENT ''关联派车单号,仓储转运直接出库时为空''');
|
||||
CALL ensure_transfer_nullable_column('ll_delivery_order', 'destination_warehouse_code', 'VARCHAR(128) NULL COMMENT ''本派送单目的FBA仓编码快照,私人仓为空''');
|
||||
|
||||
DROP PROCEDURE IF EXISTS `ensure_transfer_column`;
|
||||
DROP PROCEDURE IF EXISTS `ensure_transfer_nullable_column`;
|
||||
DROP PROCEDURE IF EXISTS `ensure_transfer_index`;
|
||||
DROP PROCEDURE IF EXISTS `ensure_transfer_pallet_unique_index`;
|
||||
|
||||
SELECT DATABASE() AS target_database,
|
||||
IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_inbound_plan_pallet'
|
||||
AND index_name = 'uk_ll_inbound_pallet_no'
|
||||
),
|
||||
'WAREHOUSE_TRANSFER_LATEST_SCHEMA_READY',
|
||||
'WAREHOUSE_TRANSFER_SCHEMA_APPLIED_WITH_INDEX_BLOCKER'
|
||||
) AS migration_result,
|
||||
IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_inbound_plan_pallet'
|
||||
AND index_name = 'uk_ll_inbound_pallet_no'
|
||||
),
|
||||
'READY',
|
||||
'BLOCKED_BY_DUPLICATE_CUSTOMER_PALLET_NO'
|
||||
) AS inbound_pallet_unique_index;
|
||||
@@ -22,8 +22,8 @@ function assert(value, message) { if (!value) throw new Error(message) }
|
||||
async function api(base, token, method, path, body) {
|
||||
const response = await fetch(`${base}${path}`, { method,
|
||||
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
||||
body: body === undefined ? undefined : JSON.stringify(body) })
|
||||
...(body === undefined || body instanceof FormData ? {} : { 'Content-Type': 'application/json' }) },
|
||||
body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body) })
|
||||
const text = await response.text(); let value
|
||||
try { value = text ? JSON.parse(text) : null } catch { value = { raw: text } }
|
||||
if (!response.ok || String(value?.code) !== '200') {
|
||||
@@ -48,12 +48,32 @@ const platformToken = directPlatformToken || platformLogin.overseasAccessToken
|
||||
const merchantToken = directMerchantToken || merchantLogin.overseasAccessToken
|
||||
const pdaToken = directPdaToken || pdaLogin.overseasAccessToken
|
||||
|
||||
await api(overseas, platformToken, 'PUT', `/api/platform/v1/last-leg/dispatches/${dispatchNo}`, {
|
||||
vehiclePlate: `UAT-${marker.slice(-8)}`, driverName: '尾程验收司机', driverPhone: '19900000000',
|
||||
carrierName: '尾程验收车队', route: `${marker}-ROUTE`,
|
||||
estimatedArrivalAt: new Date(Date.now() + 86400000).toISOString(),
|
||||
})
|
||||
const transfer = await api(overseas, merchantToken, 'POST',
|
||||
let task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`)
|
||||
let alreadyDeparted = task.status === 'DEPARTED'
|
||||
|| (task.status === 'COMPLETED' && String(task.departBlockReason || '').includes('已发车'))
|
||||
if (!alreadyDeparted && task.status === 'PENDING') {
|
||||
await api(overseas, platformToken, 'PUT', `/api/platform/v1/last-leg/dispatches/${dispatchNo}`, {
|
||||
vehiclePlate: `UAT-${marker.slice(-8)}`, driverName: '尾程验收司机', driverPhone: '19900000000',
|
||||
carrierName: '尾程验收车队', route: `${marker}-ROUTE`,
|
||||
estimatedArrivalAt: new Date(Date.now() + 86400000).toISOString(),
|
||||
})
|
||||
}
|
||||
const existingPage = await api(overseas, platformToken, 'GET',
|
||||
`/api/platform/v1/last-leg/delivery-orders?customerId=${customerId}&page=1&size=20&dispatchNo=${encodeURIComponent(dispatchNo)}`)
|
||||
const existingDeliveries = (existingPage.records || []).filter((value) => value.dispatchNo === dispatchNo)
|
||||
const existingOutboundNos = new Set(existingDeliveries.map((value) => value.outboundNo).filter(Boolean))
|
||||
assert(existingOutboundNos.size <= 1, 'Dispatch is linked to multiple outbound orders')
|
||||
const transfer = existingDeliveries.length > 0 ? {
|
||||
outboundNo: [...existingOutboundNos][0],
|
||||
deliveries: existingDeliveries.map((value) => ({
|
||||
deliveryNo: value.deliveryNo, deliveryStatus: value.status,
|
||||
destinationWarehouseId: value.destinationWarehouseId,
|
||||
destinationWarehouseCode: value.destinationWarehouseCode,
|
||||
destinationWarehouseName: value.destinationWarehouseName,
|
||||
palletCount: value.palletCount, packageCount: value.packageCount,
|
||||
})),
|
||||
replayed: true,
|
||||
} : await api(overseas, merchantToken, 'POST',
|
||||
`/api/merchant/v1/last-leg/delivery-orders?dispatchNo=${encodeURIComponent(dispatchNo)}`, {
|
||||
idempotencyKey: key('TRANSFER'), plannedDeliveryAt: new Date(Date.now() + 172800000).toISOString(),
|
||||
deliveryMethod: 'TRUCK_PALLET', waybillNo: `${marker}-WAYBILL`, carrierName: '尾程验收车队',
|
||||
@@ -62,29 +82,53 @@ const transfer = await api(overseas, merchantToken, 'POST',
|
||||
appointmentAccount: 'SELF', remark: marker, fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费',
|
||||
direction: 'RECEIVABLE', currency: 'CNY', amount: 10, remark: marker }],
|
||||
})
|
||||
assert(transfer.outboundNo && transfer.deliveryNo, 'Transfer did not create outbound and delivery')
|
||||
const transferDeliveries = Array.isArray(transfer.deliveries) ? transfer.deliveries : []
|
||||
assert(transfer.outboundNo && transferDeliveries.length > 0,
|
||||
'Transfer did not create outbound and delivery')
|
||||
assert(transferDeliveries.length === 1,
|
||||
`Expected one destination delivery, got ${transferDeliveries.length}`)
|
||||
const deliveryNo = transferDeliveries[0].deliveryNo
|
||||
|
||||
let task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`)
|
||||
assert(Array.isArray(task.items) && task.items.length === 2, 'Expected two PDA pallet items')
|
||||
for (const [itemIndex, item] of task.items.entries()) {
|
||||
let remaining = Number(item.scheduledPackages) - Number(item.pickedPackages)
|
||||
for (const [positionIndex, position] of item.positions.entries()) {
|
||||
if (remaining <= 0) break
|
||||
const packages = Math.min(remaining, Number(position.availablePackages))
|
||||
if (packages <= 0) continue
|
||||
await api(overseas, pdaToken, 'POST',
|
||||
`/api/pda/v1/last-leg/dispatches/${dispatchNo}/items/${id(item.dispatchItemId)}/pick`, {
|
||||
positionId: id(position.id), packages, productQuantity: packages,
|
||||
idempotencyKey: key(`PICK-${itemIndex}-${positionIndex}`), remark: marker,
|
||||
})
|
||||
remaining -= packages
|
||||
if (!alreadyDeparted) {
|
||||
if (task.items.every(item => Number(item.pickedPackages) === 0)) {
|
||||
await api(overseas, platformToken, 'PUT', `/api/platform/v1/last-leg/dispatches/${dispatchNo}`, {
|
||||
vehiclePlate: `UAT-${marker.slice(-8)}`, driverName: '尾程验收司机', driverPhone: '19900000000',
|
||||
carrierName: '尾程验收车队', fbaAppointmentNo: `UAT-${marker.slice(-8)}`,
|
||||
fbaAppointmentStatus: 'CONFIRMED', vehicleAppointmentStatus: 'CONFIRMED',
|
||||
estimatedArrivalAt: new Date(Date.now() + 172800000).toISOString().slice(0, 19),
|
||||
})
|
||||
}
|
||||
assert(remaining === 0, `PDA item ${item.dispatchItemId} has insufficient inventory`)
|
||||
for (const [itemIndex, item] of task.items.entries()) {
|
||||
let remaining = Number(item.scheduledPackages) - Number(item.pickedPackages)
|
||||
for (const [positionIndex, position] of item.positions.entries()) {
|
||||
if (remaining <= 0) break
|
||||
const packages = Math.min(remaining, Number(position.availablePackages))
|
||||
if (packages <= 0) continue
|
||||
await api(overseas, pdaToken, 'POST',
|
||||
`/api/pda/v1/last-leg/dispatches/${dispatchNo}/items/${id(item.dispatchItemId)}/pick`, {
|
||||
positionId: id(position.id), packages, productQuantity: packages,
|
||||
idempotencyKey: key(`PICK-${itemIndex}-${positionIndex}`), remark: marker,
|
||||
})
|
||||
remaining -= packages
|
||||
}
|
||||
assert(remaining === 0, `PDA item ${item.dispatchItemId} has insufficient inventory`)
|
||||
}
|
||||
task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`)
|
||||
alreadyDeparted = task.status === 'DEPARTED'
|
||||
|| (task.status === 'COMPLETED' && String(task.departBlockReason || '').includes('已发车'))
|
||||
}
|
||||
const completed = await api(overseas, pdaToken, 'POST',
|
||||
`/api/pda/v1/last-leg/dispatches/${dispatchNo}/complete`, { idempotencyKey: key('PICK-COMPLETE') })
|
||||
assert(completed.status === 'READY_TO_DEPART', 'Dispatch did not reach READY_TO_DEPART')
|
||||
const departed = await api(overseas, pdaToken, 'POST',
|
||||
if (!alreadyDeparted && task.status !== 'COMPLETED') {
|
||||
const pickingCompleted = await api(overseas, pdaToken, 'POST',
|
||||
`/api/pda/v1/last-leg/dispatches/${dispatchNo}/complete`, { idempotencyKey: key('PICK-COMPLETE') })
|
||||
assert(pickingCompleted.status === 'READY_TO_DEPART', 'Dispatch did not reach READY_TO_DEPART')
|
||||
task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`)
|
||||
}
|
||||
assert(alreadyDeparted || task.status === 'COMPLETED', 'Dispatch did not finish picking')
|
||||
const departed = alreadyDeparted ? {
|
||||
status: 'DEPARTED',
|
||||
shippedPackages: task.items.reduce((total, item) => total + Number(item.pickedPackages), 0),
|
||||
} : await api(overseas, pdaToken, 'POST',
|
||||
`/api/pda/v1/last-leg/dispatches/${dispatchNo}/depart`, { idempotencyKey: key('DEPART'), remark: marker })
|
||||
assert(departed.status === 'DEPARTED', 'Dispatch did not reach DEPARTED')
|
||||
|
||||
@@ -92,24 +136,47 @@ const outbound = await api(overseas, platformToken, 'GET',
|
||||
`/api/platform/v1/last-leg/outbound-orders/${transfer.outboundNo}?page=1&size=20`)
|
||||
assert(outbound.outbound.status === 'OUTBOUNDED', 'Outbound did not reach OUTBOUNDED')
|
||||
let delivery = await api(overseas, platformToken, 'GET',
|
||||
`/api/platform/v1/last-leg/delivery-orders/${transfer.deliveryNo}?customerId=${customerId}`)
|
||||
const delivering = await api(overseas, platformToken, 'PUT',
|
||||
`/api/platform/v1/last-leg/delivery-orders/${transfer.deliveryNo}/status?customerId=${customerId}`, {
|
||||
status: 'DELIVERING', expectedVersion: Number(delivery.order.version), remark: marker,
|
||||
`/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`)
|
||||
if (delivery.order.status === 'WAIT_DELIVERY') {
|
||||
const delivering = await api(overseas, platformToken, 'PUT',
|
||||
`/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/status?customerId=${customerId}`, {
|
||||
status: 'DELIVERING', expectedVersion: Number(delivery.order.version), remark: marker,
|
||||
})
|
||||
assert(delivering.deliveryStatus === 'DELIVERING', 'Delivery did not reach DELIVERING')
|
||||
delivery = await api(overseas, platformToken, 'GET',
|
||||
`/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`)
|
||||
}
|
||||
if (delivery.order.status !== 'DELIVERED') {
|
||||
assert(delivery.order.status === 'DELIVERING', `Unexpected delivery status ${delivery.order.status}`)
|
||||
const documentsPath = `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/documents?customerId=${customerId}`
|
||||
const documents = await api(overseas, platformToken, 'GET', documentsPath)
|
||||
const form = new FormData()
|
||||
form.append('file', new Blob([`${marker} receipt acceptance for ${deliveryNo}`], { type: 'text/plain' }),
|
||||
`${marker}-${deliveryNo}-receipt.txt`)
|
||||
const receipt = await api(overseas, platformToken, 'POST',
|
||||
`/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/documents/RECEIPT/files?customerId=${customerId}`, form)
|
||||
const existingBindings = (documents.categories || []).map((category) => ({
|
||||
category: category.category,
|
||||
fileIds: (category.files || []).map((file) => id(file.fileId)),
|
||||
})).filter((binding) => binding.category !== 'RECEIPT' && binding.fileIds.length > 0)
|
||||
await api(overseas, platformToken, 'PUT', documentsPath, {
|
||||
expectedVersion: Number(documents.version), idempotencyKey: key('SAVE-RECEIPT'),
|
||||
bolNo: documents.bolNo || null,
|
||||
documents: [...existingBindings, { category: 'RECEIPT', fileIds: [id(receipt.fileId)] }],
|
||||
})
|
||||
assert(delivering.deliveryStatus === 'DELIVERING', 'Delivery did not reach DELIVERING')
|
||||
delivery = await api(overseas, platformToken, 'GET',
|
||||
`/api/platform/v1/last-leg/delivery-orders/${transfer.deliveryNo}?customerId=${customerId}`)
|
||||
const delivered = await api(overseas, platformToken, 'POST',
|
||||
`/api/platform/v1/last-leg/delivery-orders/${transfer.deliveryNo}/complete?customerId=${customerId}`, {
|
||||
status: 'DELIVERED', expectedVersion: Number(delivery.order.version), remark: marker,
|
||||
})
|
||||
assert(delivered.deliveryStatus === 'DELIVERED', 'Delivery did not reach DELIVERED')
|
||||
delivery = await api(overseas, platformToken, 'GET',
|
||||
`/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`)
|
||||
}
|
||||
assert(delivery.order.status === 'DELIVERED', 'Receipt did not automatically complete delivery')
|
||||
const visits = await api(overseas, platformToken, 'GET',
|
||||
`/api/platform/v1/last-leg/customer-visits?page=1&size=200&customerId=${customerId}&keyword=${encodeURIComponent(deliveryNo)}`)
|
||||
const visit = (visits.records || []).find((value) => value.deliveryNo === deliveryNo)
|
||||
assert(visit?.status === 'PENDING', 'Delivery completion did not create a pending customer visit')
|
||||
|
||||
const report = { marker, scope: 'OVERSEAS_LASTLEG_ONLY', sourceReport, relabelNo: scenario.relabelNo,
|
||||
dispatchNo, dispatchStatus: departed.status, outboundNo: transfer.outboundNo,
|
||||
outboundStatus: outbound.outbound.status, deliveryNo: transfer.deliveryNo,
|
||||
deliveryStatus: delivered.deliveryStatus, visitId: id(delivered.visitId), shippedPackages: departed.shippedPackages,
|
||||
outboundStatus: outbound.outbound.status, deliveryNo,
|
||||
deliveryStatus: delivery.order.status, visitId: id(visit.id), shippedPackages: departed.shippedPackages,
|
||||
passed: true, finishedAt: new Date().toISOString() }
|
||||
await writeFile(output, JSON.stringify(report, null, 2), 'utf8')
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
|
||||
@@ -13,6 +13,8 @@ const directMerchantToken = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN
|
||||
const directPdaToken = process.env.PDA_OVERSEAS_ACCESS_TOKEN
|
||||
const output = process.env.OVERSEAS_ACCEPTANCE_OUTPUT || 'reports/overseas-relabel-real-acceptance.json'
|
||||
const marker = `TAIL-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}`
|
||||
const chargeAmount = Number(process.env.OVERSEAS_RELABEL_CHARGE_AMOUNT ?? '0.01')
|
||||
if (!Number.isFinite(chargeAmount) || chargeAmount < 0) throw new Error('Invalid charge amount')
|
||||
|
||||
for (const [name, value] of Object.entries({ customerId, warehouseId })) {
|
||||
if (!value) throw new Error(`Missing environment variable: ${name}`)
|
||||
@@ -21,7 +23,7 @@ if (!directPlatformToken && (!platformUser || !platformPassword)) throw new Erro
|
||||
if (!directMerchantToken && (!platformUser || !platformPassword)) throw new Error('Missing merchant token or platform credentials')
|
||||
if (!directPdaToken && (!pdaUser || !pdaPassword)) throw new Error('Missing PDA credentials or token')
|
||||
|
||||
const report = { marker, startedAt: new Date().toISOString(), scope: 'OVERSEAS_LASTLEG_ONLY', scenarios: [], passed: false }
|
||||
const report = { marker, startedAt: new Date().toISOString(), scope: 'OVERSEAS_LASTLEG_ONLY', chargeAmount, photoEvidence: 'SYNTHETIC_PNG_NOT_PHYSICAL_WORK', scenarios: [], passed: false }
|
||||
function key(value) { return `${marker}-${value}`.slice(0, 200) }
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
async function api(base, token, method, path, body, headers = {}) {
|
||||
@@ -70,7 +72,7 @@ async function resolvePallet(palletCode) {
|
||||
return api(overseas, pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode })
|
||||
}
|
||||
|
||||
async function createAndApprove(name, cartonCount) {
|
||||
async function createAndConfirm(name, cartonCount, operationType = 'CARTON_AND_SKU', prepare = null) {
|
||||
const suffix = `${name}-${Date.now()}`
|
||||
const items = Array.from({ length: cartonCount }, (_, index) => ({
|
||||
sourceInventoryBalanceId: id(source.inventoryBalanceId), sourceInventoryPositionId: id(sourcePosition.positionId),
|
||||
@@ -78,49 +80,68 @@ async function createAndApprove(name, cartonCount) {
|
||||
productNameZh: source.productNameZh || source.skuCode, totalPackageCount: 1, notifiedPackageCount: 1,
|
||||
shipmentNo: `SHIP-${suffix}-${index + 1}`, newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128),
|
||||
labelRequirement: marker, cartonMark: `CM-${suffix}-${index + 1}`,
|
||||
fbaCartonMark: `FBA-${suffix}-${index + 1}`, destinationFbaWarehouseCode: source.destinationFbaWarehouseCode,
|
||||
operationType: 'CARTON_AND_SKU', skuItems: [{ skuCode: source.skuCode,
|
||||
fbaCartonMark: operationType === 'CARTON_AND_SKU' ? `FBA-${suffix}-${index + 1}` : null,
|
||||
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode,
|
||||
operationType, skuItems: [{ skuCode: source.skuCode,
|
||||
newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), quantity: 1, remark: marker }],
|
||||
}))
|
||||
const created = await api(overseas, platformToken, 'POST',
|
||||
`/api/platform/v1/last-leg/relabel-applications?customerId=${customerId}`, {
|
||||
customerOrderNo: `TAIL-${suffix}`.slice(0, 128), operationWarehouseId: warehouseId,
|
||||
labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19),
|
||||
contactName: '尾程验收', contactPhone: '13800000006', chargeAmount: 0.01,
|
||||
remark: `${marker}-${name}`, idempotencyKey: key(`${name}-CREATE`), deliveryMethod: 'TRUCK',
|
||||
contactName: '尾程验收', contactPhone: '13800000006', ...(chargeAmount > 0 ? { chargeAmount } : {}),
|
||||
remark: `${marker}-${name}`, idempotencyKey: key(`${name}-CREATE`),
|
||||
deliveryMethod: operationType === 'SKU_LABEL' ? null : 'TRUCK',
|
||||
printShippingLabel: false, items,
|
||||
})
|
||||
const relabelNo = created.application.relabelNo
|
||||
await poll(() => api(overseas, platformToken, 'GET',
|
||||
if (chargeAmount > 0) await poll(() => api(overseas, platformToken, 'GET',
|
||||
`/api/platform/v1/last-leg/relabel-applications/${relabelNo}`),
|
||||
(detail) => detail.application.walletStatus === 'FROZEN')
|
||||
let detail = await api(overseas, pdaToken, 'POST',
|
||||
`/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/claim`, { idempotencyKey: key(`${name}-CLAIM`) })
|
||||
for (const [index, item] of detail.items.entries()) {
|
||||
if (operationType === 'CARTON_AND_SKU') {
|
||||
detail = await api(overseas, pdaToken, 'POST',
|
||||
`/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/cartons/scan`, {
|
||||
cartonMark: item.cartonMark, expectedVersion: Number(detail.version),
|
||||
idempotencyKey: key(`${name}-CARTON-${index}`),
|
||||
})
|
||||
const stageItem = detail.items.find((row) => id(row.itemId) === id(item.itemId))
|
||||
assert(Number(stageItem?.cartonProcessedPackages) === 1
|
||||
&& Number(stageItem?.skuProcessedPackages) === 0,
|
||||
`${relabelNo} carton stage incorrectly locked or completed SKU stage`)
|
||||
}
|
||||
detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/units`, {
|
||||
itemId: id(item.itemId), oldLabel: item.cartonMark, newLabel: item.fbaCartonMark,
|
||||
itemId: id(item.itemId), oldLabel: item.cartonMark, newLabel: null,
|
||||
skuVerifications: (item.skuItems || []).map((sku) => ({
|
||||
skuCode: sku.skuCode, newSkuCode: sku.newSkuCode, quantity: Number(sku.quantity),
|
||||
})),
|
||||
expectedVersion: Number(detail.version), idempotencyKey: key(`${name}-SCAN-${index}`),
|
||||
})
|
||||
}
|
||||
const sourceBefore = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages)
|
||||
const scenario = { name, operationType, relabelNo, items: detail.items, sourceBefore }
|
||||
if (prepare) await prepare(scenario)
|
||||
const form = new FormData()
|
||||
form.append('file', new Blob([Buffer.from('89504e470d0a1a0a', 'hex')], { type: 'image/png' }), `${marker}.png`)
|
||||
// Valid synthetic PNG for HTTP validation; not a photograph of real warehouse work.
|
||||
form.append('file', new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64')], { type: 'image/png' }), `${marker}.png`)
|
||||
const photo = await api(overseas, pdaToken, 'POST',
|
||||
`/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/photos/files`, form)
|
||||
detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/complete`, {
|
||||
photoFileIds: [id(photo.fileId)], expectedVersion: Number(detail.version),
|
||||
idempotencyKey: key(`${name}-COMPLETE`), remark: marker,
|
||||
})
|
||||
detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/review`, {
|
||||
decision: 'APPROVE', expectedVersion: Number(detail.version),
|
||||
idempotencyKey: key(`${name}-APPROVE`), remark: marker,
|
||||
})
|
||||
assert(detail.applicationStatus === 'CUSTOMER_CONFIRM_PENDING', `${relabelNo} did not reach customer confirmation`)
|
||||
const confirmed = await api(overseas, merchantToken, 'POST',
|
||||
`/api/merchant/v1/last-leg/relabel-applications/${relabelNo}/confirm-completion`, {
|
||||
idempotencyKey: key(`${name}-CONFIRM`), remark: marker,
|
||||
})
|
||||
assert(confirmed.application.status === 'READY_FOR_OUTBOUND', `${relabelNo} did not reach READY_FOR_OUTBOUND`)
|
||||
return { name, relabelNo, items: confirmed.items, sourceBefore: Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) }
|
||||
const expectedStatus = operationType === 'SKU_LABEL' ? 'COMPLETED' : 'READY_FOR_OUTBOUND'
|
||||
assert(confirmed.application.status === expectedStatus, `${relabelNo} did not reach ${expectedStatus}`)
|
||||
assert(confirmed.items.every((item) => item.destinationFbaWarehouseCode === source.destinationFbaWarehouseCode),
|
||||
`${relabelNo} did not preserve the destination warehouse`)
|
||||
return { ...scenario, items: confirmed.items }
|
||||
}
|
||||
|
||||
async function printPallet(relabelNo, suffix) {
|
||||
@@ -141,26 +162,38 @@ async function moveCarton(scenario, item, target, index) {
|
||||
return result
|
||||
}
|
||||
|
||||
const fresh = await createAndApprove('NEW-PALLET', 1)
|
||||
const sharedTarget = await printPallet(fresh.relabelNo, 'SHARED')
|
||||
const printReplay = await printPallet(fresh.relabelNo, 'SHARED')
|
||||
assert(id(sharedTarget.palletId) === id(printReplay.palletId), 'Pallet print replay was not idempotent')
|
||||
await moveCarton(fresh, fresh.items[0], sharedTarget, 0)
|
||||
fresh.targets = [sharedTarget]
|
||||
const skuOnly = await createAndConfirm('SKU-ONLY', 1, 'SKU_LABEL')
|
||||
skuOnly.sourceAfter = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages)
|
||||
assert(skuOnly.sourceAfter === skuOnly.sourceBefore, 'SKU-only relabel must not split or regroup pallets')
|
||||
skuOnly.targetPallets = []
|
||||
skuOnly.destinationFbaWarehouseCode = source.destinationFbaWarehouseCode
|
||||
delete skuOnly.items
|
||||
|
||||
let sharedTarget
|
||||
const fresh = await createAndConfirm('NEW-PALLET', 1, 'CARTON_AND_SKU', async scenario => {
|
||||
sharedTarget = await printPallet(scenario.relabelNo, 'SHARED')
|
||||
const printReplay = await printPallet(scenario.relabelNo, 'SHARED')
|
||||
assert(id(sharedTarget.palletId) === id(printReplay.palletId), 'Pallet print replay was not idempotent')
|
||||
await moveCarton(scenario, scenario.items[0], sharedTarget, 0)
|
||||
scenario.targets = [sharedTarget]
|
||||
})
|
||||
|
||||
const merged = await createAndApprove('MERGE-EXISTING', 1)
|
||||
const sharedBefore = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages)
|
||||
await moveCarton(merged, merged.items[0], sharedTarget, 0)
|
||||
const merged = await createAndConfirm('MERGE-EXISTING', 1, 'CARTON_AND_SKU', async scenario => {
|
||||
await moveCarton(scenario, scenario.items[0], sharedTarget, 0)
|
||||
scenario.targets = [sharedTarget]
|
||||
})
|
||||
const sharedAfter = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages)
|
||||
assert(sharedAfter === sharedBefore + 1, 'Existing target pallet carton count did not increase')
|
||||
merged.targets = [sharedTarget]
|
||||
|
||||
const split = await createAndApprove('SPLIT-TWO-TARGETS', 2)
|
||||
const splitA = await printPallet(split.relabelNo, 'SPLIT-A')
|
||||
const splitB = await printPallet(split.relabelNo, 'SPLIT-B')
|
||||
await moveCarton(split, split.items[0], splitA, 0)
|
||||
await moveCarton(split, split.items[1], splitB, 1)
|
||||
split.targets = [splitA, splitB]
|
||||
const split = await createAndConfirm('SPLIT-TWO-TARGETS', 2, 'CARTON_AND_SKU', async scenario => {
|
||||
const splitA = await printPallet(scenario.relabelNo, 'SPLIT-A')
|
||||
const splitB = await printPallet(scenario.relabelNo, 'SPLIT-B')
|
||||
await moveCarton(scenario, scenario.items[0], splitA, 0)
|
||||
await moveCarton(scenario, scenario.items[1], splitB, 1)
|
||||
scenario.targets = [splitA, splitB]
|
||||
})
|
||||
|
||||
for (const scenario of [fresh, merged, split]) {
|
||||
const sourceAfter = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages)
|
||||
@@ -191,7 +224,7 @@ const ledgerRows = ledger.records.filter((row) => String(row.remark || '').inclu
|
||||
assert(ledgerRows.some((row) => Number(row.deltaPackages) < 0), 'Missing negative relabel pallet ledger')
|
||||
assert(ledgerRows.some((row) => Number(row.deltaPackages) > 0), 'Missing positive relabel pallet ledger')
|
||||
assert(ledgerRows.every((row) => String(row.remark).startsWith('换标拆托')), 'Unexpected relabel ledger reason')
|
||||
report.scenarios = [fresh, merged, split]
|
||||
report.scenarios = [skuOnly, fresh, merged, split]
|
||||
report.ledger = { rows: ledgerRows.length, deltas: ledgerRows.map((row) => Number(row.deltaPackages)) }
|
||||
report.passed = true
|
||||
report.finishedAt = new Date().toISOString()
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { mkdir, writeFile, readFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
const base = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300'
|
||||
if (!['localhost', '127.0.0.1'].includes(new URL(base).hostname)) throw new Error('Local acceptance only')
|
||||
const merchant = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN, pda = process.env.PDA_OVERSEAS_ACCESS_TOKEN, admin = process.env.ADMIN_OVERSEAS_ACCESS_TOKEN
|
||||
const warehouse = process.env.OVERSEAS_TEST_WAREHOUSE_ID
|
||||
if (!merchant || !pda || !admin || !warehouse) throw new Error('Explicit session and warehouse required')
|
||||
const marker = `CLOSE-${Date.now()}`
|
||||
const report = { marker, passed: false, scope: 'REAL_HTTP_SYNTHETIC_WAREHOUSE_EVIDENCE_NO_FINANCE', scenarios: [] }
|
||||
const key = () => `${marker}-${crypto.randomUUID()}`
|
||||
const root = '/api/merchant/v1/last-leg/relabel-applications'
|
||||
const work = '/api/pda/v1/last-leg/relabel-tasks'
|
||||
const pallets = '/api/pda/v1/last-leg/pallet-operations'
|
||||
const assert = (condition, message) => { if (!condition) throw new Error(message) }
|
||||
async function raw(token, method, path, body) {
|
||||
const response = await fetch(base + path, { method, headers: { Authorization: `Bearer ${token}`, ...(body && !(body instanceof FormData) ? { 'Content-Type': 'application/json' } : {}) }, body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined })
|
||||
const result = await response.json()
|
||||
return { ...result, http: response.status }
|
||||
}
|
||||
async function api(token, method, path, body) {
|
||||
const result = await raw(token, method, path, body)
|
||||
assert(result.http === 200 && String(result.code) === '200', `${path}: ${result.code} ${result.message}`)
|
||||
return result.data
|
||||
}
|
||||
async function reject(token, path, body, message) {
|
||||
const result = await raw(token, 'POST', path, body)
|
||||
assert(String(result.code) !== '200' && message.test(result.message || ''), `Expected rejection at ${path}, got ${result.code} ${result.message}`)
|
||||
}
|
||||
const resolve = code => api(pda, 'POST', `${pallets}/pallets/resolve`, { code })
|
||||
async function move(no, mark, from, to) {
|
||||
const source = await resolve(from), target = await resolve(to)
|
||||
const request = { relabelNo: no, source: { palletCode: from, expectedVersion: source.palletVersion }, target: { palletCode: to, expectedVersion: target.palletVersion }, cartonMarks: [mark], reason: marker, idempotencyKey: key() }
|
||||
const result = await api(pda, 'POST', `${pallets}/relabel-disassemble`, request)
|
||||
const replay = await api(pda, 'POST', `${pallets}/relabel-disassemble`, request)
|
||||
assert(replay.replayed === true, 'Move replay was not idempotent')
|
||||
const after = await resolve(to)
|
||||
assert(Number(after.totalPackages) === Number(target.totalPackages) + 1 && after.status === 'OCCUPIED', 'Target stock/status did not update exactly once')
|
||||
assert(Number((await resolve(from)).totalPackages) === Number(source.totalPackages) - 1, 'Source stock was not reduced once')
|
||||
return result
|
||||
}
|
||||
try {
|
||||
for (const deliveryMethod of ['TRUCK_PICKUP', 'EXPRESS', 'EXPRESS_PICKUP']) {
|
||||
const candidates = await api(merchant, 'GET', `${root}/inventory-candidates?operationWarehouseId=${warehouse}`)
|
||||
const candidate = candidates.find(value => value.destinationFbaWarehouseCode && value.positions?.some(pos => Number(pos.availablePackages) > 0 && pos.palletNo !== 'UNBOUND'))
|
||||
assert(candidate, 'No available inventory for another scenario')
|
||||
const position = candidate.positions.find(pos => Number(pos.availablePackages) > 0 && pos.palletNo !== 'UNBOUND')
|
||||
const mark = `${marker}-${deliveryMethod}`
|
||||
const request = { customerOrderNo: mark, operationWarehouseId: warehouse, labelDeadline: new Date(Date.now() + 86400000).toISOString().slice(0, 19), contactName: '流程验收', contactPhone: '13800000000', deliveryMethod, printShippingLabel: false, idempotencyKey: key(), items: [{ sourceInventoryBalanceId: candidate.inventoryBalanceId, sourceInventoryPositionId: position.positionId, sourceInboundOrderId: candidate.inboundOrderId, skuCode: candidate.skuCode || 'TEST', productNameZh: '验收测试货物', totalPackageCount: 1, notifiedPackageCount: 1, cartonMark: mark, fbaCartonMark: `FBA-${mark}`, destinationFbaWarehouseCode: candidate.destinationFbaWarehouseCode, operationType: 'CARTON_LABEL', skuItems: [] }] }
|
||||
request.items[0].shipmentNo = `SHIP-${mark}`
|
||||
const created = await api(merchant, 'POST', root, request)
|
||||
const no = created.application.relabelNo
|
||||
const scenario = { deliveryMethod, relabelNo: no, originalPallet: position.palletNo, checks: [] }
|
||||
report.scenarios.push(scenario)
|
||||
await reject(merchant, root, { ...request, idempotencyKey: key() }, /已用于换标单/)
|
||||
scenario.checks.push('DUPLICATE_APPLICATION_REJECTED')
|
||||
// Read an actual Print-generated test document from File, then upload it through the merchant API.
|
||||
const pdfReport = JSON.parse(await readFile('reports/relabel-closure-20260916/pdf.json', 'utf8'))
|
||||
const doc = pdfReport.files[0]
|
||||
const pdfResponse = await fetch(`http://127.0.0.1:9206/internal/file/v1/business-files/${process.env.OVERSEAS_TEST_CUSTOMER_ID}/${doc.category}/${pdfReport.businessId}/${doc.fileId}/content`, { headers: { 'X-Internal-Service-Token': process.env.INTERNAL_SERVICE_TOKEN } })
|
||||
const pdf = Buffer.from(await pdfResponse.arrayBuffer())
|
||||
assert(pdfResponse.ok && pdf.subarray(0, 5).toString() === '%PDF-', 'Test PDF not available')
|
||||
for (const scope of ['CARTON_TOTAL', 'SKU_TOTAL', 'ITEM']) {
|
||||
const form = new FormData(); form.append('file', new Blob([pdf], { type: 'application/pdf' }), `${marker}.pdf`)
|
||||
await api(merchant, 'POST', `${root}/${no}/files?scope=${scope}${scope === 'ITEM' ? `&itemId=${created.items[0].id}` : ''}`, form)
|
||||
}
|
||||
let task = await api(pda, 'POST', `${work}/${no}/claim`, { idempotencyKey: key() })
|
||||
const documents = await api(pda, 'GET', `${work}/${no}/documents`)
|
||||
assert(documents.length >= 3, 'PDA is missing uploaded label documents')
|
||||
for (const file of documents) {
|
||||
const response = await fetch(`${base}${work}/${no}/documents/${file.fileId}/download?scope=${file.scope}${file.itemId ? `&itemId=${file.itemId}` : ''}`, { headers: { Authorization: `Bearer ${pda}` } })
|
||||
const downloaded = Buffer.from(await response.arrayBuffer())
|
||||
assert(response.ok && createHash('sha256').update(downloaded).digest('hex') === createHash('sha256').update(pdf).digest('hex'), 'PDA document mismatch')
|
||||
}
|
||||
scenario.checks.push('MERCHANT_UPLOAD_PDA_DOWNLOAD_THREE_SCOPES')
|
||||
task = await api(pda, 'POST', `${work}/${no}/cartons/scan`, { cartonMark: mark, expectedVersion: task.version, idempotencyKey: key() })
|
||||
const photoForm = new FormData(); photoForm.append('file', new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64')], { type: 'image/png' }), 'synthetic.png')
|
||||
const photo = await api(pda, 'POST', `${work}/${no}/photos/files`, photoForm)
|
||||
const complete = () => ({ photoFileIds: [photo.fileId], expectedVersion: task.version, remark: marker, idempotencyKey: key() })
|
||||
await reject(pda, `${work}/${no}/complete`, complete(), /托盘绑定/)
|
||||
scenario.checks.push('UNBOUND_COMPLETION_REJECTED')
|
||||
const first = await api(pda, 'POST', `${pallets}/relabel-pallet-label`, { relabelNo: no, idempotencyKey: key() })
|
||||
await move(no, mark, position.palletNo, first.palletCode)
|
||||
const second = await api(pda, 'POST', `${pallets}/relabel-pallet-label`, { relabelNo: no, idempotencyKey: key() })
|
||||
await move(no, mark, first.palletCode, second.palletCode)
|
||||
scenario.targetPallet = second.palletCode
|
||||
scenario.checks.push('REBIND_STOCK_CONSERVED_AND_REPLAY_SAFE')
|
||||
task = await api(pda, 'POST', `${work}/${no}/complete`, complete())
|
||||
if (deliveryMethod === 'TRUCK_PICKUP') {
|
||||
task = await api(admin, 'POST', `/api/platform/v1/last-leg/relabel-applications/${no}/rework`, { decision: 'REWORK', expectedVersion: task.version, idempotencyKey: key(), remark: '验收退回:请核对并重新贴标' })
|
||||
assert(task.reviewRemark?.includes('验收退回'), 'Rework remark missing on PDA')
|
||||
task = await api(pda, 'POST', `${work}/${no}/cartons/scan`, { cartonMark: mark, expectedVersion: task.version, idempotencyKey: key() })
|
||||
await reject(pda, `${work}/${no}/complete`, complete(), /托盘绑定/)
|
||||
await move(no, mark, second.palletCode, first.palletCode)
|
||||
scenario.targetPallet = first.palletCode
|
||||
task = await api(pda, 'POST', `${work}/${no}/complete`, complete())
|
||||
scenario.checks.push('REWORK_RESCAN_REBIND_AND_RESUBMIT')
|
||||
}
|
||||
const confirmed = await api(merchant, 'POST', `${root}/${no}/confirm-completion`, { idempotencyKey: key(), remark: marker })
|
||||
scenario.outboundNo = confirmed.application.outboundOrderNo
|
||||
assert(scenario.outboundNo, 'Customer confirmation did not create direct outbound')
|
||||
await reject(pda, `${pallets}/relabel-disassemble`, { relabelNo: no, source: { palletCode: scenario.targetPallet, expectedVersion: 0 }, target: { palletCode: position.palletNo, expectedVersion: 0 }, cartonMarks: [mark], idempotencyKey: key() }, /排车或出库/)
|
||||
const resolved = await api(pda, 'GET', `/api/pda/v1/last-leg/outbound-tasks/resolve/${scenario.outboundNo}`)
|
||||
const picked = await api(pda, 'POST', `/api/pda/v1/last-leg/outbound-tasks/${resolved.taskNo}/pallets/${scenario.targetPallet}/pick`, { idempotencyKey: key() })
|
||||
await api(pda, 'POST', `/api/pda/v1/last-leg/outbound-tasks/${resolved.taskNo}/complete`, { idempotencyKey: key() })
|
||||
const outbound = await api(admin, 'GET', `/api/platform/v1/last-leg/outbound-orders/${scenario.outboundNo}?page=1&size=20`)
|
||||
assert(outbound.outbound.status === 'OUTBOUNDED', 'Direct outbound not completed')
|
||||
scenario.outboundStatus = outbound.outbound.status
|
||||
scenario.checks.push('DIRECT_OUTBOUND_COMPLETED', 'OUTBOUND_REBIND_REJECTED')
|
||||
}
|
||||
report.passed = true
|
||||
} catch (error) { report.error = error.message; process.exitCode = 1 }
|
||||
finally {
|
||||
await mkdir('reports/relabel-closure-20260916', { recursive: true })
|
||||
await writeFile('reports/relabel-closure-20260916/extra.json', JSON.stringify(report, null, 2))
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
const token = process.env.INTERNAL_SERVICE_TOKEN
|
||||
if (!token) throw new Error('INTERNAL_SERVICE_TOKEN is required')
|
||||
const printBase = process.env.PRINT_SERVICE_URL || 'http://127.0.0.1:9207'
|
||||
const fileBase = process.env.FILE_SERVICE_URL || 'http://127.0.0.1:9206'
|
||||
for (const url of [printBase, fileBase]) if (!['localhost', '127.0.0.1'].includes(new URL(url).hostname)) throw new Error('Local services only')
|
||||
const customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID
|
||||
if (!customerId) throw new Error('OVERSEAS_TEST_CUSTOMER_ID is required')
|
||||
const businessId = String(Date.now())
|
||||
const headers = { 'X-Internal-Service-Token': token, 'X-Operator-Id': '1' }
|
||||
const report = { scope: 'LOCAL_PRINT_AND_FILE_HTTP_ONLY', businessId, passed: false, files: [] }
|
||||
const ensure = (ok, message) => { if (!ok) throw new Error(message) }
|
||||
try {
|
||||
const rendered = await fetch(`${printBase}/internal/print/v1/pdfs/inbound-marks`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ templateCode: 'INBOUND_MARK_V1', pageSpec: { widthMm: 100, heightMm: 100 }, downloadFileName: 'relabel-acceptance.pdf', labels: [{ topCode: 'RELABEL-ACCEPTANCE', warehouseText: 'ONT8', cartonText: 'CTNS:1-1', routeText: '验收测试标签', qrContent: `ACCEPTANCE-${businessId}` }] }) })
|
||||
const pdf = Buffer.from(await rendered.arrayBuffer())
|
||||
ensure(rendered.ok && pdf.subarray(0, 5).toString() === '%PDF-', `Print failed: HTTP ${rendered.status}`)
|
||||
report.pdfBytes = pdf.length
|
||||
const hash = value => createHash('sha256').update(value).digest('hex')
|
||||
for (const category of ['OVERSEAS_RELABEL_CARTON_TOTAL_DOCUMENT', 'OVERSEAS_RELABEL_SKU_TOTAL_DOCUMENT', 'OVERSEAS_RELABEL_ITEM_DOCUMENT']) {
|
||||
const form = new FormData(); form.append('file', new Blob([pdf], { type: 'application/pdf' }), 'relabel-acceptance.pdf')
|
||||
const path = `/internal/file/v1/business-files/${customerId}/${category}/${businessId}`
|
||||
const upload = await fetch(fileBase + path, { method: 'POST', headers, body: form })
|
||||
const body = await upload.json()
|
||||
ensure(upload.ok && String(body.code) === '200', `${category}: ${body.code} ${body.message}`)
|
||||
const fileId = body.data.fileId
|
||||
const downloaded = await fetch(`${fileBase}${path}/${fileId}/content`, { headers })
|
||||
const bytes = Buffer.from(await downloaded.arrayBuffer())
|
||||
ensure(downloaded.ok && hash(bytes) === hash(pdf), 'Downloaded PDF does not match uploaded bytes')
|
||||
const wrongOwner = await fetch(`${fileBase}/internal/file/v1/business-files/1/${category}/${businessId}/${fileId}`, { headers })
|
||||
const denied = await wrongOwner.json()
|
||||
ensure(!wrongOwner.ok || String(denied.code) !== '200', 'File was visible across customers')
|
||||
report.files.push({ category, fileId, sha256: hash(bytes), ownershipCheck: 'PASS' })
|
||||
}
|
||||
report.passed = true
|
||||
} catch (error) { report.error = error.message; process.exitCode = 1 }
|
||||
finally {
|
||||
await mkdir('reports/relabel-closure-20260916', { recursive: true })
|
||||
await writeFile('reports/relabel-closure-20260916/pdf.json', JSON.stringify(report, null, 2))
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
}
|
||||
Reference in New Issue
Block a user