import { createTaskEvent } from '../../../repositories/task-event-repo.js' import { updateTask } from '../../../repositories/task-repo.js' import { createHttpError } from '../../../utils/http.js' import { nowIso } from '../../../utils/time.js' import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js' import type { TaskRow } from '../../../types/repository/rows.js' import { getClaimIdentityFromTask } from '../../claim/claim-identity.js' import { buildKuaishouCloudShippedSnapshot, isIndustryEVoucherTask, isKuaishouCloudTask, maskCode, maskPhone, normalizeKuaishouCloudFlow, type JsonObject, } from './domain.js' import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js' import { syncKuaishouCloudRoleInfoBeforeDispatch } from './dispatch-role-sync.js' import { markKuaishouCloudDispatchFailed } from './dispatch-failure.js' import { prepareStockAndDispatch, resolveCloudSkuDispatchLockKeys, withCloudSkuDispatchLocks, } from './dispatch-stock.js' import { resolveDispatchDeliveryItems, type DispatchResultItem, type DispatchStockResult, } from './dispatch-types.js' import { returnKuaishouCloudFulfillmentTask } from './return-fulfillment.js' import { normalizeActor, parseTaskContext } from './task-context.js' /** * lewan 自动/后台发货主用例: * UID 闸门同步角色 → 库存与采购 → cloudtentacles 下发 → 可选自动退号收尾。 */ export async function dispatchKuaishouCloudFulfillmentTask( task: TaskRow, options: JsonObject = {}, ) { if (!isKuaishouCloudTask(task)) { throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', { statusCode: 409, errorCode: 'kuaishou_cloud_task_invalid', }) } const actor = normalizeActor(options.actor) const now = nowIso() const taskContext = parseTaskContext(task) const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([ flow.binding.resolvedSourceKey, ...flow.binding.cloudSourceKeys, ]) if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) { throw createHttpError('当前任务还没有准备好绑定资源,请先完成绑定资源准备', { statusCode: 409, errorCode: 'kuaishou_cloud_not_prepared', }) } const persistedTicketCode = String(flow.ticket.code || '').trim() const voucherContext = isPlainObject(taskContext.kuaishouIndustryVoucher) ? taskContext.kuaishouIndustryVoucher : {} const industryVoucherCode = String( voucherContext.voucherCode || voucherContext.eticketId || '', ).trim() const hasIndustryVoucherForDispatch = Boolean(industryVoucherCode) || isIndustryEVoucherTask(task) const resolvedTicketCode = persistedTicketCode || industryVoucherCode if (!resolvedTicketCode && !hasIndustryVoucherForDispatch) { throw createHttpError('旧快手小店核销流程已停用,请改用行业电子凭证处理', { statusCode: 409, errorCode: 'kuaishou_cloud_missing_ticket_code', }) } if ( flow.dispatch.status === 'success' && normalizeTaskStatus(task.task_status) === TASK_STATUS.DISPATCHED_PENDING_RETURN ) { return { task, flow } } const synced = await syncKuaishouCloudRoleInfoBeforeDispatch(task, { now, actor, source: options.source || 'system_before_dispatch', cloudContext, taskContext, flow, }) const syncedFlow = synced.flow const syncedTaskContext = synced.taskContext const deliveryItems = resolveDispatchDeliveryItems(syncedFlow as JsonObject) if (deliveryItems.length === 0) { throw createHttpError('当前任务缺少 cloud 发货物品配置', { statusCode: 409, errorCode: 'kuaishou_cloud_missing_delivery_items', }) } let dispatchResults: DispatchResultItem[] let stockResult: DispatchStockResult try { ;({ dispatchResults, stockResult } = await withCloudSkuDispatchLocks( resolveCloudSkuDispatchLockKeys(cloudContext.resolvedSourceKey, deliveryItems), () => prepareStockAndDispatch({ task, flow: syncedFlow as JsonObject, cloudContext, deliveryItems, }), )) } catch (error) { await markKuaishouCloudDispatchFailed( task, syncedTaskContext as JsonObject, syncedFlow as JsonObject, error, { actor, source: options.source || 'system', }, ) throw error } const firstDispatchResult: DispatchResultItem = dispatchResults[0] || { cloudSkuId: syncedFlow.binding.skuId, cloudSkuName: syncedFlow.binding.skuName, quantity: 1, unitIndex: 1, sendType: 0, note: '', responseMessage: '', } const dispatchSummary = dispatchResults.length > 1 ? `cloudtentacles 发货成功,共 ${dispatchResults.length} 次` : String( firstDispatchResult.responseMessage || firstDispatchResult.note || 'cloudtentacles 发货成功', ).trim() const claimIdentity = getClaimIdentityFromTask(synced.task || task) const shippedSnapshot = buildKuaishouCloudShippedSnapshot({ expectedUid: claimIdentity.expectedUid, roleId: syncedFlow.binding.roleId || syncedFlow.role.rid, roleName: syncedFlow.binding.roleName || syncedFlow.role.name, vnId: syncedFlow.binding.vnId, vnPhone: syncedFlow.binding.vnPhone, vnKey: syncedFlow.binding.vnKey, dispatchedAt: now, }) const nextContext = { ...syncedTaskContext, kuaishouCloudFulfillment: { ...syncedFlow, ticket: { ...syncedFlow.ticket, code: resolvedTicketCode, capturedAt: resolvedTicketCode ? syncedFlow.ticket.capturedAt || now : syncedFlow.ticket.capturedAt, capturedBy: syncedFlow.ticket.capturedBy, }, dispatch: { ...syncedFlow.dispatch, status: 'success', dispatchAt: now, dispatchBy: actor, sendType: Number(firstDispatchResult.sendType || 0) || 0, note: dispatchSummary, items: dispatchResults, }, // 冻结发货身份:后续 claim 轮询/过期换号不得改写展示与匹配依据 shipped: syncedFlow.shipped || shippedSnapshot, purchase: { ...syncedFlow.purchase, usedKnapsack: stockResult.usedKnapsack, purchaseTriggered: stockResult.purchaseTriggered, assetBefore: stockResult.assetBefore, assetAfter: stockResult.assetAfter, purchaseAt: stockResult.purchaseTriggered ? now : syncedFlow.purchase.purchaseAt, items: stockResult.items, }, }, } let updatedTask = await updateTask(task.id, { task_status: TASK_STATUS.DISPATCHED_PENDING_RETURN, delivery_status: 'delivered', result_code: 'kuaishou_cloud_dispatched', result_message: dispatchSummary, user_action_status: 'not_required', last_error: '', context_json: JSON.stringify(nextContext), updated_at: now, }) if (!updatedTask) { throw createHttpError('kuaishou-lewan 发货状态更新失败', { statusCode: 500, errorCode: 'kuaishou_cloud_dispatch_update_failed', }) } await createTaskEvent( task.id, 'kuaishou_cloud_dispatched', { source: String(options.source || 'system').trim() || 'system', ticketCodeMasked: maskCode(resolvedTicketCode), skuId: syncedFlow.binding.skuId, vnId: syncedFlow.binding.vnId, vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone), expectedUid: shippedSnapshot.expectedUid, roleId: shippedSnapshot.roleId, roleName: shippedSnapshot.roleName, sendType: firstDispatchResult.sendType, note: dispatchSummary, deliveryItems, dispatchResults, stockResult, shipped: shippedSnapshot, actor, }, now, ) const shouldAutoFinalize = options.autoFinalize === true && normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment).returnNumber .autoReturnEnabled === true if (shouldAutoFinalize) { const finalizeResult = await returnKuaishouCloudFulfillmentTask(updatedTask, { actor, source: options.source || 'system_auto_finalize', // 发货后自动收尾:退号并尝试核销(与 admin 清理退号不同) consumeIndustryVoucher: true, }) updatedTask = finalizeResult.task } return { task: updatedTask, flow: normalizeKuaishouCloudFlow( parseTaskContext(updatedTask).kuaishouCloudFulfillment, ), } } function isPlainObject(value: unknown): value is JsonObject { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) }