客户领取页 ui与逻辑优化
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
||||
"lastRunStartedAt": "2026-05-03T15:58:17.921Z",
|
||||
"lastRunFinishedAt": "2026-05-03T15:58:18.685Z",
|
||||
"lastRunStartedAt": "2026-05-03T17:50:39.717Z",
|
||||
"lastRunFinishedAt": "2026-05-03T17:50:40.412Z",
|
||||
"lastRunStatus": "success",
|
||||
"lastErrorMessage": "",
|
||||
"fetchedCount": 40,
|
||||
"syncedCount": 1,
|
||||
"ignoredCount": 39,
|
||||
"lastOrderCreatedAt": "2026-05-03T15:57:42.000Z",
|
||||
"syncedCount": 0,
|
||||
"ignoredCount": 40,
|
||||
"lastOrderCreatedAt": "2026-05-03T17:47:36.000Z",
|
||||
"watchMode": "normal",
|
||||
"watchOrders": []
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
redeemClaimTask,
|
||||
} from '../services/claim/claim-session-service.js'
|
||||
import {
|
||||
confirmKuaishouCloudClaimRole,
|
||||
getKuaishouCloudClaimGuideAssetPath,
|
||||
redeemKuaishouCloudClaim,
|
||||
verifyKuaishouCloudClaimTicket,
|
||||
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
||||
import { buildNotFoundPayload, buildSuccessPayload, sendRouteError } from '../utils/http.js'
|
||||
@@ -90,6 +92,24 @@ router.post('/:token/kuaishou-cloud/verify-ticket', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:token/kuaishou-cloud/confirm-role', async (req, res) => {
|
||||
try {
|
||||
const data = await confirmKuaishouCloudClaimRole(req.params.token)
|
||||
res.json(buildSuccessPayload(data, '角色已确认'))
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '确认角色失败', '[claims/:token/kuaishou-cloud/confirm-role]')
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:token/kuaishou-cloud/redeem', async (req, res) => {
|
||||
try {
|
||||
const data = await redeemKuaishouCloudClaim(req.params.token)
|
||||
res.json(buildSuccessPayload(data, '兑换请求已提交'))
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '兑换失败', '[claims/:token/kuaishou-cloud/redeem]')
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:token/screenshot', async (req, res) => {
|
||||
try {
|
||||
const screenshotPath = await getClaimScreenshotPath(req.params.token)
|
||||
|
||||
@@ -824,6 +824,16 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
vnPhone,
|
||||
bindUrl: bindUrlResult.bindUrl,
|
||||
bindPreparedAt: now,
|
||||
roleName: '',
|
||||
roleId: '',
|
||||
},
|
||||
role: {
|
||||
status: 'pending',
|
||||
name: '',
|
||||
rid: '',
|
||||
refreshedAt: null,
|
||||
errorMessage: '',
|
||||
rawInfo: null,
|
||||
},
|
||||
purchase: {
|
||||
...flowWithResolvedBinding.purchase,
|
||||
@@ -1017,6 +1027,11 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(taskId, session = nu
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
binding: {
|
||||
...flow.binding,
|
||||
roleName: bindInfo.name,
|
||||
roleId: bindInfo.rid,
|
||||
},
|
||||
role: {
|
||||
status: bindInfo.name || bindInfo.rid ? 'ready' : 'pending',
|
||||
name: bindInfo.name,
|
||||
@@ -1029,8 +1044,8 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(taskId, session = nu
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
role_id: bindInfo.rid || task.role_id || '',
|
||||
role_name: bindInfo.name || task.role_name || '',
|
||||
role_id: bindInfo.rid || '',
|
||||
role_name: bindInfo.name || '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { reserveInventoryForTask } from '../order/inventory-service.js'
|
||||
import { buildClaimUrl } from './claim-service.js'
|
||||
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
|
||||
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
@@ -33,7 +34,13 @@ const KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH = '/kuaishou-cloud-guide'
|
||||
|
||||
export async function getClaimDetail(token, { includeQrImage = true } = {}) {
|
||||
const context = await getClaimContext(token)
|
||||
const { task, session } = await loadTaskSession(context.task, { includeQrImage })
|
||||
let { task, session } = await loadTaskSession(context.task, { includeQrImage })
|
||||
|
||||
// 如果是快手 Cloud 流程,尝试同步角色信息
|
||||
if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
|
||||
task = await syncKuaishouCloudRoleInfo(task)
|
||||
}
|
||||
|
||||
const syncedTask = session ? await syncTaskWithSession(task, session) : task
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
@@ -879,6 +886,8 @@ function mapClaimKuaishouCloudFulfillment(task, order) {
|
||||
}
|
||||
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
const role = source.role && typeof source.role === 'object' ? source.role : {}
|
||||
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
|
||||
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
|
||||
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
|
||||
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
|
||||
@@ -903,9 +912,32 @@ function mapClaimKuaishouCloudFulfillment(task, order) {
|
||||
},
|
||||
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,
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
bindPreparedAt: binding.bindPreparedAt || null,
|
||||
vnPhone: String(binding.vnPhone || '').trim(),
|
||||
roleName: String(binding.roleName || '').trim(),
|
||||
roleId: String(binding.roleId || '').trim(),
|
||||
},
|
||||
role: {
|
||||
status: String(role.status || 'pending').trim() || 'pending',
|
||||
name: String(role.name || binding.roleName || '').trim(),
|
||||
rid: String(role.rid || binding.roleId || '').trim(),
|
||||
refreshedAt: role.refreshedAt || null,
|
||||
errorMessage: String(role.errorMessage || '').trim(),
|
||||
},
|
||||
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',
|
||||
|
||||
@@ -11,6 +11,12 @@ import {
|
||||
resolveKuaishouEticketShopConfig,
|
||||
} from '../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import { queryKuaishouEticketConsumeDetail } from '../platforms/kuaishou-eticket/consume-service.js'
|
||||
import {
|
||||
dispatchKuaishouCloudFulfillmentTask,
|
||||
normalizeKuaishouCloudFlow,
|
||||
prepareKuaishouCloudFulfillmentTask,
|
||||
refreshKuaishouCloudTaskRoleInfo,
|
||||
} from '../fulfillment/kuaishou-cloud-task-service.js'
|
||||
import { getClaimContext, getClaimDetail } from './claim-session-service.js'
|
||||
|
||||
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
|
||||
@@ -28,12 +34,6 @@ export async function verifyKuaishouCloudClaimTicket(token, payload = {}) {
|
||||
}
|
||||
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(context.task).kuaishouCloudFulfillment)
|
||||
if (!flow.binding.bindUrl || flow.binding.prepareStatus !== 'ready') {
|
||||
throw createHttpError('当前任务还没有准备好绑定资源,请联系客服稍后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_binding_not_ready',
|
||||
})
|
||||
}
|
||||
|
||||
const ticketCode = String(payload.ticketCode || payload.eTicketId || '').trim()
|
||||
if (!ticketCode) {
|
||||
@@ -96,7 +96,7 @@ export async function verifyKuaishouCloudClaimTicket(token, payload = {}) {
|
||||
},
|
||||
}
|
||||
|
||||
await updateTask(context.task.id, {
|
||||
const updatedTask = await updateTask(context.task.id, {
|
||||
claim_token: context.claimToken.token,
|
||||
claim_expires_at: context.claimToken.expired_at,
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
@@ -112,6 +112,14 @@ export async function verifyKuaishouCloudClaimTicket(token, payload = {}) {
|
||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
||||
}, now)
|
||||
|
||||
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
|
||||
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
|
||||
await prepareKuaishouCloudFulfillmentTask(updatedTask, {
|
||||
source: 'claim_page_ticket_verified',
|
||||
actor: { source: 'claim_page' },
|
||||
})
|
||||
}
|
||||
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
}
|
||||
|
||||
@@ -135,47 +143,6 @@ export async function getKuaishouCloudClaimGuideAssetPath(filename) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
function normalizeKuaishouCloudFlow(value) {
|
||||
const source = value && typeof value === 'object' ? value : {}
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
|
||||
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
|
||||
|
||||
return {
|
||||
...source,
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
capturedBy: ticket.capturedBy || null,
|
||||
verifiedAt: ticket.verifiedAt || null,
|
||||
oid: String(ticket.oid || '').trim(),
|
||||
formToken: String(ticket.formToken || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
},
|
||||
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,
|
||||
},
|
||||
consume: {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
shopId: String(consume.shopId || '').trim(),
|
||||
shopName: String(consume.shopName || '').trim(),
|
||||
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
||||
consumedAt: consume.consumedAt || null,
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function parseTaskContext(task) {
|
||||
const rawValue = task?.context_json
|
||||
if (!rawValue) {
|
||||
@@ -193,11 +160,84 @@ function parseTaskContext(task) {
|
||||
}
|
||||
}
|
||||
|
||||
function maskCode(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized.length <= 6) {
|
||||
return normalized
|
||||
export async function confirmKuaishouCloudClaimRole(token) {
|
||||
const context = await getClaimContext(token)
|
||||
const now = nowIso()
|
||||
|
||||
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_not_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
|
||||
return `${normalized.slice(0, 3)}***${normalized.slice(-3)}`
|
||||
if (['role_confirmed', 'dispatched_pending_return', 'completed'].includes(String(context.task.task_status || '').trim())) {
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
}
|
||||
|
||||
const refreshed = await refreshKuaishouCloudTaskRoleInfo(context.task, {
|
||||
source: 'claim_page_role_confirm',
|
||||
actor: { source: 'claim_page' },
|
||||
recordEvent: false,
|
||||
})
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
||||
|
||||
if (flow.ticket.status !== 'verified') {
|
||||
throw createHttpError('请先验证核销码', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_ticket_not_verified',
|
||||
})
|
||||
}
|
||||
|
||||
if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) {
|
||||
throw createHttpError('角色信息还未刷新到系统,请完成绑定后稍等片刻再试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_role_not_ready',
|
||||
})
|
||||
}
|
||||
|
||||
await updateTask(context.task.id, {
|
||||
task_status: 'role_confirmed',
|
||||
role_id: flow.binding.roleId,
|
||||
role_name: flow.binding.roleName,
|
||||
user_action_status: 'role_confirmed',
|
||||
role_confirmed_at: now,
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(context.task.id, 'kuaishou_cloud_role_confirmed', {
|
||||
vnPhone: flow.binding.vnPhone,
|
||||
roleName: flow.binding.roleName,
|
||||
roleId: flow.binding.roleId,
|
||||
}, now)
|
||||
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
}
|
||||
|
||||
export async function redeemKuaishouCloudClaim(token) {
|
||||
const context = await getClaimContext(token)
|
||||
const now = nowIso()
|
||||
|
||||
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_not_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
|
||||
if (context.task.task_status !== 'role_confirmed') {
|
||||
throw createHttpError('请先确认角色信息', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_role_not_confirmed',
|
||||
})
|
||||
}
|
||||
|
||||
await dispatchKuaishouCloudFulfillmentTask(context.task, {
|
||||
source: 'claim_page_redeem',
|
||||
actor: { source: 'claim_page' },
|
||||
autoFinalize: true,
|
||||
})
|
||||
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
normalizeKuaishouCloudFlow,
|
||||
refreshKuaishouCloudTaskRoleInfo,
|
||||
} from '../fulfillment/kuaishou-cloud-task-service.js'
|
||||
|
||||
export async function syncKuaishouCloudRoleInfo(task) {
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey || !flow.binding.vnPhone) {
|
||||
return task
|
||||
}
|
||||
|
||||
if (flow.binding.roleName && flow.binding.roleId) {
|
||||
return task
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await refreshKuaishouCloudTaskRoleInfo(task, {
|
||||
source: 'claim_page_polling',
|
||||
actor: { source: 'system' },
|
||||
recordEvent: false,
|
||||
})
|
||||
return result.task
|
||||
} catch {
|
||||
return task
|
||||
}
|
||||
}
|
||||
|
||||
function parseTaskContext(task) {
|
||||
const rawValue = task?.context_json
|
||||
|
||||
if (!rawValue) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'object') {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(String(rawValue || '{}'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,945 @@
|
||||
// @ts-check
|
||||
|
||||
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
||||
import { getOrderById } from '../../repositories/order-repo.js'
|
||||
import { updateTask } from '../../repositories/task-repo.js'
|
||||
import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { normalizeProductName } from '../order/product-match-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,
|
||||
getCloudtentaclesBindInfo,
|
||||
getCloudtentaclesBindUrl,
|
||||
verifyCloudtentaclesLoginCode,
|
||||
} from '../platforms/cloudtentacles/virtual-number-service.js'
|
||||
import { consumeKuaishouEticket } from '../platforms/kuaishou-eticket/consume-service.js'
|
||||
import {
|
||||
getKuaishouEticketSourceConfig,
|
||||
resolveKuaishouEticketShopConfig,
|
||||
} from '../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
const KUAISHOU_CLOUD_FIXED_VN_KEY = '1'
|
||||
|
||||
export function isKuaishouCloudTask(task) {
|
||||
return String(task?.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
}
|
||||
|
||||
export function normalizeKuaishouCloudFlow(value) {
|
||||
const source = value && typeof value === 'object' ? value : {}
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
const role = source.role && typeof source.role === 'object' ? source.role : {}
|
||||
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 : {}
|
||||
|
||||
const roleName = String(role.name || binding.roleName || '').trim()
|
||||
const roleId = String(role.rid || binding.roleId || '').trim()
|
||||
|
||||
return {
|
||||
...source,
|
||||
configId: String(source.configId || '').trim(),
|
||||
internalSkuCode: String(source.internalSkuCode || '').trim(),
|
||||
internalSkuName: String(source.internalSkuName || '').trim(),
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
capturedBy: ticket.capturedBy || null,
|
||||
verifiedAt: ticket.verifiedAt || null,
|
||||
oid: String(ticket.oid || '').trim(),
|
||||
formToken: String(ticket.formToken || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
},
|
||||
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 || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
vnId: Number(binding.vnId || 0) || 0,
|
||||
vnPhone: String(binding.vnPhone || '').trim(),
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
bindPreparedAt: binding.bindPreparedAt || null,
|
||||
roleName,
|
||||
roleId,
|
||||
},
|
||||
role: {
|
||||
status: String(role.status || (roleName || roleId ? 'ready' : 'pending')).trim() || 'pending',
|
||||
name: roleName,
|
||||
rid: roleId,
|
||||
refreshedAt: role.refreshedAt || null,
|
||||
errorMessage: String(role.errorMessage || '').trim(),
|
||||
rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : 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(),
|
||||
shopName: String(consume.shopName || '').trim(),
|
||||
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
||||
consumedAt: consume.consumedAt || null,
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
notes: String(source.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeKuaishouCloudRoleInfo(value) {
|
||||
const rawInfo = value && typeof value === 'object' ? value : null
|
||||
|
||||
return {
|
||||
name: String(rawInfo?.name || rawInfo?.roleName || rawInfo?.nickname || '').trim(),
|
||||
rid: String(rawInfo?.rid || rawInfo?.roleId || rawInfo?.uid || '').trim(),
|
||||
rawInfo,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePersistedCloudtentaclesContext() {
|
||||
const source = getCloudtentaclesSourceConfig()
|
||||
const session = getCloudtentaclesSessionState()
|
||||
const token = String(session.token || '').trim()
|
||||
|
||||
if (!token) {
|
||||
throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', {
|
||||
statusCode: 409,
|
||||
errorCode: '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),
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureTaskClaimLink(task) {
|
||||
const tokenStatus = String(task?.primary_claim_token_status || '').trim()
|
||||
const token = String(task?.primary_claim_token || task?.claim_token || '').trim()
|
||||
const expiredAt = getTaskClaimExpiresAt(task)
|
||||
|
||||
if (tokenStatus === 'active' && token && !isClaimExpired(expiredAt)) {
|
||||
return {
|
||||
token,
|
||||
expiredAt,
|
||||
claimUrl: buildClaimUrl(token),
|
||||
}
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
return {
|
||||
token: claimToken.token,
|
||||
expiredAt: claimToken.expired_at,
|
||||
claimUrl: claimToken.claimUrl,
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepareKuaishouCloudFulfillmentTask(task, options = {}) {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_task_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const actor = normalizeActor(options.actor)
|
||||
const force = options.force === true
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
const claimLinkState = await ensureTaskClaimLink(task)
|
||||
|
||||
if (!force && flow.binding.prepareStatus === 'ready' && flow.binding.vnId > 0 && flow.binding.vnPhone && flow.binding.bindUrl) {
|
||||
let readyTask = task
|
||||
|
||||
if (String(task.task_status || '').trim() !== 'waiting_binding') {
|
||||
readyTask = await updateTask(task.id, {
|
||||
task_status: 'waiting_binding',
|
||||
claim_token: claimLinkState.token || task.claim_token || '',
|
||||
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
task: readyTask,
|
||||
claimUrl: claimLinkState.claimUrl,
|
||||
token: claimLinkState.token,
|
||||
flow,
|
||||
}
|
||||
}
|
||||
|
||||
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 (!resolvedBinding.skuId || vnKeyCandidates.length === 0) {
|
||||
throw createHttpError('当前任务缺少可用 cloud 资源,无法自动准备绑定', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_missing_binding_config',
|
||||
})
|
||||
}
|
||||
|
||||
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 (!flowWithResolvedBinding.purchase.autoBuyEnabled) {
|
||||
throw createHttpError('背包中没有现成库存,且当前配置未开启自动购买', {
|
||||
statusCode: 409,
|
||||
errorCode: '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: '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: '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 nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flowWithResolvedBinding,
|
||||
binding: {
|
||||
...flowWithResolvedBinding.binding,
|
||||
vnKey: preparedBinding.vnKey,
|
||||
prepareStatus: 'ready',
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhone: preparedBinding.vnPhone,
|
||||
bindUrl: preparedBinding.bindUrl,
|
||||
bindPreparedAt: now,
|
||||
roleName: '',
|
||||
roleId: '',
|
||||
},
|
||||
role: {
|
||||
status: 'pending',
|
||||
name: '',
|
||||
rid: '',
|
||||
refreshedAt: null,
|
||||
errorMessage: '',
|
||||
rawInfo: null,
|
||||
},
|
||||
purchase: {
|
||||
...flowWithResolvedBinding.purchase,
|
||||
usedKnapsack,
|
||||
purchaseTriggered,
|
||||
assetBefore,
|
||||
assetAfter,
|
||||
purchaseAt: purchaseTriggered ? now : flowWithResolvedBinding.purchase.purchaseAt,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: 'waiting_binding',
|
||||
inventory_status: 'not_required',
|
||||
user_action_status: 'pending_claim',
|
||||
claim_token: claimLinkState.token || task.claim_token || '',
|
||||
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
|
||||
role_id: '',
|
||||
role_name: '',
|
||||
last_error: '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_binding_prepared', {
|
||||
source: String(options.source || 'system').trim() || 'system',
|
||||
skuId: flowWithResolvedBinding.binding.skuId,
|
||||
skuName: flowWithResolvedBinding.binding.skuName,
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
|
||||
purchaseTriggered,
|
||||
usedKnapsack,
|
||||
resolvedByName: resolvedBinding.resolvedByName,
|
||||
actor,
|
||||
}, now)
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
claimUrl: claimLinkState.claimUrl,
|
||||
token: claimLinkState.token,
|
||||
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshKuaishouCloudTaskRoleInfo(task, options = {}) {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_task_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const actor = normalizeActor(options.actor)
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError('当前任务还没有可查询的绑定角色信息,请先准备绑定资源', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_missing_bind_info_context',
|
||||
})
|
||||
}
|
||||
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext()
|
||||
const bindInfoResult = await getCloudtentaclesBindInfo({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
id: flow.binding.vnId,
|
||||
})
|
||||
|
||||
const bindInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
|
||||
const hasRoleInfo = Boolean(bindInfo.name || bindInfo.rid)
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
binding: {
|
||||
...flow.binding,
|
||||
roleName: hasRoleInfo ? bindInfo.name : '',
|
||||
roleId: hasRoleInfo ? bindInfo.rid : '',
|
||||
},
|
||||
role: {
|
||||
status: hasRoleInfo ? 'ready' : 'pending',
|
||||
name: hasRoleInfo ? bindInfo.name : '',
|
||||
rid: hasRoleInfo ? bindInfo.rid : '',
|
||||
refreshedAt: now,
|
||||
errorMessage: hasRoleInfo ? '' : '当前还没有查询到角色信息,请完成绑定后稍等片刻再试',
|
||||
rawInfo: bindInfo.rawInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
role_id: hasRoleInfo ? bindInfo.rid : '',
|
||||
role_name: hasRoleInfo ? bindInfo.name : '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
if (options.recordEvent !== false) {
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_role_info_refreshed', {
|
||||
source: String(options.source || 'system').trim() || 'system',
|
||||
roleName: bindInfo.name,
|
||||
roleId: bindInfo.rid,
|
||||
vnId: flow.binding.vnId,
|
||||
actor,
|
||||
}, now)
|
||||
}
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
roleInfo: bindInfo,
|
||||
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
|
||||
}
|
||||
}
|
||||
|
||||
export async function dispatchKuaishouCloudFulfillmentTask(task, options = {}) {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
|
||||
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 = resolvePersistedCloudtentaclesContext()
|
||||
|
||||
if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) {
|
||||
throw createHttpError('当前任务还没有准备好绑定资源,请先完成绑定资源准备', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_not_prepared',
|
||||
})
|
||||
}
|
||||
|
||||
const ticketCode = String(options.ticketCode || '').trim()
|
||||
const persistedTicketCode = String(flow.ticket.code || '').trim()
|
||||
if (!persistedTicketCode && !ticketCode) {
|
||||
throw createHttpError('客户还没有提交有效核销码,暂时不能继续兑换', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_missing_ticket_code',
|
||||
})
|
||||
}
|
||||
|
||||
if (flow.dispatch.status === 'success' && String(task.task_status || '').trim() === 'dispatched_pending_return') {
|
||||
return { task, flow }
|
||||
}
|
||||
|
||||
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 || persistedTicketCode,
|
||||
capturedAt: ticketCode ? now : flow.ticket.capturedAt,
|
||||
capturedBy: ticketCode && actor ? actor : flow.ticket.capturedBy,
|
||||
},
|
||||
dispatch: {
|
||||
...flow.dispatch,
|
||||
status: 'success',
|
||||
dispatchAt: now,
|
||||
dispatchBy: actor,
|
||||
sendType: Number(dispatchResult.sendType || 0) || 0,
|
||||
note: String(dispatchResult.note || dispatchResult.responseMessage || 'cloudtentacles 发货成功').trim(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let 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(),
|
||||
user_action_status: 'not_required',
|
||||
last_error: '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_dispatched', {
|
||||
source: String(options.source || 'system').trim() || 'system',
|
||||
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
||||
skuId: flow.binding.skuId,
|
||||
vnId: flow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||
sendType: dispatchResult.sendType,
|
||||
note: dispatchResult.note,
|
||||
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',
|
||||
})
|
||||
updatedTask = finalizeResult.task
|
||||
}
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
flow: normalizeKuaishouCloudFlow(parseTaskContext(updatedTask).kuaishouCloudFulfillment),
|
||||
}
|
||||
}
|
||||
|
||||
export async function returnKuaishouCloudFulfillmentTask(task, options = {}) {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
|
||||
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 = resolvePersistedCloudtentaclesContext()
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError('当前任务缺少可退还的虚拟号信息', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_missing_return_context',
|
||||
})
|
||||
}
|
||||
|
||||
if (flow.returnNumber.status === 'success' && ['completed', 'manual_review'].includes(String(task.task_status || '').trim())) {
|
||||
return { task, flow }
|
||||
}
|
||||
|
||||
await backCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
id: flow.binding.vnId,
|
||||
})
|
||||
|
||||
const order = await getOrderById(task.order_id)
|
||||
const ticketCode = String(flow.ticket.code || '').trim()
|
||||
const shopId = String(order?.shop_id || '').trim()
|
||||
const eticketSource = getKuaishouEticketSourceConfig()
|
||||
const shopConfig = resolveKuaishouEticketShopConfig({
|
||||
shopId,
|
||||
shopName: String(order?.shop_name || '').trim(),
|
||||
})
|
||||
|
||||
let consumeStatus = 'pending'
|
||||
let consumeErrorMessage = ''
|
||||
let consumedAt = null
|
||||
let nextTaskStatus = 'completed'
|
||||
let nextResultCode = 'kuaishou_cloud_completed'
|
||||
let nextResultMessage = 'cloudtentacles 发货、退号并完成快手核销'
|
||||
|
||||
if (!order) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '任务关联订单不存在,无法执行快手核销'
|
||||
} else if (!ticketCode) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '客户未提交有效核销码,无法执行快手核销'
|
||||
} else if (!shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || '').trim()) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销'
|
||||
} else {
|
||||
try {
|
||||
const consumeResult = await consumeKuaishouEticket({
|
||||
baseUrl: eticketSource.baseUrl,
|
||||
cookie: shopConfig.cookie,
|
||||
eTicketId: ticketCode,
|
||||
oid: String(flow.ticket.oid || '').trim(),
|
||||
formToken: String(flow.ticket.formToken || '').trim(),
|
||||
})
|
||||
|
||||
if (consumeResult.consumed) {
|
||||
consumeStatus = 'success'
|
||||
consumedAt = now
|
||||
} else {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = String(consumeResult.errorMessage || '快手核销失败').trim()
|
||||
}
|
||||
} catch (error) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = error instanceof Error ? error.message : '快手核销失败'
|
||||
}
|
||||
}
|
||||
|
||||
if (consumeStatus !== 'success') {
|
||||
nextTaskStatus = 'manual_review'
|
||||
nextResultCode = 'kuaishou_cloud_consume_failed'
|
||||
nextResultMessage = consumeErrorMessage || '号码已退还,但快手核销未完成,请人工处理'
|
||||
}
|
||||
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
returnNumber: {
|
||||
...flow.returnNumber,
|
||||
status: 'success',
|
||||
returnedAt: now,
|
||||
returnedBy: actor,
|
||||
},
|
||||
consume: {
|
||||
...flow.consume,
|
||||
status: consumeStatus,
|
||||
shopId: shopId || flow.consume.shopId,
|
||||
shopName: String(flow.consume.shopName || order?.shop_name || '').trim(),
|
||||
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
||||
consumedAt,
|
||||
errorMessage: consumeErrorMessage,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: nextTaskStatus,
|
||||
delivery_status: 'delivered',
|
||||
result_code: nextResultCode,
|
||||
result_message: nextResultMessage,
|
||||
redeemed_at: consumeStatus === 'success' ? now : task.redeemed_at,
|
||||
last_error: consumeErrorMessage,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_number_returned', {
|
||||
source: String(options.source || 'system').trim() || 'system',
|
||||
vnId: flow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||
actor,
|
||||
}, now)
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
consumeStatus === 'success' ? 'kuaishou_cloud_consumed' : 'kuaishou_cloud_consume_failed',
|
||||
{
|
||||
source: String(options.source || 'system').trim() || 'system',
|
||||
ticketCodeMasked: maskCode(ticketCode),
|
||||
shopId,
|
||||
consumeStatus,
|
||||
errorMessage: consumeErrorMessage,
|
||||
actor,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
flow: normalizeKuaishouCloudFlow(parseTaskContext(updatedTask).kuaishouCloudFulfillment),
|
||||
}
|
||||
}
|
||||
|
||||
export 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)}`
|
||||
}
|
||||
|
||||
export 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)}`
|
||||
}
|
||||
|
||||
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 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),
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKuaishouCloudVnKeyCandidates() {
|
||||
return [KUAISHOU_CLOUD_FIXED_VN_KEY]
|
||||
}
|
||||
|
||||
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: '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: '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 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 normalizeActor(actor) {
|
||||
if (!actor || typeof actor !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const source = String(actor.source || '').trim()
|
||||
const userId = Number(actor.userId || 0) || 0
|
||||
const username = String(actor.username || '').trim()
|
||||
const role = String(actor.role || '').trim()
|
||||
|
||||
if (!source && !userId && !username && !role) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
userId,
|
||||
username,
|
||||
role,
|
||||
}
|
||||
}
|
||||
|
||||
function parseTaskContext(task) {
|
||||
const value = task?.context_json
|
||||
|
||||
if (!value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return value
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(String(value || '{}'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function getTaskClaimExpiresAt(task) {
|
||||
return task?.claim_expires_at || task?.primary_claim_expires_at || null
|
||||
}
|
||||
|
||||
function isClaimExpired(expiredAt) {
|
||||
if (!expiredAt) {
|
||||
return false
|
||||
}
|
||||
|
||||
const timestamp = new Date(expiredAt).getTime()
|
||||
return Number.isFinite(timestamp) && timestamp <= Date.now()
|
||||
}
|
||||
@@ -196,14 +196,28 @@ async function preparePaidTask(task) {
|
||||
'manual_review',
|
||||
'failed',
|
||||
].includes(String(task.task_status || '').trim())) {
|
||||
if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) {
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
return updateTask(task.id, {
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
|
||||
return updateTask(task.id, {
|
||||
task_status: 'pending_binding_prepare',
|
||||
inventory_status: 'not_required',
|
||||
user_action_status: 'pending',
|
||||
last_error: task.last_error || '等待准备绑定资源',
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: task.last_error || '领取链接已生成,等待客户提交核销码',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,10 +132,16 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } =
|
||||
})
|
||||
|
||||
for (const task of tasks) {
|
||||
const isClaimLinkTask = String(task.task_status || '') === 'link_generated'
|
||||
|| (
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& ['pending_binding_prepare', 'waiting_binding', 'role_confirmed', 'dispatched_pending_return'].includes(String(task.task_status || '').trim())
|
||||
)
|
||||
|
||||
if (
|
||||
event.provider !== 'agiso'
|
||||
|| event.platform !== 'xianyu'
|
||||
|| String(task.task_status || '') !== 'link_generated'
|
||||
|| !isClaimLinkTask
|
||||
|| !task.primary_claim_token_id
|
||||
) {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user