完整流程-调试

This commit is contained in:
yml
2026-05-03 18:44:44 +08:00
parent 063c3fd8a1
commit 90cb7aa154
25 changed files with 2337 additions and 27 deletions
@@ -68,6 +68,11 @@ import {
getOrderFulfillmentBindingsFilePath,
saveOrderFulfillmentBindingConfigs,
} from '../order/fulfillment-binding-config-service.js'
import {
getKuaishouCloudFulfillmentConfig,
getKuaishouCloudFulfillmentFilePath,
saveKuaishouCloudFulfillmentConfig,
} from '../order/kuaishou-cloud-fulfillment-config-service.js'
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
import { createHttpError } from '../../utils/http.js'
import { formatFenToAmount } from '../../utils/money.js'
@@ -84,6 +89,7 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouCloudFulfillmentConfigInput} AdminKuaishouCloudFulfillmentConfigInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesTestLoginInput} AdminCloudtentaclesTestLoginInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionInput */
@@ -778,6 +784,84 @@ export async function getAdminFulfillmentBindingConfigs() {
}
}
export function getAdminKuaishouCloudFulfillmentConfig() {
const config = getKuaishouCloudFulfillmentConfig()
return {
filePath: getKuaishouCloudFulfillmentFilePath(),
source: {
enabled: config.enabled !== false,
items: (Array.isArray(config.items) ? config.items : []).map((item) => ({
id: String(item.id || '').trim(),
enabled: item.enabled !== false,
priority: Number(item.priority || 100) || 100,
provider: String(item.provider || 'khhao').trim() || 'khhao',
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(item.shopId || '').trim(),
internalSkuCode: String(item.internalSkuCode || '').trim(),
internalSkuName: String(item.internalSkuName || '').trim(),
externalSkuCode: String(item.externalSkuCode || '').trim(),
externalItemId: String(item.externalItemId || '').trim(),
externalSkuName: String(item.externalSkuName || '').trim(),
resolvedSkuName: String(item.resolvedSkuName || '').trim(),
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
cloudSkuName: String(item.cloudSkuName || '').trim(),
vnKey: String(item.vnKey || '').trim(),
autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
notes: String(item.notes || '').trim(),
})),
},
}
}
/** @param {AdminKuaishouCloudFulfillmentConfigInput} [payload] */
export async function updateAdminKuaishouCloudFulfillmentConfig(
payload = /** @type {AdminKuaishouCloudFulfillmentConfigInput} */ ({}),
) {
const saved = saveKuaishouCloudFulfillmentConfig({
enabled: payload.enabled !== false,
items: Array.isArray(payload.items) ? payload.items : [],
})
await syncConfiguredFulfillmentBindings()
return {
filePath: getKuaishouCloudFulfillmentFilePath(),
source: {
enabled: saved.enabled !== false,
items: (Array.isArray(saved.items) ? saved.items : []).map((item) => ({
id: String(item.id || '').trim(),
enabled: item.enabled !== false,
priority: Number(item.priority || 100) || 100,
provider: String(item.provider || 'khhao').trim() || 'khhao',
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(item.shopId || '').trim(),
internalSkuCode: String(item.internalSkuCode || '').trim(),
internalSkuName: String(item.internalSkuName || '').trim(),
externalSkuCode: String(item.externalSkuCode || '').trim(),
externalItemId: String(item.externalItemId || '').trim(),
externalSkuName: String(item.externalSkuName || '').trim(),
resolvedSkuName: String(item.resolvedSkuName || '').trim(),
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
cloudSkuName: String(item.cloudSkuName || '').trim(),
vnKey: String(item.vnKey || '').trim(),
autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
notes: String(item.notes || '').trim(),
})),
},
}
}
/** @param {AdminFulfillmentBindingLookupInput} [payload] */
export async function lookupAdminFulfillmentBindingOrder(payload = /** @type {AdminFulfillmentBindingLookupInput} */ ({})) {
const provider = String(payload.provider || 'agiso').trim() || 'agiso'
@@ -27,6 +27,7 @@ import {
getTaskPrimaryInventoryItemId,
isAssistedClaimTask,
isManualDispatchTask,
mapKuaishouCloudFulfillmentContext,
mapManualDispatchContext,
mapRedeemResolutionContext,
parseTaskContext,
@@ -278,6 +279,7 @@ export async function getAdminTaskDetail(taskId, session = null) {
roleName: String(taskState.reviewRoleName || '').trim() || '',
},
redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution),
kuaishouCloudFulfillment: mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment),
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
events: taskEvents.map(mapAdminTaskEvent),
operations: {
@@ -289,6 +291,15 @@ export async function getAdminTaskDetail(taskId, session = null) {
canClose: canViewerCloseTask(task, viewerContext),
canMarkManualReview: viewerContext.canManageTaskLifecycle && !['redeemed', 'closed', 'manual_review'].includes(task.task_status),
canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && !['redeemed', 'closed'].includes(task.task_status),
canPrepareKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()),
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['waiting_binding'].includes(String(task.task_status || '').trim()),
canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['dispatched_pending_return'].includes(String(task.task_status || '').trim()),
canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext),
canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext),
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
@@ -25,6 +25,67 @@ export function mapManualDispatchContext(value, viewerContext = createAdminViewe
}
}
export function mapKuaishouCloudFulfillmentContext(value) {
if (!value || typeof value !== 'object') {
return null
}
const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {}
const binding = value.binding && typeof value.binding === 'object' ? value.binding : {}
const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {}
const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {}
const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {}
const consume = value.consume && typeof value.consume === 'object' ? value.consume : {}
return {
flowType: String(value.flowType || '').trim(),
configId: String(value.configId || '').trim(),
internalSkuCode: String(value.internalSkuCode || '').trim(),
internalSkuName: String(value.internalSkuName || '').trim(),
ticket: {
code: String(ticket.code || '').trim(),
capturedAt: ticket.capturedAt || null,
},
binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || '').trim(),
vnKey: String(binding.vnKey || '').trim(),
vnId: Number(binding.vnId || 0) || 0,
vnPhone: String(binding.vnPhone || '').trim(),
bindUrl: String(binding.bindUrl || '').trim(),
bindPreparedAt: binding.bindPreparedAt || null,
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
usedKnapsack: purchase.usedKnapsack === true,
purchaseTriggered: purchase.purchaseTriggered === true,
assetBefore: Number(purchase.assetBefore || 0) || 0,
assetAfter: Number(purchase.assetAfter || 0) || 0,
purchaseAt: purchase.purchaseAt || null,
},
dispatch: {
status: String(dispatch.status || 'pending').trim() || 'pending',
dispatchAt: dispatch.dispatchAt || null,
sendType: Number(dispatch.sendType || 0) || 0,
note: String(dispatch.note || '').trim(),
},
returnNumber: {
status: String(returnNumber.status || 'pending').trim() || 'pending',
returnedAt: returnNumber.returnedAt || null,
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
},
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
},
notes: String(value.notes || '').trim(),
}
}
export function mapRedeemResolutionContext(value) {
if (!value || typeof value !== 'object') {
return null
@@ -85,7 +146,7 @@ export function canViewerCloseTask(task, viewerContext) {
return false
}
return !['redeemed', 'closed'].includes(String(task?.task_status || '').trim())
return !['redeemed', 'completed', 'closed'].includes(String(task?.task_status || '').trim())
}
export function resolveDisplayShopName(provider, shopId, shopName) {
@@ -175,6 +236,10 @@ export function resolveOrderItemDeliveryMode(tasks, orderItemId) {
return 'manual_dispatch'
}
if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
return 'kuaishou_cloud'
}
if (task.requires_claim || getTaskPrimaryClaimTokenId(task) || task.primary_claim_token || task.claim_token) {
return 'claim_link'
}
@@ -61,15 +61,18 @@ export {
completeAdminTaskManualDispatch,
confirmAdminTaskAssistedRole,
createAdminInventoryItem,
dispatchAdminTaskKuaishouCloudFulfillment,
importAdminInventoryItems,
invalidateAdminInventoryItem,
markAdminTaskManualReview,
prepareAdminTaskKuaishouCloudFulfillment,
redeemAdminTaskAssisted,
regenerateAdminTaskClaimLink,
releaseAdminInventoryItem,
releaseAdminTaskInventory,
releaseAdminTaskInventoryBinding,
replayAdminWebhookEvent,
returnNumberAdminTaskKuaishouCloudFulfillment,
retryAdminTask,
} from './admin-write-service.js'
@@ -348,6 +348,22 @@ function buildTaskBindingState(task) {
return { systemBindingStatus: 'waiting_inventory', userBindingStatus: 'not_started' }
}
if (normalizedStatus === 'pending_binding_prepare') {
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
}
if (normalizedStatus === 'waiting_binding') {
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
}
if (normalizedStatus === 'dispatched_pending_return') {
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
}
if (normalizedStatus === 'completed') {
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
}
if (normalizedStatus === 'manual_review') {
return { systemBindingStatus: 'manual_review', userBindingStatus: 'binding_exception' }
}
@@ -22,6 +22,23 @@ import { confirmClaimRoleForAdminTask, redeemClaimTaskForAdminTask } from '../cl
import { reserveInventoryForTask } from '../order/inventory-service.js'
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
import { getCloudtentaclesSourceConfig } from '../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionState } from '../platforms/cloudtentacles/session-state-service.js'
import {
buyCloudtentaclesSku,
getCloudtentaclesAsset,
listCloudtentaclesSku,
useCloudtentaclesSku,
} from '../platforms/cloudtentacles/catalog-service.js'
import { getCloudtentaclesKnapsack } from '../platforms/cloudtentacles/knapsack-service.js'
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
verifyCloudtentaclesLoginCode,
} from '../platforms/cloudtentacles/virtual-number-service.js'
import { closeTencentBrowserSession } from '../session/session.js'
import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
@@ -56,6 +73,7 @@ import {
/** @typedef {import('../../types/admin-write-models.js').AdminTaskBindingReleaseResponse} AdminTaskBindingReleaseResponse */
/** @typedef {import('../../types/admin-write-models.js').AdminTaskManualDispatchResponse} AdminTaskManualDispatchResponse */
/** @typedef {import('../../types/admin-write-models.js').AdminWebhookReplayResponse} AdminWebhookReplayResponse */
/** @typedef {import('../../types/admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchInput */
/** @returns {Promise<AdminInventoryMutationResponse>} */
/** @param {AdminInventoryCreateInput} [payload] */
@@ -675,6 +693,352 @@ export async function completeAdminTaskManualDispatch(taskId, payload = {}, sess
}
}
/** @returns {Promise<AdminTaskActionResponse>} */
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session = null) {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限准备绑定资源', {
statusCode: 403,
errorCode: 'admin_task_prepare_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 cloud 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const cloudContext = resolvePersistedCloudtentaclesContext()
if (!flow.binding.skuId || !flow.binding.vnKey) {
throw createHttpError('当前任务缺少 cloud SKU 或 VN Key 配置', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_binding_config',
})
}
const knapsack = await getCloudtentaclesKnapsack(cloudContext)
const knapsackItem = (Array.isArray(knapsack.items) ? knapsack.items : []).find((item) => Number(item.id || 0) === flow.binding.skuId) || null
let usedKnapsack = Number(knapsackItem?.count || 0) > 0
let purchaseTriggered = false
let assetBefore = 0
let assetAfter = 0
if (!usedKnapsack) {
if (!flow.purchase.autoBuyEnabled) {
throw createHttpError('背包中没有现成库存,且当前配置未开启自动购买', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_auto_buy_disabled',
})
}
const [asset, skuList] = await Promise.all([
getCloudtentaclesAsset(cloudContext),
listCloudtentaclesSku(cloudContext),
])
const targetSku = (Array.isArray(skuList.items) ? skuList.items : []).find((item) => Number(item.id || 0) === flow.binding.skuId) || null
if (!targetSku) {
throw createHttpError(`cloudtentacles 未找到 SKU ${flow.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 + flow.purchase.minAssetReserve
if (assetBefore < requiredAsset) {
throw createHttpError(`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_asset_not_enough',
})
}
await buyCloudtentaclesSku({
...cloudContext,
id: flow.binding.skuId,
count: 1,
})
purchaseTriggered = true
const assetResult = await getCloudtentaclesAsset(cloudContext)
assetAfter = Number(assetResult.asset || 0) || 0
}
const appointed = await appointCloudtentaclesVirtualNumber({
...cloudContext,
key: flow.binding.vnKey,
})
const vnId = Number(appointed.item?.id || 0)
const vnPhone = String(appointed.item?.phone || '').trim()
if (!vnId || !vnPhone) {
throw createHttpError('申请虚拟号成功但返回数据不完整', {
statusCode: 502,
errorCode: 'admin_task_kuaishou_cloud_invalid_vn',
})
}
await generateCloudtentaclesLoginCode({
...cloudContext,
key: flow.binding.vnKey,
id: vnId,
})
const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({
...cloudContext,
key: flow.binding.vnKey,
phone: vnPhone,
})
await verifyCloudtentaclesLoginCode({
...cloudContext,
key: flow.binding.vnKey,
id: vnId,
code: fetchedCode.code,
})
const bindUrlResult = await getCloudtentaclesBindUrl({
...cloudContext,
key: flow.binding.vnKey,
id: vnId,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
prepareStatus: 'ready',
vnId,
vnPhone,
bindUrl: bindUrlResult.bindUrl,
bindPreparedAt: now,
},
purchase: {
...flow.purchase,
usedKnapsack,
purchaseTriggered,
assetBefore,
assetAfter,
purchaseAt: purchaseTriggered ? now : flow.purchase.purchaseAt,
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: 'waiting_binding',
inventory_status: 'not_required',
user_action_status: 'pending',
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(task.id, 'kuaishou_cloud_binding_prepared', {
skuId: flow.binding.skuId,
vnKey: flow.binding.vnKey,
vnId,
vnPhoneMasked: maskPhone(vnPhone),
bindUrl: bindUrlResult.bindUrl,
purchaseTriggered,
usedKnapsack,
}, now)
return {
task: mapTaskActionPayload(updatedTask),
}
}
/** @returns {Promise<AdminTaskActionResponse>} */
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminTaskKuaishouCloudDispatchInput} [payload] */
/** @param {AdminViewerSessionInput | null} [session] */
export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload = {}, session = null) {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限执行发货', {
statusCode: 403,
errorCode: 'admin_task_dispatch_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 cloud 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const cloudContext = resolvePersistedCloudtentaclesContext()
if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) {
throw createHttpError('当前任务还没有准备好绑定资源,请先准备绑定资源', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_not_prepared',
})
}
const ticketCode = String(payload.ticketCode || '').trim()
const dispatchResult = await useCloudtentaclesSku({
...cloudContext,
id: flow.binding.skuId,
virtualNumberId: flow.binding.vnId,
phone: flow.binding.vnPhone,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
ticket: {
...flow.ticket,
code: ticketCode || flow.ticket.code,
capturedAt: ticketCode ? now : flow.ticket.capturedAt,
capturedBy: ticketCode && session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: flow.ticket.capturedBy,
},
dispatch: {
...flow.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: '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(ticketCode),
skuId: flow.binding.skuId,
vnId: flow.binding.vnId,
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
sendType: dispatchResult.sendType,
note: dispatchResult.note,
}, now)
return {
task: mapTaskActionPayload(updatedTask),
}
}
/** @returns {Promise<AdminTaskActionResponse>} */
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, session = null) {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限退还号码', {
statusCode: 403,
errorCode: 'admin_task_return_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 cloud 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const cloudContext = resolvePersistedCloudtentaclesContext()
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError('当前任务缺少可退还的虚拟号信息', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_return_context',
})
}
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
returnNumber: {
...flow.returnNumber,
status: 'success',
returnedAt: now,
returnedBy: session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null,
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: 'completed',
delivery_status: 'delivered',
result_code: 'kuaishou_cloud_completed',
result_message: 'cloudtentacles 发货并退号完成',
redeemed_at: now,
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(task.id, 'kuaishou_cloud_number_returned', {
vnId: flow.binding.vnId,
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
}, now)
return {
task: mapTaskActionPayload(updatedTask),
}
}
/**
* @param {AdminInventoryImportInput} payload
* @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>}
@@ -725,6 +1089,116 @@ function normalizeInventoryImportRows(payload) {
return dedupeRows(rows)
}
function isKuaishouCloudTask(task) {
return String(task?.executor_key || '').trim() === 'kuaishou_ct_assisted'
}
function resolvePersistedCloudtentaclesContext() {
const source = getCloudtentaclesSourceConfig()
const session = getCloudtentaclesSessionState()
const token = String(session.token || '').trim()
if (!token) {
throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_cloud_token',
})
}
return {
baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176',
token,
deviceId: String(session.deviceId || source.deviceId || '-').trim() || '-',
deviceType: Number(session.deviceType ?? source.deviceType ?? 0),
}
}
function normalizeKuaishouCloudFlow(value) {
const source = value && typeof value === 'object' ? value : {}
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
return {
...source,
configId: String(source.configId || '').trim(),
internalSkuCode: String(source.internalSkuCode || '').trim(),
internalSkuName: String(source.internalSkuName || '').trim(),
ticket: {
code: String(ticket.code || '').trim(),
capturedAt: ticket.capturedAt || null,
capturedBy: ticket.capturedBy || null,
},
binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKey: String(binding.cloudSourceKey || 'default').trim() || 'default',
skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || '').trim(),
vnKey: String(binding.vnKey || '').trim(),
vnId: Number(binding.vnId || 0) || 0,
vnPhone: String(binding.vnPhone || '').trim(),
bindUrl: String(binding.bindUrl || '').trim(),
bindPreparedAt: binding.bindPreparedAt || null,
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
usedKnapsack: purchase.usedKnapsack === true,
purchaseTriggered: purchase.purchaseTriggered === true,
assetBefore: Number(purchase.assetBefore || 0) || 0,
assetAfter: Number(purchase.assetAfter || 0) || 0,
purchaseAt: purchase.purchaseAt || null,
},
dispatch: {
status: String(dispatch.status || 'pending').trim() || 'pending',
dispatchAt: dispatch.dispatchAt || null,
dispatchBy: dispatch.dispatchBy || null,
sendType: Number(dispatch.sendType || 0) || 0,
note: String(dispatch.note || '').trim(),
},
returnNumber: {
status: String(returnNumber.status || 'pending').trim() || 'pending',
returnedAt: returnNumber.returnedAt || null,
returnedBy: returnNumber.returnedBy || null,
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
},
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
},
}
}
function maskPhone(value) {
const text = String(value || '').trim()
if (!text) {
return ''
}
if (text.length <= 7) {
return `${text.slice(0, 2)}***${text.slice(-2)}`
}
return `${text.slice(0, 3)}****${text.slice(-4)}`
}
function maskCode(value) {
const text = String(value || '').trim()
if (!text) {
return ''
}
if (text.length <= 8) {
return `${text.slice(0, 2)}***${text.slice(-2)}`
}
return `${text.slice(0, 4)}****${text.slice(-4)}`
}
/**
* @param {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>} rows
*/
@@ -797,14 +1271,3 @@ function normalizeManualDispatchOutcome(value) {
function getTaskClaimExpiresAt(task) {
return task?.claim_expires_at || task?.primary_claim_expires_at || null
}
function maskCode(value) {
const text = String(value || '').trim()
if (!text) {
return ''
}
if (text.length <= 8) {
return `${text.slice(0, 2)}****${text.slice(-2)}`
}
return `${text.slice(0, 4)}****${text.slice(-4)}`
}