优化了任务
This commit is contained in:
@@ -20,6 +20,7 @@ import { getWebhookEventById } from '../../repositories/webhook-event-repo.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { confirmClaimRoleForAdminTask, redeemClaimTaskForAdminTask } from '../claim/claim-session-service.js'
|
||||
import { reserveInventoryForTask } from '../order/inventory-service.js'
|
||||
import { normalizeProductName } from '../order/product-match-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'
|
||||
@@ -75,6 +76,8 @@ import {
|
||||
/** @typedef {import('../../types/admin-write-models.js').AdminWebhookReplayResponse} AdminWebhookReplayResponse */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchInput */
|
||||
|
||||
const KUAISHOU_CLOUD_FIXED_VN_KEY = '1'
|
||||
|
||||
/** @returns {Promise<AdminInventoryMutationResponse>} */
|
||||
/** @param {AdminInventoryCreateInput} [payload] */
|
||||
export async function createAdminInventoryItem(payload = /** @type {AdminInventoryCreateInput} */ ({})) {
|
||||
@@ -718,37 +721,54 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext()
|
||||
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 (!flow.binding.skuId || !flow.binding.vnKey) {
|
||||
throw createHttpError('当前任务缺少 cloud SKU 或 VN Key 配置', {
|
||||
if (!resolvedBinding.skuId || vnKeyCandidates.length === 0) {
|
||||
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
|
||||
const flowWithResolvedBinding = {
|
||||
...flow,
|
||||
binding: {
|
||||
...flow.binding,
|
||||
skuId: resolvedBinding.skuId,
|
||||
skuName: resolvedBinding.skuName,
|
||||
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
},
|
||||
}
|
||||
const knapsackItem = resolvedBinding.knapsackItem
|
||||
let usedKnapsack = Number(knapsackItem?.count || 0) > 0
|
||||
let purchaseTriggered = false
|
||||
let assetBefore = 0
|
||||
let assetAfter = 0
|
||||
|
||||
if (!usedKnapsack) {
|
||||
if (!flow.purchase.autoBuyEnabled) {
|
||||
if (!flowWithResolvedBinding.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
|
||||
const asset = await getCloudtentaclesAsset(cloudContext)
|
||||
const targetSku = resolvedBinding.skuItem
|
||||
|
||||
if (!targetSku) {
|
||||
throw createHttpError(`cloudtentacles 未找到 SKU ${flow.binding.skuId}`, {
|
||||
throw createHttpError(`cloudtentacles 未找到 SKU ${flowWithResolvedBinding.binding.skuId}`, {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_task_kuaishou_cloud_sku_not_found',
|
||||
})
|
||||
@@ -756,7 +776,7 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
|
||||
assetBefore = Number(asset.asset || 0) || 0
|
||||
const targetPrice = Number(targetSku.price || 0) || 0
|
||||
const requiredAsset = targetPrice + flow.purchase.minAssetReserve
|
||||
const requiredAsset = targetPrice + flowWithResolvedBinding.purchase.minAssetReserve
|
||||
if (assetBefore < requiredAsset) {
|
||||
throw createHttpError(`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, {
|
||||
statusCode: 409,
|
||||
@@ -766,7 +786,7 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
|
||||
await buyCloudtentaclesSku({
|
||||
...cloudContext,
|
||||
id: flow.binding.skuId,
|
||||
id: flowWithResolvedBinding.binding.skuId,
|
||||
count: 1,
|
||||
})
|
||||
purchaseTriggered = true
|
||||
@@ -775,51 +795,23 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
assetAfter = Number(assetResult.asset || 0) || 0
|
||||
}
|
||||
|
||||
const appointed = await appointCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
|
||||
cloudContext,
|
||||
vnKeyCandidates,
|
||||
})
|
||||
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',
|
||||
})
|
||||
const vnId = preparedBinding.vnId
|
||||
const vnPhone = preparedBinding.vnPhone
|
||||
const bindUrlResult = {
|
||||
bindUrl: preparedBinding.bindUrl,
|
||||
}
|
||||
|
||||
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,
|
||||
...flowWithResolvedBinding,
|
||||
binding: {
|
||||
...flow.binding,
|
||||
...flowWithResolvedBinding.binding,
|
||||
vnKey: preparedBinding.vnKey,
|
||||
prepareStatus: 'ready',
|
||||
vnId,
|
||||
vnPhone,
|
||||
@@ -827,12 +819,12 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
bindPreparedAt: now,
|
||||
},
|
||||
purchase: {
|
||||
...flow.purchase,
|
||||
...flowWithResolvedBinding.purchase,
|
||||
usedKnapsack,
|
||||
purchaseTriggered,
|
||||
assetBefore,
|
||||
assetAfter,
|
||||
purchaseAt: purchaseTriggered ? now : flow.purchase.purchaseAt,
|
||||
purchaseAt: purchaseTriggered ? now : flowWithResolvedBinding.purchase.purchaseAt,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -847,13 +839,15 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_binding_prepared', {
|
||||
skuId: flow.binding.skuId,
|
||||
vnKey: flow.binding.vnKey,
|
||||
skuId: flowWithResolvedBinding.binding.skuId,
|
||||
skuName: flowWithResolvedBinding.binding.skuName,
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId,
|
||||
vnPhoneMasked: maskPhone(vnPhone),
|
||||
bindUrl: bindUrlResult.bindUrl,
|
||||
purchaseTriggered,
|
||||
usedKnapsack,
|
||||
resolvedByName: resolvedBinding.resolvedByName,
|
||||
}, now)
|
||||
|
||||
return {
|
||||
@@ -1173,6 +1167,196 @@ function normalizeKuaishouCloudFlow(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKuaishouCloudBindingResources(flow, { skuItems = [], knapsackItems = [] } = {}) {
|
||||
const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : []
|
||||
const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) : []
|
||||
const currentSkuId = Number(flow?.binding?.skuId || 0) || 0
|
||||
const currentVnKey = String(flow?.binding?.vnKey || '').trim()
|
||||
const currentSkuName = String(flow?.binding?.skuName || '').trim()
|
||||
const nameCandidates = collectKuaishouCloudNameCandidates(flow)
|
||||
|
||||
const skuItemById = currentSkuId > 0
|
||||
? normalizedSkuItems.find((item) => Number(item.id || 0) === currentSkuId) || null
|
||||
: null
|
||||
const knapsackItemById = currentSkuId > 0
|
||||
? normalizedKnapsackItems.find((item) => Number(item.id || 0) === currentSkuId) || null
|
||||
: null
|
||||
|
||||
if (skuItemById || knapsackItemById) {
|
||||
const matchedItem = skuItemById || knapsackItemById
|
||||
return {
|
||||
skuId: Number(matchedItem?.id || 0) || 0,
|
||||
skuName: String(currentSkuName || matchedItem?.name || '').trim(),
|
||||
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
skuItem: skuItemById,
|
||||
knapsackItem: knapsackItemById,
|
||||
resolvedByName: false,
|
||||
}
|
||||
}
|
||||
|
||||
const matchedSkuItem = findCloudItemByNames(normalizedSkuItems, nameCandidates)
|
||||
const matchedKnapsackItem = findCloudItemByNames(
|
||||
normalizedKnapsackItems,
|
||||
nameCandidates,
|
||||
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0,
|
||||
)
|
||||
const matchedItem = matchedSkuItem || matchedKnapsackItem
|
||||
|
||||
return {
|
||||
skuId: Number(matchedItem?.id || 0) || 0,
|
||||
skuName: String(currentSkuName || matchedItem?.name || '').trim(),
|
||||
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
skuItem: matchedSkuItem,
|
||||
knapsackItem: matchedKnapsackItem,
|
||||
resolvedByName: Boolean(matchedItem),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ flow?: any, binding?: any }} [input]
|
||||
*/
|
||||
function resolveKuaishouCloudVnKeyCandidates(input = {}) {
|
||||
return [KUAISHOU_CLOUD_FIXED_VN_KEY]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ cloudContext?: Record<string, unknown>, vnKeyCandidates?: string[] }} [input]
|
||||
*/
|
||||
async function prepareKuaishouCloudBindResourceWithFallback(input = {}) {
|
||||
const { cloudContext = {}, vnKeyCandidates = [] } = input
|
||||
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : []
|
||||
let lastError = null
|
||||
|
||||
for (const vnKey of candidates) {
|
||||
let vnId = 0
|
||||
let vnPhone = ''
|
||||
|
||||
try {
|
||||
const appointed = await appointCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
})
|
||||
vnId = Number(appointed.item?.id || 0)
|
||||
vnPhone = String(appointed.item?.phone || '').trim()
|
||||
|
||||
if (!vnId || !vnPhone) {
|
||||
throw createHttpError('申请虚拟号成功但返回数据不完整', {
|
||||
statusCode: 502,
|
||||
errorCode: 'admin_task_kuaishou_cloud_invalid_vn',
|
||||
})
|
||||
}
|
||||
|
||||
await generateCloudtentaclesLoginCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
})
|
||||
|
||||
const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
phone: vnPhone,
|
||||
})
|
||||
|
||||
await verifyCloudtentaclesLoginCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
code: fetchedCode.code,
|
||||
})
|
||||
|
||||
const bindUrlResult = await getCloudtentaclesBindUrl({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
})
|
||||
|
||||
return {
|
||||
vnKey,
|
||||
vnId,
|
||||
vnPhone,
|
||||
bindUrl: String(bindUrlResult.bindUrl || '').trim(),
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
|
||||
if (vnId > 0) {
|
||||
try {
|
||||
await backCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
})
|
||||
} catch {
|
||||
// 兜底退号失败时保留原始错误,避免吞掉主链路异常
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRecoverableKuaishouCloudVnKeyError(error)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || createHttpError('没有找到可用的 VN Key', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_missing_binding_config',
|
||||
})
|
||||
}
|
||||
|
||||
function collectKuaishouCloudNameCandidates(flow) {
|
||||
return Array.from(new Set([
|
||||
String(flow?.binding?.skuName || '').trim(),
|
||||
String(flow?.internalSkuName || '').trim(),
|
||||
String(flow?.internalSkuCode || '').trim(),
|
||||
].filter(Boolean)))
|
||||
}
|
||||
|
||||
function findCloudItemByNames(items, nameCandidates, preferredId = 0) {
|
||||
const normalizedItems = Array.isArray(items) ? items : []
|
||||
const normalizedNames = nameCandidates
|
||||
.map((item) => ({
|
||||
raw: String(item || '').trim(),
|
||||
normalized: normalizeProductName(item),
|
||||
}))
|
||||
.filter((item) => item.raw && item.normalized)
|
||||
|
||||
if (normalizedNames.length === 0 || normalizedItems.length === 0) {
|
||||
return preferredId > 0
|
||||
? normalizedItems.find((item) => Number(item.id || 0) === preferredId) || null
|
||||
: null
|
||||
}
|
||||
|
||||
if (preferredId > 0) {
|
||||
const preferred = normalizedItems.find((item) => Number(item.id || 0) === preferredId) || null
|
||||
if (preferred) {
|
||||
return preferred
|
||||
}
|
||||
}
|
||||
|
||||
const exactMatches = normalizedItems.filter((item) => {
|
||||
const itemName = normalizeProductName(item.name)
|
||||
return normalizedNames.some((candidate) => candidate.normalized === itemName)
|
||||
})
|
||||
if (exactMatches.length > 0) {
|
||||
return exactMatches[0]
|
||||
}
|
||||
|
||||
const partialMatches = normalizedItems.filter((item) => {
|
||||
const itemName = normalizeProductName(item.name)
|
||||
return normalizedNames.some((candidate) => itemName.includes(candidate.normalized) || candidate.normalized.includes(itemName))
|
||||
})
|
||||
if (partialMatches.length > 0) {
|
||||
return partialMatches.sort((left, right) => String(left.name || '').length - String(right.name || '').length)[0]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function isCloudSkuLikeItem(item) {
|
||||
return Boolean(item) && typeof item === 'object' && Number(item.id || 0) > 0
|
||||
}
|
||||
|
||||
function maskPhone(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
@@ -1258,6 +1442,13 @@ function isRecoverableTaskSessionCloseError(error) {
|
||||
return errorCode === 'session_not_found' || errorCode === 'session_closed'
|
||||
}
|
||||
|
||||
function isRecoverableKuaishouCloudVnKeyError(error) {
|
||||
const errorCode = String(error?.errorCode || error?.code || '').trim()
|
||||
const errorMessage = String(error?.message || '').trim()
|
||||
return errorCode === 'cloudtentacles_vn_bind_url_failed'
|
||||
&& errorMessage.includes('不支持的游戏类型')
|
||||
}
|
||||
|
||||
function normalizeManualDispatchOutcome(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
cloudSourceKey: String(fulfillmentConfig.cloudtentacles?.cloudSourceKey || 'default').trim() || 'default',
|
||||
skuId: Number(fulfillmentConfig.cloudtentacles?.skuId || 0) || 0,
|
||||
skuName: String(fulfillmentConfig.cloudtentacles?.skuName || '').trim(),
|
||||
vnKey: String(fulfillmentConfig.cloudtentacles?.vnKey || '').trim(),
|
||||
vnKey: '1',
|
||||
vnId: 0,
|
||||
vnPhone: '',
|
||||
bindUrl: '',
|
||||
|
||||
@@ -45,7 +45,7 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) {
|
||||
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
|
||||
skuId: normalizePositiveInteger(item.cloudSkuId),
|
||||
skuName: String(item.cloudSkuName || '').trim(),
|
||||
vnKey: String(item.vnKey || '').trim(),
|
||||
vnKey: '1',
|
||||
autoBuyEnabled: item.autoBuyEnabled !== false,
|
||||
minAssetReserve: normalizeNonNegativeInteger(item.minAssetReserve, 0),
|
||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
||||
@@ -99,7 +99,6 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
||||
|
||||
const internalSkuCode = String(rawValue.internalSkuCode || '').trim()
|
||||
const cloudSkuId = normalizePositiveInteger(rawValue.cloudSkuId)
|
||||
const vnKey = String(rawValue.vnKey || '').trim()
|
||||
const externalSkuCode = String(rawValue.externalSkuCode || '').trim()
|
||||
const externalItemId = String(rawValue.externalItemId || '').trim()
|
||||
const externalSkuName = String(rawValue.externalSkuName || '').trim()
|
||||
@@ -108,7 +107,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!cloudSkuId || !vnKey) {
|
||||
if (!cloudSkuId) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -132,7 +131,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
||||
cloudSourceKey: String(rawValue.cloudSourceKey || 'default').trim() || 'default',
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(rawValue.cloudSkuName || '').trim(),
|
||||
vnKey,
|
||||
vnKey: '1',
|
||||
autoBuyEnabled: rawValue.autoBuyEnabled !== false,
|
||||
minAssetReserve: normalizeNonNegativeInteger(rawValue.minAssetReserve, 0),
|
||||
autoReturnNumberAfterDispatch: rawValue.autoReturnNumberAfterDispatch === true,
|
||||
|
||||
Reference in New Issue
Block a user