feat(pda): persist and batch cabinet picking
This commit is contained in:
+109
-1
@@ -4,6 +4,16 @@ const EVIDENCE_TYPES = Object.freeze([
|
||||
'PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'
|
||||
])
|
||||
|
||||
// Keep the confirmed business labels in the PDA while the server retains its
|
||||
// legacy evidence codes. Remove this mapping only after the backend contract is migrated.
|
||||
const EVIDENCE_SLOTS = Object.freeze([
|
||||
{ type: 'PHOTO_STEP_1', label: '空仓照片' },
|
||||
{ type: 'PHOTO_STEP_2', label: '半仓照片' },
|
||||
{ type: 'PHOTO_STEP_3', label: '满仓照片' },
|
||||
{ type: 'PHOTO_STEP_4', label: '锁仓照片' },
|
||||
{ type: 'SEAL', label: '带锁码照' }
|
||||
])
|
||||
|
||||
// C02 supplies inboundId/inboundVersion and loading.pickingVersion. C04 supplies
|
||||
// inbound-scoped task and evidence operations. No local task is promoted to a
|
||||
// server success when any required identifier or version is absent.
|
||||
@@ -26,6 +36,8 @@ function request(options) {
|
||||
const error = new Error(body.message || '服务端请求失败')
|
||||
error.code = body.code || response.statusCode
|
||||
error.statusCode = response.statusCode
|
||||
error.requestId = body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id']
|
||||
error.traceId = body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id']
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
@@ -44,6 +56,10 @@ function idempotencyKey(operation, inboundId) {
|
||||
return `pda-${operation}-${inboundId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
function stableIdempotencyKey(operationId, suffix = '') {
|
||||
return `pda-${operationId}${suffix ? `-${suffix}` : ''}`
|
||||
}
|
||||
|
||||
function tenantQuery(tenantId) {
|
||||
const value = tenantId ?? session.tenantId
|
||||
if (!value) throw new Error('缺少客户隔离标识 tenantId,无法请求现场任务')
|
||||
@@ -54,6 +70,70 @@ export function isRemoteReady(inboundId) {
|
||||
return Boolean(session.token && session.tenantId && inboundId)
|
||||
}
|
||||
|
||||
export function getContainerPickingTask(containerNo, warehouseId) {
|
||||
if (!containerNo) throw new Error('缺少柜次号,无法请求现场任务')
|
||||
const query = [`containerNo=${encodeURIComponent(containerNo)}`]
|
||||
if (warehouseId) query.push(`warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
query.push(`tenantId=${encodeURIComponent(tenantQuery())}`)
|
||||
return request({ url: `/first-leg/pda-loading/tasks?${query.join('&')}`, method: 'GET' }).then((data) => {
|
||||
if (Array.isArray(data)) return data[0] || null
|
||||
if (data?.records) return data.records[0] || null
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
export function claimContainerPickingTask(taskId, expectedAssignmentVersion, operationId) {
|
||||
if (!taskId) throw new Error('缺少拣货任务标识,无法开始作业')
|
||||
return request({
|
||||
url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/claim`,
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': stableIdempotencyKey(operationId || `claim-${taskId}`) },
|
||||
data: { expectedAssignmentVersion, operationId: operationId || `pda-claim-${taskId}` }
|
||||
})
|
||||
}
|
||||
|
||||
function itemAction(taskId, itemId, path, payload, operationId) {
|
||||
// Concrete routes include /items/${encodeURIComponent(itemId)}/pick, /items/${encodeURIComponent(itemId)}/partial and /items/${encodeURIComponent(itemId)}/skip.
|
||||
if (!taskId || itemId == null) throw new Error('缺少拣货任务或明细标识,无法提交')
|
||||
return request({
|
||||
url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/items/${encodeURIComponent(itemId)}/${path}`,
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': stableIdempotencyKey(operationId || `${path}-${taskId}-${itemId}`) },
|
||||
data: payload
|
||||
})
|
||||
}
|
||||
|
||||
export function pickContainerItem(taskId, itemId, payload = {}) {
|
||||
return itemAction(taskId, itemId, 'pick', payload, payload.operationId)
|
||||
}
|
||||
|
||||
export function partialContainerItem(taskId, itemId, payload = {}) {
|
||||
return itemAction(taskId, itemId, 'partial', payload, payload.operationId)
|
||||
}
|
||||
|
||||
export function skipContainerItem(taskId, itemId, payload = {}) {
|
||||
return itemAction(taskId, itemId, 'skip', payload, payload.operationId)
|
||||
}
|
||||
|
||||
export async function submitContainerPickingBatch(task, actions, { warehouseId, operationId } = {}) {
|
||||
if (!task?.taskId) throw new Error('缺少拣货任务标识,无法提交')
|
||||
const submitOperationId = operationId || `submit-${task.taskId}-${task.containerNo || ''}-${task.version ?? task.pickingVersion ?? ''}`
|
||||
const results = []
|
||||
let version = task.version ?? task.pickingVersion
|
||||
for (const action of actions || []) {
|
||||
const common = { warehouseId, version: action.version ?? version, operationId: `${submitOperationId}-${action.itemId}` }
|
||||
let result
|
||||
if (action.type === 'PICK') result = await pickContainerItem(task.taskId, action.itemId, { ...common, pickedPackageCount: action.pickedPackageCount })
|
||||
else if (action.type === 'PARTIAL') result = await partialContainerItem(task.taskId, action.itemId, { ...common, finalPickedPackageCount: action.finalPickedPackageCount })
|
||||
else if (action.type === 'SKIP') result = await skipContainerItem(task.taskId, action.itemId, { ...common, reason: action.reason })
|
||||
if (result !== undefined) {
|
||||
results.push(result)
|
||||
version = result?.version ?? result?.taskVersion ?? result?.pickingVersion ?? version
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export function listLoadingOrders({ page = 1, size = 50, keyword = '' } = {}) {
|
||||
const query = [`mainStatus=LOADING`, `page=${page}`, `size=${size}`]
|
||||
if (session.tenantId) query.push(`tenantId=${encodeURIComponent(session.tenantId)}`)
|
||||
@@ -125,6 +205,34 @@ export function upsertEvidence(inboundId, payload, tenantId) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Upload a local photo to the file service and return its durable fileId. */
|
||||
export function uploadEvidenceFile(inboundId, evidenceType, localPath, tenantId, onProgress) {
|
||||
if (!EVIDENCE_TYPES.includes(evidenceType)) throw new Error('不支持的装柜证据类型')
|
||||
if (!localPath) throw new Error('缺少待上传的照片')
|
||||
const tenant = tenantQuery(tenantId)
|
||||
const baseUrl = session.apiBase || '/api/platform/v1'
|
||||
const headers = {}
|
||||
if (session.token) headers.Authorization = `Bearer ${session.token}`
|
||||
if (session.requestId) headers['X-Request-Id'] = session.requestId
|
||||
return new Promise((resolve, reject) => {
|
||||
const task = uni.uploadFile({
|
||||
url: `${baseUrl}/files/loading-evidence/${encodeURIComponent(tenant)}/${encodeURIComponent(inboundId)}?evidenceType=${encodeURIComponent(evidenceType)}`,
|
||||
filePath: localPath,
|
||||
name: 'file',
|
||||
header: headers,
|
||||
success(response) {
|
||||
let body = response.data
|
||||
try { body = typeof body === 'string' ? JSON.parse(body) : body } catch (_) { /* handled below */ }
|
||||
const code = String(body?.code ?? response.statusCode)
|
||||
if (code !== '200') { const error = new Error(body?.message || '文件上传失败'); error.code = body?.code || response.statusCode; reject(error); return }
|
||||
resolve(body?.data || body)
|
||||
},
|
||||
fail(error) { const next = new Error(error.errMsg || '网络连接失败'); next.network = true; reject(next) }
|
||||
})
|
||||
if (task && typeof task.onProgressUpdate === 'function' && typeof onProgress === 'function') task.onProgressUpdate(({ progress }) => onProgress(progress))
|
||||
})
|
||||
}
|
||||
|
||||
export function completeLoading(inboundId, payload, tenantId) {
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/complete?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
@@ -134,4 +242,4 @@ export function completeLoading(inboundId, payload, tenantId) {
|
||||
})
|
||||
}
|
||||
|
||||
export { EVIDENCE_TYPES }
|
||||
export { EVIDENCE_SLOTS, EVIDENCE_TYPES }
|
||||
|
||||
Reference in New Issue
Block a user