完整流程加节流 + 高频重试

This commit is contained in:
yml2213
2026-05-03 00:37:50 +08:00
parent 4cc91bf194
commit d7c61ed20f
10 changed files with 313 additions and 7 deletions
@@ -19,6 +19,7 @@ import {
listAdminCloudtentaclesVirtualNumbers,
lookupAdminFulfillmentBindingOrder,
queryAdminKhhaoOrders,
runAdminCloudtentaclesFullFlow,
sendAdminCloudtentaclesSmsCode,
syncAdminKhhaoOrders,
testAdminCloudtentaclesLogin,
@@ -45,6 +46,7 @@ import { createJsonHandler, requireAdminRoles } from './shared.js'
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesCatalogQueryRouteBody} AdminCloudtentaclesCatalogQueryRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSkuBuyRouteBody} AdminCloudtentaclesSkuBuyRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesVirtualNumberRouteBody} AdminCloudtentaclesVirtualNumberRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesFullFlowRouteBody} AdminCloudtentaclesFullFlowRouteBody */
/** @typedef {import('../../types/admin-write-models.js').AdminAgisoShopConfigSaveResponse} AdminAgisoShopConfigSaveResponse */
const router = Router()
@@ -381,6 +383,15 @@ router.post('/platform-config/cloudtentacles/vn/bind-url', createJsonHandler(
},
))
router.post('/platform-config/cloudtentacles/debug/full-flow', createJsonHandler(
(req) => runAdminCloudtentaclesFullFlow(/** @type {AdminCloudtentaclesFullFlowRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 完整调试流程执行成功',
errorMessage: 'cloudtentacles 完整调试流程执行失败',
scope: '[admin/platform-config/cloudtentacles/debug/full-flow]',
},
))
router.get('/platform-config/fulfillment-bindings', createJsonHandler(
() => getAdminFulfillmentBindingConfigs(),
{
@@ -46,6 +46,7 @@ import {
listCloudtentaclesVirtualNumbers,
verifyCloudtentaclesLoginCode,
} from '../platforms/cloudtentacles/virtual-number-service.js'
import { runCloudtentaclesFullDebugFlow } from '../platforms/cloudtentacles/debug-flow-service.js'
import { normalizeAgisoMessageTemplate } from '../platforms/agiso/xianyu/message-service.js'
import {
getOrderFulfillmentBindingConfigs,
@@ -70,6 +71,7 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesCatalogQueryInput} AdminCloudtentaclesCatalogQueryInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSkuBuyInput} AdminCloudtentaclesSkuBuyInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesVirtualNumberInput} AdminCloudtentaclesVirtualNumberInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesFullFlowInput} AdminCloudtentaclesFullFlowInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigSaveInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingLookupInput} AdminFulfillmentBindingLookupInput */
@@ -609,6 +611,17 @@ export async function getAdminCloudtentaclesBindUrl(payload = /** @type {AdminCl
})
}
/** @param {AdminCloudtentaclesFullFlowInput} [payload] */
export async function runAdminCloudtentaclesFullFlow(payload = /** @type {AdminCloudtentaclesFullFlowInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return runCloudtentaclesFullDebugFlow({
...context,
skuId: payload.skuId,
skuCount: payload.skuCount,
vnKey: payload.vnKey,
})
}
export async function getAdminFulfillmentBindingConfigs() {
const bindings = getOrderFulfillmentBindingConfigs()
const rowsResult = await query(
@@ -0,0 +1,232 @@
// @ts-check
import { createHttpError } from '../../../utils/http.js'
import { getCloudtentaclesAsset, listCloudtentaclesSku, buyCloudtentaclesSku } from './catalog-service.js'
import { getCloudtentaclesKnapsack } from './knapsack-service.js'
import {
appointCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
verifyCloudtentaclesLoginCode,
} from './virtual-number-service.js'
export async function runCloudtentaclesFullDebugFlow(payload = {}) {
const skuId = normalizePositiveInteger(payload.skuId, 0)
const skuCount = normalizePositiveInteger(payload.skuCount, 1)
const vnKey = String(payload.vnKey || '').trim()
if (!skuId) {
throw createHttpError('cloudtentacles 完整流程缺少 SKU ID', {
statusCode: 400,
errorCode: 'cloudtentacles_full_flow_missing_sku_id',
})
}
if (!vnKey) {
throw createHttpError('cloudtentacles 完整流程缺少 VN Key', {
statusCode: 400,
errorCode: 'cloudtentacles_full_flow_missing_vn_key',
})
}
const beforeAsset = await runFlowStep('查询购买前余额', () => getCloudtentaclesAsset(payload))
await sleep(350)
const skuList = await runFlowStep('查询 SKU 列表', () => listCloudtentaclesSku(payload))
const targetSku = skuList.items.find((item) => Number(item.id || 0) === skuId) || null
if (!targetSku) {
throw createHttpError(`cloudtentacles 未找到 SKU ${skuId}`, {
statusCode: 404,
errorCode: 'cloudtentacles_full_flow_sku_not_found',
})
}
const requiredAsset = Number(targetSku.price || 0) * skuCount
if (beforeAsset.asset < requiredAsset) {
throw createHttpError(`余额不足,当前 ${beforeAsset.asset},需要 ${requiredAsset}`, {
statusCode: 409,
errorCode: 'cloudtentacles_full_flow_insufficient_asset',
})
}
await sleep(350)
const beforeKnapsack = await runFlowStep('查询购买前背包', () => getCloudtentaclesKnapsack(payload))
const beforeItem = findKnapsackItem(beforeKnapsack.items, skuId)
await sleep(500)
const buyResult = await runFlowStep('购买 SKU', () => buyCloudtentaclesSku({
...payload,
id: skuId,
count: skuCount,
}), { retries: 1, retryDelayMs: 1200 })
await sleep(1200)
const afterAsset = await runFlowStep('查询购买后余额', () => getCloudtentaclesAsset(payload), {
retries: 2,
retryDelayMs: 1200,
})
await sleep(1200)
const afterKnapsack = await runFlowStep('查询购买后背包', () => getCloudtentaclesKnapsack(payload), {
retries: 2,
retryDelayMs: 1200,
})
const afterItem = findKnapsackItem(afterKnapsack.items, skuId)
const beforeCount = Number(beforeItem?.count || 0)
const afterCount = Number(afterItem?.count || 0)
const knapsackIncreased = afterCount >= beforeCount + skuCount
if (!knapsackIncreased) {
throw createHttpError(`购买后背包校验失败,购买前 ${beforeCount},购买后 ${afterCount},期望至少 ${beforeCount + skuCount}`, {
statusCode: 409,
errorCode: 'cloudtentacles_full_flow_knapsack_not_updated',
})
}
await sleep(700)
const appointed = await runFlowStep('申请虚拟号', () => appointCloudtentaclesVirtualNumber({
...payload,
key: vnKey,
}), { retries: 1, retryDelayMs: 1200 })
const appointedId = Number(appointed.item?.id || 0)
const appointedPhone = String(appointed.item?.phone || '').trim()
if (!appointedId || !appointedPhone) {
throw createHttpError('cloudtentacles 申请虚拟号成功但返回数据不完整', {
statusCode: 502,
errorCode: 'cloudtentacles_full_flow_invalid_appointed_vn',
})
}
await sleep(1200)
const generateCodeResult = await runFlowStep('生成登录码', () => generateCloudtentaclesLoginCode({
...payload,
key: vnKey,
id: appointedId,
}), { retries: 1, retryDelayMs: 1200 })
await sleep(1600)
const fetchedCode = await runFlowStep('获取验证码', () => fetchCloudtentaclesVirtualNumberCode({
...payload,
key: vnKey,
phone: appointedPhone,
}), { retries: 2, retryDelayMs: 1500 })
await sleep(1000)
const verified = await runFlowStep('校验验证码', () => verifyCloudtentaclesLoginCode({
...payload,
key: vnKey,
id: appointedId,
code: fetchedCode.code,
}), { retries: 1, retryDelayMs: 1200 })
await sleep(1000)
const bindUrlResult = await runFlowStep('获取兑换链接', () => getCloudtentaclesBindUrl({
...payload,
key: vnKey,
id: appointedId,
}), { retries: 1, retryDelayMs: 1200 })
return {
sku: {
id: skuId,
count: skuCount,
name: String(targetSku.name || '').trim(),
price: Number(targetSku.price || 0),
requiredAsset,
},
assetCheck: {
before: beforeAsset.asset,
after: afterAsset.asset,
enough: beforeAsset.asset >= requiredAsset,
spent: beforeAsset.asset - afterAsset.asset,
},
knapsackCheck: {
beforeCount,
afterCount,
expectedIncrease: skuCount,
increased: knapsackIncreased,
},
purchase: buyResult,
virtualNumber: {
key: vnKey,
id: appointedId,
phone: appointedPhone,
appoint: appointed,
generateLoginCode: generateCodeResult,
fetchedCode,
verify: verified,
},
bindUrl: bindUrlResult.bindUrl,
bindUrlResult,
}
}
function normalizePositiveInteger(value, fallback) {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
}
function findKnapsackItem(items, skuId) {
return (Array.isArray(items) ? items : []).find((item) => Number(item.id || 0) === Number(skuId)) || null
}
async function runFlowStep(label, runner, options = {}) {
const retries = Number.isInteger(options.retries) && options.retries > 0 ? options.retries : 0
const retryDelayMs = Number.isFinite(options.retryDelayMs) && options.retryDelayMs > 0 ? options.retryDelayMs : 1000
for (let attempt = 0; attempt <= retries; attempt += 1) {
try {
return await runner()
} catch (error) {
if (attempt >= retries || !isHighFrequencyError(error)) {
throw wrapStepError(label, error)
}
await sleep(retryDelayMs * (attempt + 1))
}
}
throw createHttpError(`cloudtentacles ${label}失败`, {
statusCode: 500,
errorCode: 'cloudtentacles_full_flow_step_failed',
})
}
function isHighFrequencyError(error) {
const message = error instanceof Error ? error.message : String(error || '')
return message.toLowerCase().includes('high-frequency request')
}
function wrapStepError(label, error) {
const message = error instanceof Error ? error.message : String(error || '未知错误')
if (isHighFrequencyError(error)) {
return createHttpError(`cloudtentacles ${label}失败:请求过于频繁,请稍后重试`, {
statusCode: 429,
errorCode: 'cloudtentacles_rate_limited',
})
}
if (error && typeof error === 'object' && 'statusCode' in error) {
const statusCode = Number(Reflect.get(error, 'statusCode') || 500)
const errorCode = String(Reflect.get(error, 'errorCode') || 'cloudtentacles_full_flow_step_failed')
return createHttpError(`cloudtentacles ${label}失败:${message}`, {
statusCode,
errorCode,
})
}
return createHttpError(`cloudtentacles ${label}失败:${message}`, {
statusCode: 500,
errorCode: 'cloudtentacles_full_flow_step_failed',
})
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
@@ -74,6 +74,10 @@ export {}
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesVirtualNumberInput} AdminCloudtentaclesVirtualNumberRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesFullFlowInput} AdminCloudtentaclesFullFlowRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigRouteBody
*/
@@ -180,6 +180,18 @@ export {}
* }} AdminCloudtentaclesVirtualNumberInput
*/
/**
* @typedef {{
* baseUrl?: string
* token?: string
* deviceId?: string
* deviceType?: number | string
* skuId?: number | string
* skuCount?: number | string
* vnKey?: string
* }} AdminCloudtentaclesFullFlowInput
*/
/**
* @typedef {{
* provider?: string