openapi: 3.0.3 info: title: Lastleg Overseas PDA Warehouse Operations API version: 1.0.0 description: 海外仓PDA排车拣货发车、一件代发出库、盘点、同仓整托移库、托盘与换标作业;全部范围从PDA Token解析,派送不进入PDA。 security: - bearerAuth: [] paths: /api/pda/v1/last-leg/dispatches: get: summary: 查询Token绑定仓的排车PDA任务 description: 仅 OVERSEAS_WAREHOUSE_PDA_OPERATOR + lastleg:pda:operate;跨客户读取绑定仓任务,不接收customerId或warehouseId。 parameters: [{ name: status, in: query, required: false, schema: { type: string } }] responses: { '200': { description: 排车任务列表 }, '403': { description: 角色、权限或仓库绑定缺失 } } /api/pda/v1/last-leg/dispatches/{dispatchNo}: get: summary: 查询绑定仓排车任务详情 description: 返回 canDepart 与 departBlockReason;PDA 必须以服务端门禁决定是否开放发车,不得仅按本地拣货数量推断。 parameters: [{ name: dispatchNo, in: path, required: true, schema: { type: string } }] responses: { '200': { description: 排车托盘、目的仓与库存位置详情 }, '404': { description: 当前绑定仓无此排车任务 } } /api/pda/v1/last-leg/dispatches/{dispatchNo}/items/{itemId}/pick: post: summary: 扫码拣取排车托盘包裹 parameters: - { name: dispatchNo, in: path, required: true, schema: { type: string } } - { name: itemId, in: path, required: true, schema: { type: string, pattern: '^[0-9]+$' } } requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DispatchPick' } } } } responses: { '200': { description: 本次及累计拣货结果;同键同请求重放既有结果 }, '409': { description: 跨仓、跨客户、库存、数量或幂等冲突 } } /api/pda/v1/last-leg/dispatches/{dispatchNo}/complete: post: summary: 完成排车拣货 description: 每个要求托盘及全部包裹必须精确完成;不允许部分完成。 parameters: [{ name: dispatchNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/IdempotentAction' } } } } responses: { '200': { description: READY_TO_DEPART结果 }, '409': { description: 未全部拣货或幂等冲突 } } /api/pda/v1/last-leg/dispatches/{dispatchNo}/depart: post: summary: 排车发车并完成出库门禁 description: 仅完整拣货且存在待出库单时发车;同事务推进出库与排车状态、订单归属并释放托盘锁,不重复扣减扫码时已扣库存。 parameters: [{ name: dispatchNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DepartAction' } } } } responses: { '200': { description: DEPARTED结果;同键同请求返回既有结果 }, '409': { description: 拣货、出库或幂等门禁未满足 } } /api/pda/v1/last-leg/outbound-tasks: get: summary: 查询Token绑定仓的一件代发出库任务 description: 一件代发复用统一出库任务模型;不接收customerId或warehouseId,派送不进入PDA。 parameters: [{ name: status, in: query, required: false, schema: { type: string } }] responses: { '200': { description: 出库任务列表 } } /api/pda/v1/last-leg/outbound-tasks/{taskNo}: get: summary: 查询一件代发出库任务详情 parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }] responses: '200': description: 出库任务、出库项及每条可拣库存位置;inventoryPositionId仅作为pick提交键,不作为用户展示编号 content: { application/json: { schema: { $ref: '#/components/schemas/PdaOutboundTaskDetail' } } } '404': { description: 当前绑定仓无此任务 } /api/pda/v1/last-leg/outbound-tasks/{taskNo}/pick: post: summary: 扫码拣取一件代发箱件 parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/OutboundPick' } } } } responses: { '200': { description: 更新后的统一出库任务 }, '409': { description: 库存、数量或幂等冲突 } } /api/pda/v1/last-leg/outbound-tasks/{taskNo}/complete: post: summary: 完成一件代发出库任务 description: 全部托盘及箱件精确拣完后完成;同一完成幂等键返回既有结果,其他键明确冲突。 parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/IdempotentAction' } } } } responses: { '200': { description: COMPLETED任务 }, '409': { description: 未全部拣货或幂等冲突 } } /api/pda/v1/last-leg/stocktakes: get: summary: 查询Token绑定仓盘点任务 parameters: - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } - { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } } - { name: status, in: query, schema: { type: string, enum: [IN_PROGRESS, SUBMITTED] } } responses: { '200': { description: 盘点任务分页 } } post: summary: 按库位或托盘创建盘点任务 requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeCreate' } } } } responses: { '200': { description: 创建后的完整盘点任务 } } /api/pda/v1/last-leg/stocktakes/{taskNo}: get: summary: 查询盘点任务详情 parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }] responses: { '200': { description: 盘点任务及冻结库存位置 } } /api/pda/v1/last-leg/stocktakes/{taskNo}/counts: post: summary: 提交一个库存位置的实盘箱数 parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeCount' } } } } responses: { '200': { description: 已计数明细 }, '409': { description: 版本或幂等冲突 } } /api/pda/v1/last-leg/stocktakes/{taskNo}/submit: post: summary: 提交盘点并按差异调整库存 parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeSubmit' } } } } responses: { '200': { description: 已提交盘点任务 }, '409': { description: 未全盘、库存版本变化或负库存风险 } } /api/pda/v1/last-leg/inventory-moves: post: summary: 同仓整托移库 requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryMove' } } } } responses: { '200': { description: 移库结果 }, '409': { description: 跨仓、资源或库存版本冲突 } } /api/pda/v1/last-leg/pallet-operations/pallets/resolve: post: summary: 扫码查询托盘及库存内容 requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ScanCode' } } } } responses: { '200': { description: Token绑定仓托盘内容 } } /api/pda/v1/last-leg/pallet-operations/assemble: post: summary: 将多个来源托盘的指定库存行组到目标托盘 requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/PalletAssemble' } } } } responses: { '200': { description: 组托及实物托盘状态结果 }, '409': { description: 目的仓、库存或资源版本冲突 } } /api/pda/v1/last-leg/pallet-operations/disassemble: post: summary: 将来源托盘库存拆到一个或多个目标托盘 requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/PalletDisassemble' } } } } responses: { '200': { description: 拆托结果 }, '409': { description: 库存或资源版本冲突 } } /api/pda/v1/last-leg/pallet-operations/swap: post: summary: 将整托库存更换到空目标托盘 requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/PalletSwap' } } } } responses: { '200': { description: 换托结果 }, '409': { description: 目标托盘非空或版本冲突 } } /api/pda/v1/last-leg/pallet-operations/unbind: post: summary: 解除整托库存与实物托盘绑定 requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/PalletUnbind' } } } } responses: { '200': { description: 解绑结果 }, '409': { description: 库存或资源版本冲突 } } /api/pda/v1/last-leg/relabel-tasks/candidates: get: summary: 查询Token绑定仓可执行换标任务 responses: { '200': { description: 待认领、处理中或待复核换标任务 } } /api/pda/v1/last-leg/relabel-tasks/{relabelNo}: get: summary: 查询换标现场任务详情 parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }] responses: { '200': { description: 申请明细、逐件事实、照片与历史 } } /api/pda/v1/last-leg/relabel-tasks/{relabelNo}/claim: post: summary: 认领换标任务并推进申请至处理中 parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/IdempotentAction' } } } } responses: { '200': { description: 已认领任务 }, '409': { description: 仓库、状态或幂等冲突 } } /api/pda/v1/last-leg/relabel-tasks/{relabelNo}/units: post: summary: 登记一件正常换标事实 parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelUnit' } } } } responses: { '200': { description: 更新后的任务 }, '409': { description: 旧标重复、新标不符或版本冲突 } } /api/pda/v1/last-leg/relabel-tasks/{relabelNo}/units/skip: post: summary: 登记一件有原因的跳过事实 parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelSkip' } } } } responses: { '200': { description: 更新后的任务 } } /api/pda/v1/last-leg/relabel-tasks/{relabelNo}/exceptions: post: summary: 登记一件换标异常事实 parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelException' } } } } responses: { '200': { description: 更新后的任务 } } /api/pda/v1/last-leg/relabel-tasks/{relabelNo}/photos/files: post: summary: 上传换标完成现场照片 parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { multipart/form-data: { schema: { type: object, required: [file], properties: { file: { type: string, format: binary } } } } } } responses: { '200': { description: OVERSEAS_RELABEL_PHOTO文件元数据 } } /api/pda/v1/last-leg/relabel-tasks/{relabelNo}/complete: post: summary: 提交现场任务等待复核,不提前完成申请 parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelComplete' } } } } responses: { '200': { description: WAIT_REVIEW任务 }, '409': { description: 箱数未核平、照片或版本冲突 } } /api/pda/v1/last-leg/relabel-tasks/{relabelNo}/review: post: summary: 现场复核;APPROVE才完成既有换标申请,REWORK退回处理 parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelReview' } } } } responses: { '200': { description: COMPLETED或PROCESSING任务 }, '409': { description: 状态或版本冲突 } } components: securitySchemes: bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT } schemas: ScanCode: type: object required: [code] properties: { code: { type: string, minLength: 1, maxLength: 128 } } IdempotentAction: type: object required: [idempotencyKey] properties: { idempotencyKey: { type: string, minLength: 1, maxLength: 255 } } DepartAction: type: object required: [idempotencyKey] properties: idempotencyKey: { type: string, minLength: 1, maxLength: 255 } remark: { type: string, maxLength: 1000, nullable: true } DispatchPick: type: object required: [positionId, packages, productQuantity, idempotencyKey] properties: positionId: { type: string, pattern: '^[0-9]+$' } packages: { type: integer, format: int64, minimum: 1 } productQuantity: { type: integer, format: int64, minimum: 0 } idempotencyKey: { type: string, minLength: 1, maxLength: 255 } remark: { type: string, maxLength: 1000, nullable: true } OutboundPick: type: object required: [outboundPalletItemId, inventoryPositionId, packages, idempotencyKey] properties: outboundPalletItemId: { type: string, pattern: '^[0-9]+$' } inventoryPositionId: { type: string, pattern: '^[0-9]+$' } packages: { type: integer, format: int64, minimum: 1 } idempotencyKey: { type: string, minLength: 1, maxLength: 255 } PdaOutboundTaskDetail: type: object required: [task, items] properties: task: type: object required: [taskNo, status, outboundNo] properties: taskNo: { type: string } status: { type: string } outboundNo: { type: string } pushedAt: { type: string, format: date-time, nullable: true } completedAt: { type: string, format: date-time, nullable: true } items: type: array items: type: object required: [outboundPalletItemId, palletNo, skuCode, inboundCode, packageCount, pickedPackageCount, status, positions] properties: outboundPalletItemId: { type: string, description: pick请求提交键,不作为用户展示编号 } palletNo: { type: string } skuCode: { type: string } inboundCode: { type: string } packageCount: { type: integer } pickedPackageCount: { type: integer } status: { type: string } positions: type: array items: type: object required: [inventoryPositionId, palletNo, cartonMark, skuCode, inboundCode, availablePackages, pickedPackages] properties: inventoryPositionId: { type: string, description: pick请求提交键,不作为用户展示编号 } palletNo: { type: string } cartonMark: { type: string, nullable: true } skuCode: { type: string } inboundCode: { type: string } availablePackages: { type: integer, format: int64, minimum: 0 } pickedPackages: { type: integer, format: int64, minimum: 0 } StocktakeCreate: type: object required: [scopeType, scopeCode, idempotencyKey] properties: scopeType: { type: string, enum: [LOCATION, PALLET] } scopeCode: { type: string, maxLength: 128 } idempotencyKey: { type: string, maxLength: 128 } StocktakeCount: type: object required: [positionId, actualPackages, expectedVersion, idempotencyKey] properties: positionId: { type: string, pattern: '^[0-9]+$' } actualPackages: { type: integer, format: int64, minimum: 0 } expectedVersion: { type: integer, format: int64, minimum: 0 } idempotencyKey: { type: string, maxLength: 128 } StocktakeSubmit: type: object required: [expectedVersion, reason, idempotencyKey] properties: expectedVersion: { type: integer, format: int64, minimum: 0 } reason: { type: string, minLength: 1, maxLength: 500 } idempotencyKey: { type: string, maxLength: 128 } InventoryMove: type: object required: [sourcePalletCode, targetLocationCode, expectedPalletVersion, idempotencyKey, reason] properties: sourcePalletCode: { type: string, maxLength: 128 } targetLocationCode: { type: string, maxLength: 128 } expectedPalletVersion: { type: integer, minimum: 0 } idempotencyKey: { type: string, maxLength: 128 } reason: { type: string, minLength: 1, maxLength: 500 } PalletExpectation: type: object required: [palletCode, expectedVersion] properties: palletCode: { type: string, maxLength: 128 } expectedVersion: { type: integer, minimum: 0 } PalletMoveLine: type: object required: [positionId, packages, expectedPositionVersion] properties: positionId: { type: string, pattern: '^[0-9]+$' } packages: { type: integer, format: int64, minimum: 1 } expectedPositionVersion: { type: integer, format: int64, minimum: 0 } PalletAssemble: type: object required: [idempotencyKey, target, sources, lines] properties: idempotencyKey: { type: string, maxLength: 128 } target: { $ref: '#/components/schemas/PalletExpectation' } sources: { type: array, minItems: 1, maxItems: 20, items: { $ref: '#/components/schemas/PalletExpectation' } } lines: { type: array, minItems: 1, maxItems: 200, items: { $ref: '#/components/schemas/PalletMoveLine' } } reason: { type: string, maxLength: 500 } PalletTargetMove: type: object required: [target, lines] properties: target: { $ref: '#/components/schemas/PalletExpectation' } lines: { type: array, minItems: 1, maxItems: 200, items: { $ref: '#/components/schemas/PalletMoveLine' } } PalletDisassemble: type: object required: [idempotencyKey, source, targets] properties: idempotencyKey: { type: string, maxLength: 128 } source: { $ref: '#/components/schemas/PalletExpectation' } targets: { type: array, minItems: 1, maxItems: 20, items: { $ref: '#/components/schemas/PalletTargetMove' } } reason: { type: string, maxLength: 500 } PalletSwap: type: object required: [idempotencyKey, source, target] properties: idempotencyKey: { type: string, maxLength: 128 } source: { $ref: '#/components/schemas/PalletExpectation' } target: { $ref: '#/components/schemas/PalletExpectation' } reason: { type: string, maxLength: 500 } PalletUnbind: type: object required: [idempotencyKey, source] properties: idempotencyKey: { type: string, maxLength: 128 } source: { $ref: '#/components/schemas/PalletExpectation' } reason: { type: string, maxLength: 500 } RelabelUnit: type: object required: [itemId, oldLabel, newLabel, expectedVersion, idempotencyKey] properties: itemId: { type: string, pattern: '^[0-9]+$' } oldLabel: { type: string, maxLength: 255 } newLabel: { type: string, maxLength: 255 } expectedVersion: { type: integer, minimum: 0 } idempotencyKey: { type: string, maxLength: 255 } RelabelSkip: type: object required: [itemId, oldLabel, reason, expectedVersion, idempotencyKey] properties: itemId: { type: string, pattern: '^[0-9]+$' } oldLabel: { type: string, maxLength: 255 } reason: { type: string, maxLength: 1000 } expectedVersion: { type: integer, minimum: 0 } idempotencyKey: { type: string, maxLength: 255 } RelabelException: type: object required: [itemId, exceptionCode, reason, expectedVersion, idempotencyKey] properties: itemId: { type: string, pattern: '^[0-9]+$' } oldLabel: { type: string, maxLength: 255, nullable: true } exceptionCode: { type: string, enum: [DAMAGED, MISSING_LABEL, LABEL_MISMATCH, UNREADABLE, OTHER] } reason: { type: string, maxLength: 1000 } expectedVersion: { type: integer, minimum: 0 } idempotencyKey: { type: string, maxLength: 255 } RelabelComplete: type: object required: [photoFileIds, expectedVersion, idempotencyKey] properties: photoFileIds: { type: array, minItems: 1, maxItems: 20, items: { type: string, pattern: '^[0-9]+$' } } expectedVersion: { type: integer, minimum: 0 } idempotencyKey: { type: string, maxLength: 255 } remark: { type: string, maxLength: 1000, nullable: true } RelabelReview: type: object required: [decision, expectedVersion, idempotencyKey] properties: decision: { type: string, enum: [APPROVE, REWORK] } expectedVersion: { type: integer, minimum: 0 } idempotencyKey: { type: string, maxLength: 255 } remark: { type: string, maxLength: 1000, nullable: true }