统一自动发货逻辑
后台准备和发货复用统一履约服务,更新提示文案。
This commit is contained in:
@@ -6,13 +6,6 @@ import {
|
|||||||
listKuaishouIndustryVouchersByOid,
|
listKuaishouIndustryVouchersByOid,
|
||||||
listKuaishouIndustryVouchersByTaskId,
|
listKuaishouIndustryVouchersByTaskId,
|
||||||
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
|
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
|
||||||
import {
|
|
||||||
buyCloudtentaclesSku,
|
|
||||||
getCloudtentaclesAsset,
|
|
||||||
listCloudtentaclesSku,
|
|
||||||
useCloudtentaclesSku,
|
|
||||||
} from '../../platforms/cloudtentacles/catalog-service.js'
|
|
||||||
import { getCloudtentaclesKnapsack } from '../../platforms/cloudtentacles/knapsack-service.js'
|
|
||||||
import {
|
import {
|
||||||
backCloudtentaclesVirtualNumber,
|
backCloudtentaclesVirtualNumber,
|
||||||
getCloudtentaclesBindInfo,
|
getCloudtentaclesBindInfo,
|
||||||
@@ -28,24 +21,18 @@ import { nowIso } from '../../../utils/time.js'
|
|||||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||||
import { createAdminViewerContext, parseTaskContext } from '../admin-read-shared-helpers.js'
|
import { createAdminViewerContext, parseTaskContext } from '../admin-read-shared-helpers.js'
|
||||||
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
|
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
|
||||||
|
import { maskCode, maskPhone } from '../write-helpers.js'
|
||||||
import {
|
import {
|
||||||
ensureTaskClaimLink,
|
dispatchKuaishouCloudFulfillmentTask,
|
||||||
getTaskClaimExpiresAt,
|
prepareKuaishouCloudFulfillmentTask,
|
||||||
maskCode,
|
rebindKuaishouCloudTaskRole,
|
||||||
maskPhone,
|
} from '../../fulfillment/kuaishou-cloud/index.js'
|
||||||
} from '../write-helpers.js'
|
|
||||||
import { rebindKuaishouCloudTaskRole } from '../../fulfillment/kuaishou-cloud/index.js'
|
|
||||||
import {
|
import {
|
||||||
isKuaishouCloudTask,
|
isKuaishouCloudTask,
|
||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
normalizeKuaishouCloudRoleInfo,
|
normalizeKuaishouCloudRoleInfo,
|
||||||
prepareKuaishouCloudBindResourceWithFallback,
|
|
||||||
resolveKuaishouCloudBindUrlExpiresAt,
|
|
||||||
resolveKuaishouCloudBindingResources,
|
|
||||||
resolveKuaishouCloudVnKeyCandidates,
|
|
||||||
resolvePersistedCloudtentaclesContext,
|
resolvePersistedCloudtentaclesContext,
|
||||||
} from './kuaishou-cloud-helpers.js'
|
} from './kuaishou-cloud-helpers.js'
|
||||||
import { syncKuaishouCloudRoleInfoBeforeDispatch } from '../../fulfillment/kuaishou-cloud/dispatch-role-sync.js'
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AdminEntityIdInput,
|
AdminEntityIdInput,
|
||||||
@@ -62,7 +49,6 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
|
|||||||
session: AdminViewerSessionInput | null = null,
|
session: AdminViewerSessionInput | null = null,
|
||||||
): Promise<AdminTaskActionResponse> {
|
): Promise<AdminTaskActionResponse> {
|
||||||
const task = await getRequiredTask(taskId)
|
const task = await getRequiredTask(taskId)
|
||||||
const now = nowIso()
|
|
||||||
const viewerContext = createAdminViewerContext(session)
|
const viewerContext = createAdminViewerContext(session)
|
||||||
|
|
||||||
if (!viewerContext.canManageTaskLifecycle) {
|
if (!viewerContext.canManageTaskLifecycle) {
|
||||||
@@ -79,161 +65,15 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskContext = parseTaskContext(task)
|
const result = await prepareKuaishouCloudFulfillmentTask(task, {
|
||||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
source: 'admin_task_prepare',
|
||||||
const cloudContext = resolvePersistedCloudtentaclesContext(flow.binding.cloudSourceKeys)
|
actor: buildAdminActionActor(session),
|
||||||
const [knapsack, skuList] = await Promise.all([
|
|
||||||
getCloudtentaclesKnapsack(cloudContext),
|
|
||||||
listCloudtentaclesSku(cloudContext),
|
|
||||||
])
|
|
||||||
const resolvedBinding = resolveKuaishouCloudBindingResources(flow, {
|
|
||||||
skuItems: Array.isArray(skuList.items) ? skuList.items : [],
|
|
||||||
knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [],
|
|
||||||
})
|
})
|
||||||
const vnKeyCandidates = resolveKuaishouCloudVnKeyCandidates({
|
|
||||||
flow,
|
|
||||||
binding: resolvedBinding,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!resolvedBinding.skuId || vnKeyCandidates.length === 0) {
|
|
||||||
throw createHttpError('当前任务缺少可用 cloud 资源,且无法根据商品名自动解析 SKU / VN Key', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'admin_task_kuaishou_cloud_missing_binding_config',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const flowWithResolvedBinding = {
|
|
||||||
...flow,
|
|
||||||
binding: {
|
|
||||||
...flow.binding,
|
|
||||||
skuId: resolvedBinding.skuId,
|
|
||||||
skuName: resolvedBinding.skuName,
|
|
||||||
vnKey: resolvedBinding.vnKey,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const knapsackItem = resolvedBinding.knapsackItem
|
|
||||||
let usedKnapsack = Number(knapsackItem?.count || 0) > 0
|
|
||||||
let purchaseTriggered = false
|
|
||||||
let assetBefore = 0
|
|
||||||
let assetAfter = 0
|
|
||||||
const claimLinkState = await ensureTaskClaimLink(task)
|
|
||||||
|
|
||||||
if (!usedKnapsack) {
|
|
||||||
if (!flowWithResolvedBinding.purchase.autoBuyEnabled) {
|
|
||||||
throw createHttpError('背包中没有现成库存,且当前配置未开启自动购买', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'admin_task_kuaishou_cloud_auto_buy_disabled',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset = await getCloudtentaclesAsset(cloudContext)
|
|
||||||
const targetSku = resolvedBinding.skuItem
|
|
||||||
|
|
||||||
if (!targetSku) {
|
|
||||||
throw createHttpError(`cloudtentacles 未找到 SKU ${flowWithResolvedBinding.binding.skuId}`, {
|
|
||||||
statusCode: 404,
|
|
||||||
errorCode: 'admin_task_kuaishou_cloud_sku_not_found',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
assetBefore = Number(asset.asset || 0) || 0
|
|
||||||
const targetPrice = Number(targetSku.price || 0) || 0
|
|
||||||
const requiredAsset = targetPrice + flowWithResolvedBinding.purchase.minAssetReserve
|
|
||||||
if (assetBefore < requiredAsset) {
|
|
||||||
throw createHttpError(`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'admin_task_kuaishou_cloud_asset_not_enough',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await buyCloudtentaclesSku({
|
|
||||||
...cloudContext,
|
|
||||||
id: flowWithResolvedBinding.binding.skuId,
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
purchaseTriggered = true
|
|
||||||
|
|
||||||
const assetResult = await getCloudtentaclesAsset(cloudContext)
|
|
||||||
assetAfter = Number(assetResult.asset || 0) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
|
|
||||||
cloudContext,
|
|
||||||
vnKeyCandidates,
|
|
||||||
})
|
|
||||||
const vnId = preparedBinding.vnId
|
|
||||||
const vnPhone = preparedBinding.vnPhone
|
|
||||||
|
|
||||||
const nextContext = {
|
|
||||||
...taskContext,
|
|
||||||
kuaishouCloudFulfillment: {
|
|
||||||
...flowWithResolvedBinding,
|
|
||||||
binding: {
|
|
||||||
...flowWithResolvedBinding.binding,
|
|
||||||
resolvedSourceKey: cloudContext.resolvedSourceKey,
|
|
||||||
vnKey: preparedBinding.vnKey,
|
|
||||||
prepareStatus: 'ready',
|
|
||||||
vnId,
|
|
||||||
vnPhone,
|
|
||||||
bindUrl: preparedBinding.bindUrl,
|
|
||||||
bindPreparedAt: now,
|
|
||||||
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
|
||||||
bindProbeAt: null as null,
|
|
||||||
bindProbeStatus: 'pending',
|
|
||||||
bindProbeMessage: '',
|
|
||||||
roleName: '',
|
|
||||||
roleId: '',
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
status: 'pending',
|
|
||||||
name: '',
|
|
||||||
rid: '',
|
|
||||||
refreshedAt: null as null,
|
|
||||||
errorMessage: '',
|
|
||||||
rawInfo: null as null,
|
|
||||||
},
|
|
||||||
purchase: {
|
|
||||||
...flowWithResolvedBinding.purchase,
|
|
||||||
usedKnapsack,
|
|
||||||
purchaseTriggered,
|
|
||||||
assetBefore,
|
|
||||||
assetAfter,
|
|
||||||
purchaseAt: purchaseTriggered ? now : flowWithResolvedBinding.purchase.purchaseAt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTask = await updateTask(task.id, {
|
|
||||||
task_status: TASK_STATUS.WAITING_BINDING,
|
|
||||||
user_action_status: 'pending',
|
|
||||||
claim_token: claimLinkState.token || task.claim_token || '',
|
|
||||||
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
|
|
||||||
last_error: '',
|
|
||||||
context_json: JSON.stringify(nextContext),
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
|
|
||||||
await createTaskEvent(
|
|
||||||
task.id,
|
|
||||||
'kuaishou_cloud_binding_prepared',
|
|
||||||
{
|
|
||||||
skuId: flowWithResolvedBinding.binding.skuId,
|
|
||||||
skuName: flowWithResolvedBinding.binding.skuName,
|
|
||||||
vnKey: preparedBinding.vnKey,
|
|
||||||
vnId,
|
|
||||||
vnPhoneMasked: maskPhone(vnPhone),
|
|
||||||
bindUrl: preparedBinding.bindUrl,
|
|
||||||
purchaseTriggered,
|
|
||||||
usedKnapsack,
|
|
||||||
resolvedByName: resolvedBinding.resolvedByName,
|
|
||||||
},
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
task: mapTaskActionPayload(updatedTask),
|
task: mapTaskActionPayload(result.task),
|
||||||
claimUrl: claimLinkState.claimUrl,
|
claimUrl: result.claimUrl,
|
||||||
token: claimLinkState.token,
|
token: result.token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +82,6 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
session: AdminViewerSessionInput | null = null,
|
session: AdminViewerSessionInput | null = null,
|
||||||
): Promise<AdminTaskActionResponse> {
|
): Promise<AdminTaskActionResponse> {
|
||||||
const task = await getRequiredTask(taskId)
|
const task = await getRequiredTask(taskId)
|
||||||
const now = nowIso()
|
|
||||||
const viewerContext = createAdminViewerContext(session)
|
const viewerContext = createAdminViewerContext(session)
|
||||||
|
|
||||||
if (!viewerContext.canManageTaskLifecycle) {
|
if (!viewerContext.canManageTaskLifecycle) {
|
||||||
@@ -259,114 +98,14 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskContext = parseTaskContext(task)
|
const result = await dispatchKuaishouCloudFulfillmentTask(task, {
|
||||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
|
||||||
const cloudContext = resolvePersistedCloudtentaclesContext([
|
|
||||||
flow.binding.resolvedSourceKey,
|
|
||||||
...flow.binding.cloudSourceKeys,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) {
|
|
||||||
throw createHttpError('当前任务还没有准备好绑定资源,请先准备绑定资源', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'admin_task_kuaishou_cloud_not_prepared',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistedTicketCode = String(flow.ticket.code || '').trim()
|
|
||||||
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
|
||||||
const industryVoucherCode = String(
|
|
||||||
voucherContext.voucherCode || voucherContext.eticketId || '',
|
|
||||||
).trim()
|
|
||||||
const resolvedTicketCode = persistedTicketCode || industryVoucherCode
|
|
||||||
|
|
||||||
if (!resolvedTicketCode) {
|
|
||||||
throw createHttpError('旧快手小店核销流程已停用,请改用行业电子凭证处理', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'admin_task_kuaishou_cloud_missing_ticket_code',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const synced = await syncKuaishouCloudRoleInfoBeforeDispatch(task, {
|
|
||||||
now,
|
|
||||||
actor: session
|
|
||||||
? {
|
|
||||||
userId: Number(session.userId || 0) || 0,
|
|
||||||
username: String(session.username || '').trim(),
|
|
||||||
role: String(session.role || '').trim(),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
source: 'admin_task_dispatch',
|
source: 'admin_task_dispatch',
|
||||||
errorCodePrefix: 'admin_task_kuaishou_cloud',
|
actor: buildAdminActionActor(session),
|
||||||
cloudContext,
|
autoFinalize: false,
|
||||||
taskContext,
|
|
||||||
flow,
|
|
||||||
})
|
})
|
||||||
const syncedFlow = synced.flow
|
|
||||||
const syncedTaskContext = synced.taskContext
|
|
||||||
|
|
||||||
const dispatchResult = await useCloudtentaclesSku({
|
|
||||||
...cloudContext,
|
|
||||||
id: syncedFlow.binding.skuId,
|
|
||||||
virtualNumberId: syncedFlow.binding.vnId,
|
|
||||||
phone: syncedFlow.binding.vnPhone,
|
|
||||||
})
|
|
||||||
|
|
||||||
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: session
|
|
||||||
? {
|
|
||||||
userId: Number(session.userId || 0) || 0,
|
|
||||||
username: String(session.username || '').trim(),
|
|
||||||
role: String(session.role || '').trim(),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
sendType: Number(dispatchResult.sendType || 0) || 0,
|
|
||||||
note: String(dispatchResult.note || dispatchResult.responseMessage || '').trim(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTask = await updateTask(task.id, {
|
|
||||||
task_status: TASK_STATUS.DISPATCHED_PENDING_RETURN,
|
|
||||||
delivery_status: 'delivered',
|
|
||||||
result_code: 'kuaishou_cloud_dispatched',
|
|
||||||
result_message: String(
|
|
||||||
dispatchResult.responseMessage || dispatchResult.note || 'cloudtentacles 发货成功',
|
|
||||||
).trim(),
|
|
||||||
last_error: '',
|
|
||||||
context_json: JSON.stringify(nextContext),
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
|
|
||||||
await createTaskEvent(
|
|
||||||
task.id,
|
|
||||||
'kuaishou_cloud_dispatched',
|
|
||||||
{
|
|
||||||
ticketCodeMasked: maskCode(resolvedTicketCode),
|
|
||||||
skuId: syncedFlow.binding.skuId,
|
|
||||||
vnId: syncedFlow.binding.vnId,
|
|
||||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
|
||||||
sendType: dispatchResult.sendType,
|
|
||||||
note: dispatchResult.note,
|
|
||||||
},
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
task: mapTaskActionPayload(updatedTask),
|
task: mapTaskActionPayload(result.task),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export default function AdminTaskDetailPage() {
|
|||||||
'prepare-kuaishou-cloud',
|
'prepare-kuaishou-cloud',
|
||||||
() => prepareAdminTaskKuaishouCloudFulfillment(resolvedDetail.task.taskId),
|
() => prepareAdminTaskKuaishouCloudFulfillment(resolvedDetail.task.taskId),
|
||||||
'绑定资源已准备完成',
|
'绑定资源已准备完成',
|
||||||
`确认开始为任务 ${resolvedDetail.task.taskNo} 准备绑定资源吗?系统会自动检查背包、余额并申请虚拟号。`,
|
`确认开始为任务 ${resolvedDetail.task.taskNo} 准备绑定资源吗?系统会自动检查库存配置并申请虚拟号。`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onDispatchKuaishouCloud={() =>
|
onDispatchKuaishouCloud={() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user