267 lines
8.6 KiB
TypeScript
267 lines
8.6 KiB
TypeScript
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)
|
|
}
|