fix(lastleg): repair dispatch cancellation and return execution contracts

This commit is contained in:
2026-10-18 16:34:14 +08:00
parent 66a0beb653
commit aa33514ec6
64 changed files with 2061 additions and 213 deletions
@@ -2927,16 +2927,53 @@ CREATE TABLE IF NOT EXISTS ll_exception_order (
CREATE TABLE IF NOT EXISTS ll_exception_return_item (
id BIGINT NOT NULL COMMENT '异常退回明细ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
exception_order_id BIGINT NOT NULL COMMENT '异常订单ID', outbound_pallet_item_id BIGINT NOT NULL COMMENT '原出库托盘明细ID',
inventory_position_id BIGINT NOT NULL COMMENT '原出库库存位置ID', pallet_no VARCHAR(128) NULL COMMENT '原出库托盘码快照',
inventory_position_id BIGINT NULL COMMENT '普通原出库库存位置;与退货原箱互斥',
return_carton_id BIGINT NULL COMMENT '原生退货箱标识;与普通库存位置互斥',
return_carton_mark VARCHAR(128) NULL COMMENT '原生退货箱业务箱码快照',
pallet_no VARCHAR(128) NULL COMMENT '原出库托盘码快照',
sku_code VARCHAR(128) NULL COMMENT '原出库SKU快照', max_return_packages BIGINT NOT NULL COMMENT '原出库可退箱数上限',
approved_return_packages BIGINT NOT NULL COMMENT '批准退回箱数', created_at DATETIME(3) NOT NULL COMMENT '创建时间',
received_packages BIGINT NOT NULL DEFAULT 0 COMMENT '已实际退回入仓的累计箱数',
CONSTRAINT chk_exception_return_received CHECK(received_packages>=0 AND received_packages<=approved_return_packages),
CONSTRAINT chk_exception_return_source CHECK((inventory_position_id IS NOT NULL AND return_carton_id IS NULL) OR (inventory_position_id IS NULL AND return_carton_id IS NOT NULL)),
PRIMARY KEY(id), UNIQUE KEY uk_ll_exception_return_source(customer_id,exception_order_id,outbound_pallet_item_id,inventory_position_id),
UNIQUE KEY uk_ll_exception_return_carton(customer_id,exception_order_id,outbound_pallet_item_id,return_carton_id),
KEY idx_ll_exception_return_order(customer_id,exception_order_id)
) ENGINE=InnoDB COMMENT='异常退回原出库库存冻结明细';
CREATE TABLE IF NOT EXISTS ll_return_carton_stock_receipt (
id BIGINT NOT NULL COMMENT '原箱回仓凭据明细标识',
customer_id VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户数据隔离键',
receipt_no VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原箱回仓实收业务号',
exception_order_id BIGINT NOT NULL COMMENT '本次异常退回订单',
exception_return_item_id BIGINT NOT NULL COMMENT '本次冻结的原箱来源明细',
delivery_order_id BIGINT NOT NULL COMMENT '原运输派送单',
source_pick_id BIGINT NOT NULL COMMENT '原箱实际出库拣货凭据',
source_outbound_no VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原出库单业务号',
source_dispatch_item_id BIGINT NOT NULL COMMENT '原排车货物明细',
source_outbound_pallet_item_id BIGINT NOT NULL COMMENT '原出库托盘明细',
return_carton_id BIGINT NOT NULL COMMENT '真实退货原箱标识',
carton_mark VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原箱业务箱码快照',
overseas_warehouse_id VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '实际回仓海外仓标识',
target_pallet_id BIGINT NOT NULL COMMENT '实际回仓目标实物托盘',
target_pallet_no VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '实际目标托盘业务码',
target_location_id BIGINT NOT NULL COMMENT '实际回仓目标库位',
target_location_code VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '实际目标库位业务码',
sku_code VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原出库快照中的商品编码',
product_name_zh VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '原出库快照中的中文品名',
quantity BIGINT NOT NULL COMMENT '本次原SKU实际恢复件数',
packages BIGINT NOT NULL COMMENT '本箱仅首条SKU记录计一箱其余为零',
evidence_file_ids_json JSON NOT NULL COMMENT '本次实收证据文件引用',
received_by VARCHAR(128) NOT NULL COMMENT '本次原箱实收确认操作人',
received_at DATETIME(3) NOT NULL COMMENT '实际原箱回仓入账时间',
PRIMARY KEY(id),
UNIQUE KEY uk_return_carton_receipt_source(customer_id,source_pick_id,sku_code),
KEY idx_return_carton_receipt_exception(customer_id,exception_order_id,received_at),
KEY idx_return_carton_receipt_delivery(customer_id,delivery_order_id),
CONSTRAINT chk_return_carton_receipt_quantity CHECK(quantity>0 AND packages IN (0,1))
) ENGINE=InnoDB COMMENT='原生退货箱按原出库SKU实际回仓的不可变凭据';
CREATE TABLE IF NOT EXISTS ll_exception_order_command (
id BIGINT NOT NULL COMMENT '异常处理命令ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
exception_order_id BIGINT NOT NULL COMMENT '异常订单ID', action VARCHAR(32) NOT NULL COMMENT '异常处理动作',
@@ -167,7 +167,7 @@ paths:
parameters: [{$ref: '#/components/parameters/TripNo'}, {$ref: '#/components/parameters/BookingNo'}]
post:
summary: 运营确认委托方实收货量与运费
description: '权限:platform:fleet:trip:booking-confirm;实际来货已完成测量,receiptHash和实托集合一致,重新核算容量和约定金额,引用真实收货证据并确认原来货流程;不重复入库。差异必须显式确认,超容量回滚。'
description: '权限:platform:fleet:trip:booking-confirm;CONFIRMED或尚未绑定排车的RECEIVED可核对。实际来货已完成测量,receiptHash和实托集合一致,重新核算容量和约定金额,引用真实收货证据并确认原来货流程;不重复入库。拆合托后先解除排车关联,再以新请求键及当前版本重新确认实收和运费,完整实收量与承诺车次限制不放宽。差异必须显式确认,超容量回滚。'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/BookingAcceptReceipt'}}}}
responses: {'200': {$ref: '#/components/responses/BookingResult'}}
/api/platform/v1/last-leg/fleet/trips/{tripNo}/bookings/{bookingNo}/dispatch:
@@ -450,6 +450,8 @@ components:
properties:
warehousePalletId: {type: string}
palletSpaces: {type: integer, minimum: 1}
weightKg: {type: number, nullable: true, minimum: 0.001, description: 当前组成缺少可信测量时必填,为该票外接货实测毛重,不填整托其他来源重量}
volumeCbm: {type: number, nullable: true, minimum: 0.001, description: 当前组成变化后重新实测的该票货体积;不沿用旧托盘或计费材积重}
BookingAcceptReceipt:
allOf:
- {$ref: '#/components/schemas/BookingCommand'}
@@ -1044,7 +1044,7 @@ paths:
/api/platform/v1/last-leg/exception-orders/{exceptionNo}/complete:
post:
summary: 完成异常处理并原子恢复退回库存
description: 权限 platform:overseas-warehouse:exception-order:process;退回海外仓支持分批实收,每批使用新幂等键及当前版本,累计不得超过批准量。库存 RETURN_STOCK_IN 成功后累计实收,未收齐保留 PROCESSING,批准量收齐才转 COMPLETED。详情 returnSources 只返回剩余可收数量。派送仅在累计实际退回覆盖整单实发数量时写 RETURNED 和 RETURN_COMPLETED;非海外仓异常结案不证明货物交接完成。原 signedAt 保留,车次及资源不由工单关闭释放。
description: 权限 platform:overseas-warehouse:exception-order:process;退回海外仓支持分批实收,每批使用新幂等键及当前版本,累计不得超过批准量。普通库存 RETURN_STOCK_IN 或原箱不可变回仓凭据成功落账后累计实收,未收齐保留 PROCESSING,批准量收齐才转 COMPLETED。详情 returnSources 只返回剩余可收数量。派送仅在累计实际退回覆盖整单实发数量时写 RETURNED 和 RETURN_COMPLETED;非海外仓异常结案不证明货物交接完成。原 signedAt 保留,车次及资源不由工单关闭释放。
security: [{ bearerAuth: [] }]
parameters: [{ name: exceptionNo, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ExceptionComplete' } } } }
@@ -1065,11 +1065,11 @@ paths:
responses: { '200': { description: 关联关系 } }
/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/return-inbound-options:
get:
summary: 查询派送退回候选入仓单
description: 权限 platform:overseas-warehouse:exception-order:list;返回按原出库拣货事实解析的 returnSources,页面不得自由填写 SKU 或猜测库存位置。
summary: 查询派送退回候选主单
description: 权限 platform:overseas-warehouse:exception-order:list;每个候选以lastMileOrderId定位、lastMileOrderNo展示;inboundOrderId和inboundCode可空,换标主单不要求普通入仓单。返回按原出库拣货事实解析的 returnSources,页面不得自由填写 SKU 或猜测库存位置。每条来源返回outboundPalletItemId、inventoryPositionId、returnCartonId、cartonMark、palletNo、skuCode和maxReturnPackages;普通仓位与原箱互斥,箱码用于展示,内部标识仅作提交键。
security: [{ bearerAuth: [] }]
parameters: [{ name: deliveryNo, in: path, required: true, schema: { type: string } }]
responses: { '200': { description: 退回候选入仓单 } }
responses: { '200': { description: 退回候选主单及真实出库来源 } }
/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/mark-return:
post:
summary: 将派送单标记为退回入仓
@@ -1081,7 +1081,7 @@ paths:
/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/status:
put:
summary: 更新派送状态
description: 权限 platform:overseas-warehouse:delivery:status。人工只允许 WAIT_DELIVERY→DELIVERING/REJECTED/CANCELLED、DELIVERING→REJECTED;DELIVERED 仅由签收资料保存成功自动触发,同状态幂等,终态不可倒退。
description: 权限 platform:overseas-warehouse:delivery:status。人工只允许实际出库后的 WAIT_DELIVERY→DELIVERING/REJECTED、DELIVERING→REJECTED;取消仅允许经未执行出库的聚合取消命令;DELIVERED 仅由签收资料保存成功自动触发,同状态幂等,终态不可倒退。
security: [{ bearerAuth: [] }]
parameters: [{ name: deliveryNo, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DeliveryStatusChange' } } } }
@@ -2865,8 +2865,9 @@ components:
dispatch: { $ref: '#/components/schemas/DispatchResponse' }
SmartDispatchPreArrange:
type: object
description: planPalletIds与returnReceiptIds是两类正式来源,合计至少一项且最多500项。退回来源仅接受实际入账生成的实收记录,不得用退回实收ID冒充计划托盘ID;同一实物托盘不可重复选择。
description: 普通来源使用planPalletIds或returnReceiptIds,合计至少一项且最多500项。指定relabelNo时planPalletIds必须是本换标申请全部已绑定实物托盘ID,普通与退货换标都不允许部分选择或混选其他来源;换标选择暂不持有统一草稿锁,仅提交时在申请锁内完整复核。退回来源仅接受实际入账生成的实收记录;同一实物托盘不可重复选择。
properties:
relabelNo: { type: string, nullable: true, description: 整单换标申请业务号;不与退回实收来源混选 }
batchAllocations:
type: array
maxItems: 1000
@@ -2992,18 +2993,19 @@ components:
type: object
required: [status, expectedVersion]
properties:
status: { type: string, enum: [WAIT_DELIVERY, DELIVERING, REJECTED, CANCELLED] }
status: { type: string, enum: [WAIT_DELIVERY, DELIVERING, REJECTED] }
expectedVersion: { type: integer, format: int64, minimum: 0 }
remark: { type: string, nullable: true, maxLength: 500 }
DeliveryAllowedAction:
type: string
enum: [START_DELIVERY, REJECT_DELIVERY, CANCEL_DELIVERY, MARK_RETURN]
description: START/REJECT/CANCEL 使用通用 status 接口;MARK_RETURN 使用 mark-return 接口;派送完成由签收资料保存成功自动触发。
enum: [START_DELIVERY, REJECT_DELIVERY, MARK_RETURN]
description: START/REJECT 仅在关联出库已实际 OUTBOUNDED 后使用通用 status 接口;取消统一经未执行出库取消命令;MARK_RETURN 使用 mark-return 接口;派送完成由签收资料保存成功自动触发。
DeliveryMarkReturn:
type: object
required: [inboundOrderId, reason, returnDirection, items, idempotencyKey]
required: [lastMileOrderId, reason, returnDirection, items, idempotencyKey]
description: 按候选中真实尾程主单定位;普通及退货换标主单无需普通入仓单,不接受虚构入仓关系。
properties:
inboundOrderId: { type: string, pattern: '^[1-9][0-9]*$' }
lastMileOrderId: { type: string, pattern: '^[1-9][0-9]*$' }
reason: { type: string, minLength: 1, maxLength: 1000 }
returnDirection: { type: string, enum: [OVERSEAS_WAREHOUSE, CUSTOMER, OTHER] }
returnLogisticsNo: { type: string, nullable: true, maxLength: 128 }
@@ -3014,10 +3016,11 @@ components:
maxItems: 200
items:
type: object
required: [outboundPalletItemId, inventoryPositionId, packageCount]
required: [outboundPalletItemId, packageCount]
properties:
outboundPalletItemId: { type: string, pattern: '^[1-9][0-9]*$' }
inventoryPositionId: { type: string, pattern: '^[1-9][0-9]*$' }
inventoryPositionId: { type: string, nullable: true, pattern: '^[1-9][0-9]*$', description: 普通库存来源;与returnCartonId必须二选一 }
returnCartonId: { type: string, nullable: true, pattern: '^[1-9][0-9]*$', description: 原生退货箱来源;按整箱实收,不得虚构库存仓位 }
packageCount: { type: integer, format: int64, minimum: 1 }
ExceptionStartProcessing:
type: object
@@ -3046,10 +3049,11 @@ components:
maxItems: 200
items:
type: object
required: [outboundPalletItemId, inventoryPositionId, receivedPackages, targetPalletId, targetPalletNo, targetLocationId, targetLocationCode]
required: [outboundPalletItemId, receivedPackages, targetPalletId, targetPalletNo, targetLocationId, targetLocationCode]
properties:
outboundPalletItemId: { type: string, pattern: '^[1-9][0-9]*$' }
inventoryPositionId: { type: string, pattern: '^[1-9][0-9]*$' }
inventoryPositionId: { type: string, nullable: true, pattern: '^[1-9][0-9]*$', description: 普通库存来源;与returnCartonId必须二选一 }
returnCartonId: { type: string, nullable: true, pattern: '^[1-9][0-9]*$', description: 原生退货箱来源;按整箱实收,不得虚构库存仓位 }
receivedPackages: { type: integer, format: int64, minimum: 1 }
targetPalletId: { type: string, pattern: '^[1-9][0-9]*$' }
targetPalletNo: { type: string, minLength: 1, maxLength: 128 }
@@ -3122,8 +3126,9 @@ components:
properties: { reason: { type: string, minLength: 1, maxLength: 1000 } }
DispatchResponse:
type: object
required: [appointmentBatches]
required: [appointmentBatches, actions]
properties:
actions: { $ref: '#/components/schemas/DispatchActions' }
id: { type: string }
dispatchNo: { type: string }
vehiclePlate: { type: string, nullable: true }
@@ -3155,12 +3160,27 @@ components:
items: { type: array, items: { $ref: '#/components/schemas/DispatchItem' } }
workflow:
nullable: true
description: 仅平台分页返回当前关联车次及两级审核;未关联为null。批量读取已有审核快照,不由执行状态推导;列表权限可读,审核操作仍需车次独立审核权限。
description: 平台查询返回当前关联车次及两级审核;未关联为null。批量读取已有审核快照,不由执行状态推导;列表权限可读,审核操作仍需车次独立审核权限。
allOf: [{ $ref: '#/components/schemas/DispatchWorkflow' }]
bolLines:
type: array
description: 仅详情接口返回的BOL打印聚合行;分页接口返回空数组,避免列表查询放大
items: { $ref: '#/components/schemas/DispatchBolLine' }
DispatchActions:
type: object
required: [canEdit, canCancel, canRemovePallet, canTransfer]
description: 服务端按执行事实计算;入口写权限仍需单独校验。取消按解除车次、取消出库、取消原排车顺序;取消单据保留,转派送原请求重放返回原取消结果。
properties:
canEdit: { type: boolean }
editBlockReason: { type: string, nullable: true }
canCancel: { type: boolean }
cancelBlockReason: { type: string, nullable: true }
canRemovePallet: { type: boolean }
removePalletBlockReason: { type: string, nullable: true }
canTransfer: { type: boolean }
transferBlockReason: { type: string, nullable: true }
outboundNo: { type: string, nullable: true }
outboundStatus: { type: string, nullable: true }
DispatchBatchSource:
type: object
required: [sourceKey, destinationKey, packageCount]