贴标申请,仓储转运

This commit is contained in:
2026-10-12 11:18:58 +08:00
parent 29b562ebf5
commit b4b00ae3dd
39 changed files with 990 additions and 315 deletions
@@ -3563,3 +3563,28 @@ CREATE TABLE IF NOT EXISTS ll_appointment_impact_history (
CONSTRAINT fk_appointment_impact_history FOREIGN KEY (container_booking_id) REFERENCES ll_appointment_impact(container_booking_id),
CONSTRAINT chk_appointment_impact_action CHECK (action IN ('OBSERVED','CLAIM','RECORD','RESOLVE'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='船期观察及人工处理追加历史';
-- 退货以包裹/箱为最小出库实物单位;部分数量必须先合箱生成新标签箱。
-- 可重复执行,目标库 baiyue_lastleg。
USE baiyue_lastleg;
-- 合箱后的新箱与原包裹共用同一箱唛、标签和来源追溯模型。
-- 流水约束允许部分合箱/拆分只扣件数,不虚减箱数。
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.table_constraints
WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement'
AND constraint_name='chk_ll_return_outbound_movement_delta'),
'ALTER TABLE ll_return_inventory_outbound_movement DROP CHECK chk_ll_return_outbound_movement_delta, ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)',
'ALTER TABLE ll_return_inventory_outbound_movement ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 同一包裹可分批合箱/出库,重复提交仍由 outbound_pick_id + SKU 幂等约束保护。
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement'
AND index_name='uk_ll_return_outbound_carton_sku'),
'ALTER TABLE ll_return_inventory_outbound_movement DROP INDEX uk_ll_return_outbound_carton_sku, ADD INDEX idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code)',
'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 新签收及新合箱必须打印并扫码确认标签;既有实物按原状态继续作业。
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='label_status'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN label_status VARCHAR(32) NULL COMMENT ''WAIT_LABEL待贴标 CONFIRMED已扫码确认标签''');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -824,8 +824,8 @@ paths:
responses: { '200': { description: 出库单分页 } }
/api/platform/v1/last-leg/outbound-orders/return:
post:
summary: 创建退货出库单,支持多个散件SKU分别填写出库数量
description: 权限 platform:overseas-warehouse:outbound:create;散件SKU来源仅允许PARCEL可用库存,不包含箱内库存;实体箱按箱或托盘出库。items逐来源指定SKU及数量,与cartonMarks一一对应,不可同时提交顶层skuCode或quantity,不支持PALLET方式。多个SKU生成同一张出库单和派送单,PDA逐SKU按件数拣货。旧版skuCode与quantity继续支持单SKU来源排序分配。未拣货前占用整个使用来源,拣货后来源余数恢复可用。相同幂等键改数量拒绝。
summary: 按包裹或箱标签创建整实物退货出库单
description: 权限 platform:overseas-warehouse:outbound:create;以包裹或箱为最小单位,cartonMarks为实际标签码,PARCEL可包含多个SKU,整包裹/箱或整托出库。新签收和新合箱须先扫码确认贴标。部分包裹及部分箱均须通过合箱生成独立新标签箱后才能出库。提交skuCode、quantity或非空items直接拒绝,不修改库存。来源合箱占用或尚未清点时拒绝,PDA按实物标签扣全部内容,同键重放不重复扣减。
security: [{ bearerAuth: [] }]
parameters: [{ name: customerId, in: query, required: true, schema: { type: string } }]
requestBody:
@@ -840,20 +840,6 @@ paths:
overseasWarehouseId: { type: string, maxLength: 128 }
returnNo: { type: string, maxLength: 128 }
cartonMarks: { type: array, minItems: 1, maxItems: 200, uniqueItems: true, items: { type: string, maxLength: 128 } }
skuCode: { type: string, maxLength: 128, nullable: true }
quantity: { type: integer, format: int64, minimum: 1, nullable: true, description: 单SKU计划件数,不得超过所选来源总数;省略出全部,只允许skuCode已填且非PALLET维度,不接受小数 }
items:
type: array
nullable: true
minItems: 1
maxItems: 200
items:
type: object
required: [cartonMark, skuCode, quantity]
properties:
cartonMark: { type: string, maxLength: 128, description: 散件库存来源标识,非实体箱号 }
skuCode: { type: string, maxLength: 128 }
quantity: { type: integer, format: int64, minimum: 1, description: 本来源本次出库件数,不接受小数,不得超过可用数量 }
unitType: { type: string, enum: [BOX, PALLET], nullable: true }
remark: { type: string, maxLength: 1000, nullable: true }
idempotencyKey: { type: string, maxLength: 255 }
@@ -1764,7 +1750,7 @@ paths:
/api/merchant/v1/last-leg/return-orders/{returnNo}:
get:
summary: 客户查询退货订单与签收批次
description: 权限 merchant:overseas-warehouse:return-order:list;返回各 SKU 的预报和实收数量,以及每次 PDA 签收产生的批次号和可选快递单号。cartons 数组包含 receiptNo、palletCode、cartonMark、countStatus、countedAt、items(skuCode、productNameZh、quantity、boxCount)。CONFIRMED 为签收或清点时不可变箱内快照;PENDING 的 items 为空,不返回预分配平均数;UNRECORDED 表示历史未留存箱内快照,不能以已变化库存冒充签收数量。包裹散件不生成实物箱明细。
description: 权限 merchant:overseas-warehouse:return-order:list;返回各 SKU 的预报和实收数量,以及每次 PDA 签收产生的批次号和可选快递单号。cartons 数组包含 receiptNo、palletCode、cartonMark、countStatus、countedAt、items(skuCode、productNameZh、quantity、boxCount)。CONFIRMED 为签收或清点时不可变箱内快照;PENDING 的 items 为空,不返回预分配平均数;UNRECORDED 表示历史未留存箱内快照,不能以已变化库存冒充签收数量。包裹作为一个独立实物明细,保留同包裹全部SKU及可打印标签。
security: [{ bearerAuth: [] }]
parameters: [{ name: returnNo, in: path, required: true, schema: { type: string } }]
responses: { '200': { description: 当前客户退货订单详情 }, '404': { description: 当前客户下退货订单不存在 } }
@@ -1851,11 +1837,11 @@ paths:
- { name: returnNo, in: query, schema: { type: string } }
- { name: status, in: query, schema: { type: string } }
responses:
'200': { description: 分页 records 每箱一行,包含 cartonMark、overseasWarehouseId、sourceBatchNo、sourceReturnNo、status、skuTypes、totalQuantity、length、width、height、weight、createdAt;total 为实际箱数 }
'200': { description: 分页 records 每箱一行,包含 cartonMark、overseasWarehouseId、sourceBatchNo、sourceReturnNo、status、skuTypes、totalQuantity、length、width、height、weight、createdAt、relabelStatus、relabelledQuantity、relabelReservedQuantity;total 为实际箱数。换标状态取实际当前或最新轮次事实,申请占用为未完成申请件数,两者不从可用库存差值推算 }
/api/merchant/v1/last-leg/return-inventory/cartons/{cartonMark}:
get:
summary: 客户查询退货箱明细
description: 仅返回当前客户的箱号、签收批次和箱内 SKU 数量。
description: 仅返回当前客户的箱号、签收批次和箱内 SKU 数量;relabelStatus 为 UNLABELLED/PROCESSING/PARTIALLY_COMPLETED/COMPLETED,relabelledQuantity 为当前或最新轮次实际商品换标件数,relabelReservedQuantity 为未完成申请件数。items.availableQuantity 为实物可用库存,历史换标不会永久扣减下一轮可申请量。
security: [{ bearerAuth: [] }]
parameters: [{ name: cartonMark, in: path, required: true, schema: { type: string, maxLength: 128 } }]
responses: { '200': { description: 当前客户退货箱明细 }, '404': { description: 当前客户下退货箱不存在 } }
@@ -3118,6 +3104,10 @@ components:
RelabelInventoryCandidate:
type: object
properties:
relabelStatus: { type: string, nullable: true, enum: [UNLABELLED, PROCESSING, PARTIALLY_COMPLETED, COMPLETED], description: 退货实物按实际完成件数和未完成申请占用计算 }
relabelledPackages: { type: integer, format: int64, description: 退货实际已完成商品换标件数,不从可用库存差值推导 }
relabelledQuantity: { type: integer, format: int64, minimum: 0 }
relabelReservedQuantity: { type: integer, format: int64, minimum: 0, description: 未完成申请件数,取消或完成后释放 }
inventoryBalanceId: { type: string }
inboundOrderId: { type: string }
inboundOrderNo: { type: string }
@@ -3132,7 +3122,7 @@ components:
availablePackages: { type: integer, format: int64 }
version: { type: integer, format: int64 }
positions: { type: array, items: { $ref: '#/components/schemas/RelabelInventoryPosition' } }
sourceType: { type: string, enum: [INBOUND, RETURN_CARTON] }
sourceType: { type: string, enum: [INBOUND, RETURN_CARTON, RETURN_PARCEL] }
returnCartonId: { type: string, nullable: true, description: 退货合箱库存主键;sourceType=RETURN_CARTON 时提供 }
cartonMark: { type: string, nullable: true, description: 仓库合箱后打印并贴箱的仓库唛头 }
skuItems:
@@ -272,11 +272,82 @@ paths:
itemId: { type: string, pattern: '^[0-9]+$' }
skuIndex: { type: integer, minimum: 0 }
oldSku: { type: string, maxLength: 255 }
cartonMark: { type: string, maxLength: 128, description: 退货SKU作业必填,须扫描当前实体来源包裹或箱 }
newSkuCode: { type: string, maxLength: 128, description: 退货SKU作业必填,须与批准的目标标签一致 }
quantity: { type: integer, minimum: 1, description: 退货SKU作业必填,须与本明细批准件数一致 }
expectedVersion: { type: integer, minimum: 0 }
idempotencyKey: { type: string, maxLength: 255 }
responses:
'200': { description: 更新后的任务, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelDetailResponse' } } } }
'409': { description: 已处理、映射不符、认领人或版本冲突 }
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/return-skus/complete:
post:
summary: 扫描来源包裹或箱及SKU登记退货按件换标实际完成量
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [itemId, quantity, expectedVersion, idempotencyKey, cartonMark, skuCode]
properties:
itemId: { type: string, pattern: '^[0-9]+$' }
quantity: { type: integer, minimum: 1 }
expectedVersion: { type: integer, minimum: 0 }
idempotencyKey: { type: string, maxLength: 255 }
cartonMark: { type: string, maxLength: 128 }
skuCode: { type: string, maxLength: 128 }
responses: { '200': { description: 更新后Detail;同键同请求重放 }, '409': { description: 来源、SKU、剩余件数或版本冲突 } }
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/return-skus/complete-total:
post:
summary: 单一来源的退货SKU按件完成
description: 同SKU涉及多个来源包裹或箱时拒绝自动分配,必须逐来源扫码。
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [skuCode, quantity, expectedVersion, idempotencyKey]
properties:
skuCode: { type: string }
quantity: { type: integer, minimum: 1 }
expectedVersion: { type: integer, minimum: 0 }
idempotencyKey: { type: string }
responses: { '200': { description: 更新后Detail }, '409': { description: 多来源或数量冲突 } }
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/return-cartons/pack:
post:
summary: 将本任务已完成的退货SKU件数隔离合箱为独立新箱
description: 必须由认领人在PROCESSING阶段完成所有申请明细后执行;原包裹余量守恒,保留跨退货单追溯。响应Detail的items切换为新箱,history的RETURN_PACK.detail JSON含round、sourceCartonMarks、sourceDeductions、cartonMark、quantity和label.printableHtml。标签为仓库识别标签,与商品换标资料分开。同键重放原结果。
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [sourceCartonMarks, idempotencyKey, length, width, height, weight]
properties:
sourceCartonMarks: { type: array, minItems: 1, maxItems: 100, items: { type: string } }
targetCartonMark: { type: string, nullable: true, description: 必须为空,由系统生成独立新箱 }
idempotencyKey: { type: string, maxLength: 255 }
sourceDeductions:
type: array
description: 可省略由服务端生成;填写时按来源箱和SKU汇总必须与本任务完成明细一致
items:
type: object
required: [cartonMark, skuCode, quantity]
properties:
cartonMark: { type: string }
skuCode: { type: string }
quantity: { type: integer, format: int64, minimum: 1 }
length: { type: number, exclusiveMinimum: true, minimum: 0 }
width: { type: number, exclusiveMinimum: true, minimum: 0 }
height: { type: number, exclusiveMinimum: true, minimum: 0 }
weight: { type: number, exclusiveMinimum: true, minimum: 0 }
responses: { '200': { description: 更新后Detail含新箱识别标签 }, '409': { description: 未完成申请、来源变动或数量冲突 } }
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/units/skip:
post:
summary: 登记一件有原因的跳过事实
@@ -751,6 +822,8 @@ components:
type: object
required: [relabelNo, applicationStatus, operationType, taskStatus, expectedPackages, relabelledPackages, skippedPackages, exceptionPackages, version, completionPhotoFileIds, items, unitFacts, history]
properties:
deliveryMethod: { type: string, nullable: true, description: 申请真实运输方式,留仓为空 }
requiresPalletBinding: { type: boolean, description: 退货任务仅TRUCK为true,留仓及快递为false }
relabelNo: { type: string }
customerOrderNo: { type: string, nullable: true }
labelDeadline: { type: string, format: date-time, nullable: true }
@@ -791,7 +864,7 @@ components:
relabelFileUrl: { type: string, nullable: true }
cartonMark: { type: string, nullable: true, description: 我方箱唛 }
fbaCartonMark: { type: string, nullable: true, description: 新FBA箱唛 }
operationType: { type: string, enum: [CARTON_LABEL, SKU_LABEL, CARTON_AND_SKU] }
operationType: { type: string, enum: [CARTON_LABEL, SKU_LABEL, CARTON_AND_SKU, RETURN_BULK_SKU_LABEL] }
skuItems: { type: array, items: { $ref: '#/components/schemas/RelabelSkuItem' } }
expectedPackages: { type: integer }
relabelledPackages: { type: integer }
@@ -799,6 +872,8 @@ components:
exceptionPackages: { type: integer }
cartonProcessedPackages: { type: integer, description: 本轮箱唛作业已完成箱数,混合单独立计数 }
skuProcessedPackages: { type: integer, description: 本轮SKU作业已完成箱数,两项均完成才增加整箱已换标数 }
returnCartonId: { type: string, nullable: true, description: 真实退货包裹或箱ID }
returnQuantity: { type: integer, format: int64, minimum: 0, description: 当前实物包裹或箱全部件数;不是申请量 }
RelabelSkuItem:
type: object
required: [skuCode, newSkuCode, quantity]
@@ -876,7 +951,7 @@ components:
inventoryPositionId: { type: string, nullable: true, pattern: '^[0-9]+$', description: 普通库存使用真实仓位键;退货箱库存没有该仓位,提交 null,不提交空字符串或退货库存内部ID }
packages: { type: integer, format: int64, minimum: 1 }
productQuantity: { type: integer, format: int64, minimum: 1, nullable: true, description: 单SKU退货明细存在returnSkuQuantity时必填且必须等于计划件数;不接受小数。历史无快照明细可省略 }
skuCode: { type: string, maxLength: 128, nullable: true, description: PARCEL散件单SKU扫码值;已装箱来源必须扫描箱唛 }
skuCode: { type: string, maxLength: 128, nullable: true, description: 仅遗留已创建SKU任务使用;新退货任务必须扫描包裹或箱标签 }
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
cartonMarks:
type: array
@@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: 退货合箱计划与箱级上架
version: 1.0.0
description: 散SKU查询客户全部同仓PARCEL可用库存;箱合并精确指定来源箱唛并扫描已有目标箱。托盘仅为可选载体。合箱不新增订单货量。
description: 包裹可包含多个SKU并拥有独立标签;整包裹/箱可直接出库,部分数量须精确选择来源包裹/箱合箱成新标签箱。托盘仅为可选载体。合箱不新增订单货量。
security: [{ bearerAuth: [] }]
paths:
/api/platform/v1/last-leg/return-packing-plans:
@@ -17,7 +17,7 @@ paths:
responses: { '200': { description: 业务响应信封 code为200成功,A0400/A0404/A0409校验或状态错误,requestId及traceId保留 }, '403': { description: 权限或角色不符 } }
post:
summary: 创建并锁定来源库存
description: return-inventory:list + lastleg:inventory:manage;同客户同仓已签收订单,BOX精确锁来源箱全部SKU,SKU只锁散件指定数量。
description: return-inventory:list + lastleg:inventory:manage;同客户同仓已签收订单,BOX精确锁来源箱全部SKU,SKU锁定所选包裹/箱的指定数量,来源实物在计划完成或取消前不可出库。
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Create' } } } }
responses: { '200': { description: 业务响应信封 code为200成功,A0400/A0404/A0409校验或状态错误,requestId及traceId保留 }, '403': { description: 权限或角色不符 } }
/api/platform/v1/last-leg/return-packing-plans/{planNo}/review:
@@ -46,16 +46,22 @@ paths:
/api/pda/v1/last-leg/return-inventory/packing/plans/{planNo}/complete:
post:
summary: 多箱分配并完成计划
description: SKU目标总量须等于锁定量。BOX先选择新增目标箱或扫描已有目标箱,sourceDeductions逐来源箱及SKU指定实际扣除量(不超过锁定量),未使用预占释放;目标原库存不扣减,新箱须填写箱规并生成系统箱唛。全事务、同载荷重放标签快照。
description: SKU目标总量须等于锁定量。SKU仅支持新增目标箱;BOX整箱可扫描已有目标箱,部分扣除仅支持新增目标箱,sourceDeductions逐来源箱及SKU指定实际扣除量(不超过锁定量),未使用预占释放;目标原库存不扣减,新箱须填写箱规并生成系统箱唛。全事务、同载荷重放标签快照。
parameters: [{ name: planNo, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Complete' } } } }
responses: { '200': { description: 业务响应信封 code为200成功,A0400/A0404/A0409校验或状态错误,requestId及traceId保留 }, '403': { description: 权限或角色不符 } }
/api/pda/v1/last-leg/return-inventory/cartons/{cartonMark}:
get:
summary: 扫描可用来源或目标箱
description: 仅同仓AVAILABLE实物箱,PARCEL不作为箱选择。
description: 仅同仓AVAILABLE包裹或箱,返回其全部SKU明细。
parameters: [{ name: cartonMark, in: path, required: true, schema: { type: string } }]
responses: { '200': { description: 业务响应信封 code为200成功,A0400/A0404/A0409校验或状态错误,requestId及traceId保留 }, '403': { description: 权限或角色不符 } }
/api/pda/v1/last-leg/return-inventory/cartons/{cartonMark}/confirm-label:
post:
summary: 扫描已贴好的包裹或箱标签
description: 绑定仓且AVAILABLE实物,幂等确认仓库识别标签;整包裹或箱无需确认此标签也可创建出库单。此操作不登记商品换标完成量。
parameters: [{ name: cartonMark, in: path, required: true, schema: { type: string } }]
responses: { '200': { description: 标签快照及业务响应 }, '403': { description: 权限或角色不符 } }
/api/pda/v1/last-leg/return-inventory/cartons/merge-boxes:
post:
summary: 来源箱并入已有目标箱
@@ -112,6 +118,7 @@ components:
returnNos: { type: array, minItems: 1, maxItems: 100, items: { type: string } }
mode: { type: string, enum: [SKU, BOX] }
sourceCartonMarks: { type: array, maxItems: 500, items: { type: string } }
sourceDeductions: { type: array, maxItems: 1000, items: { $ref: '#/components/schemas/SourceDeduction' }, description: 部分合箱精确来源及数量,与目标SKU合计一致 }
boxes: { type: array, minItems: 1, maxItems: 100, items: { $ref: '#/components/schemas/Box' } }
remark: { type: string, maxLength: 1000 }
idempotencyKey: { type: string, maxLength: 255 }