From d7c61ed20fea7b4ec0e72da596938686c1dc38f8 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 3 May 2026 00:37:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=95=B4=E6=B5=81=E7=A8=8B=E5=8A=A0?= =?UTF-8?q?=E8=8A=82=E6=B5=81=20+=20=E9=AB=98=E9=A2=91=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/data/cloudtentacles-session.json | 2 +- apps/backend/data/khhao-sync-state.json | 6 +- .../src/routes/admin/platform-config.js | 11 + .../admin/admin-platform-config-service.js | 13 + .../cloudtentacles/debug-flow-service.js | 232 ++++++++++++++++++ apps/backend/src/types/admin-route-inputs.js | 4 + apps/backend/src/types/admin-write-inputs.js | 12 + apps/frontend/src/lib/http.ts | 12 +- apps/frontend/src/services/admin.ts | 12 + .../views/admin/AdminPlatformShopsView.vue | 16 +- 10 files changed, 313 insertions(+), 7 deletions(-) create mode 100644 apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.js diff --git a/apps/backend/data/cloudtentacles-session.json b/apps/backend/data/cloudtentacles-session.json index e10147bc..025ae511 100644 --- a/apps/backend/data/cloudtentacles-session.json +++ b/apps/backend/data/cloudtentacles-session.json @@ -3,7 +3,7 @@ "baseUrl": "https://123.207.217.176", "username": "17665234375", "phone": "17665234375", - "loggedInAt": "2026-05-02T14:56:41.055Z", + "loggedInAt": "2026-05-02T15:19:01.258Z", "deviceId": "-", "deviceType": 0 } diff --git a/apps/backend/data/khhao-sync-state.json b/apps/backend/data/khhao-sync-state.json index 60d49432..17d87a58 100644 --- a/apps/backend/data/khhao-sync-state.json +++ b/apps/backend/data/khhao-sync-state.json @@ -1,11 +1,11 @@ { "syncFromCreatedAt": "2026-05-02T10:53:28.550Z", - "lastRunStartedAt": "2026-05-02T15:00:54.051Z", - "lastRunFinishedAt": "2026-05-02T15:00:55.112Z", + "lastRunStartedAt": "2026-05-02T16:37:16.806Z", + "lastRunFinishedAt": "2026-05-02T16:37:17.707Z", "lastRunStatus": "success", "lastErrorMessage": "", "fetchedCount": 40, "syncedCount": 0, "ignoredCount": 40, - "lastOrderCreatedAt": "2026-05-02T15:00:47.000Z" + "lastOrderCreatedAt": "2026-05-02T16:36:46.000Z" } diff --git a/apps/backend/src/routes/admin/platform-config.js b/apps/backend/src/routes/admin/platform-config.js index b261c9e1..fab92bc2 100644 --- a/apps/backend/src/routes/admin/platform-config.js +++ b/apps/backend/src/routes/admin/platform-config.js @@ -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(), { diff --git a/apps/backend/src/services/admin/admin-platform-config-service.js b/apps/backend/src/services/admin/admin-platform-config-service.js index 45c2e1bc..e1ecc9f9 100644 --- a/apps/backend/src/services/admin/admin-platform-config-service.js +++ b/apps/backend/src/services/admin/admin-platform-config-service.js @@ -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( diff --git a/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.js b/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.js new file mode 100644 index 00000000..6b39ee70 --- /dev/null +++ b/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.js @@ -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) + }) +} diff --git a/apps/backend/src/types/admin-route-inputs.js b/apps/backend/src/types/admin-route-inputs.js index c66d9419..f92cee79 100644 --- a/apps/backend/src/types/admin-route-inputs.js +++ b/apps/backend/src/types/admin-route-inputs.js @@ -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 */ diff --git a/apps/backend/src/types/admin-write-inputs.js b/apps/backend/src/types/admin-write-inputs.js index 7c69371f..7dc647b9 100644 --- a/apps/backend/src/types/admin-write-inputs.js +++ b/apps/backend/src/types/admin-write-inputs.js @@ -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 diff --git a/apps/frontend/src/lib/http.ts b/apps/frontend/src/lib/http.ts index cef67098..ecac539d 100644 --- a/apps/frontend/src/lib/http.ts +++ b/apps/frontend/src/lib/http.ts @@ -37,8 +37,9 @@ http.interceptors.response.use( if (error?.config?.url && String(error.config.url).startsWith('/api/v1/admin')) { const status = Number(error?.response?.status || 0) + const errorCode = String(error?.response?.data?.errorCode || '').trim() - if (status === 401) { + if (status === 401 && shouldClearAdminSession(errorCode)) { clearAdminSession() if (window.location.hash.startsWith('#/admin') && !window.location.hash.startsWith('#/admin/login')) { @@ -81,6 +82,15 @@ function resolveFallbackHttpMessage(error: unknown) { return '接口请求失败' } +function shouldClearAdminSession(errorCode: string) { + return [ + 'admin_auth_required', + 'admin_auth_invalid', + 'admin_auth_expired', + 'admin_auth_user_invalid', + ].includes(errorCode) +} + export function apiGet(url: string, params?: Record) { return http.get>(url, { params }) as unknown as Promise> } diff --git a/apps/frontend/src/services/admin.ts b/apps/frontend/src/services/admin.ts index 6b5e897e..bac462b6 100644 --- a/apps/frontend/src/services/admin.ts +++ b/apps/frontend/src/services/admin.ts @@ -311,6 +311,18 @@ export function fetchAdminCloudtentaclesBindUrl(payload: { return apiPost>('/api/v1/admin/platform-config/cloudtentacles/vn/bind-url', payload) } +export function runAdminCloudtentaclesFullFlow(payload: { + baseUrl?: string + token?: string + deviceId?: string + deviceType?: number + skuId?: number + skuCount?: number + vnKey?: string +}) { + return apiPost>('/api/v1/admin/platform-config/cloudtentacles/debug/full-flow', payload) +} + export function fetchAdminFulfillmentBindingConfigs() { return apiGet<{ filePath: string diff --git a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue index 756fd8b0..d5ff7f8d 100644 --- a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue @@ -17,6 +17,7 @@ import { fetchAdminKhhaoSourceConfig, generateAdminCloudtentaclesVnLoginCode, queryAdminKhhaoOrders, + runAdminCloudtentaclesFullFlow, saveAdminCloudtentaclesSourceConfig, saveAdminAgisoShopConfigs, saveAdminKhhaoSourceConfig, @@ -108,7 +109,7 @@ const cloudtentaclesForm = ref({ token: '', vnKey: '1', vnId: 0, - skuId: 0, + skuId: 5, skuCount: 1, deviceId: '-', deviceType: 0, @@ -235,7 +236,7 @@ async function loadConfigs() { token: cloudtentaclesResponse.data.session?.token || '', vnKey: '1', vnId: 0, - skuId: 0, + skuId: 5, skuCount: 1, deviceId: cloudtentaclesResponse.data.source.deviceId || '-', deviceType: cloudtentaclesResponse.data.source.deviceType || 0, @@ -757,6 +758,16 @@ async function handleCloudtentaclesFetchBindUrl() { })) } +async function handleCloudtentaclesRunFullFlow() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('完整测试流程', () => runAdminCloudtentaclesFullFlow({ + ...buildCloudtentaclesTokenPayload(), + skuId: Number(cloudtentaclesForm.value.skuId || 0), + skuCount: Number(cloudtentaclesForm.value.skuCount || 1), + vnKey: cloudtentaclesForm.value.vnKey.trim(), + })) +} + onMounted(loadConfigs) @@ -1385,6 +1396,7 @@ onMounted(loadConfigs) 获取验证码 校验验证码 兑换链接 + 完整流程