From e1a0a7447720241709d2d05db007bcc8f480dccc Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 8 Aug 2026 02:38:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=A4=E5=84=BF=E5=8F=B7?= =?UTF-8?q?=E7=A0=81=E5=9B=9E=E6=94=B6=EF=BC=9A=E6=9D=83=E9=99=90=E4=B8=8D?= =?UTF-8?q?=E8=B6=B3=E9=87=8D=E8=AF=953=E6=AC=A1=E5=90=8E=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=E6=89=93=E4=B8=8A=E6=B8=B8=EF=BC=8C1=E5=B0=8F?= =?UTF-8?q?=E6=97=B6=E5=90=8E=E8=87=AA=E5=8A=A8=E9=87=8D=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?health=20=E6=91=98=E8=A6=81=E5=B1=95=E7=A4=BA=E5=AD=A4=E5=84=BF?= =?UTF-8?q?=E6=95=B0=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/cloudtentacles-health-job.ts | 10 +- .../cloudtentacles-number-recycle-service.ts | 112 +++++++++++++++++- 2 files changed, 118 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts b/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts index 0548a9af..460ab280 100644 --- a/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts +++ b/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts @@ -51,11 +51,19 @@ export async function runCloudtentaclesHealthJob(job: JsonObject) { .filter((value): value is number => typeof value === 'number' && Number.isFinite(value)) const summary = buildCloudtentaclesHealthSummary(results) const status = resolveCloudtentaclesHealthStatus(results) + const orphanTaskCount = + Number(recycleResult?.orphanCount || 0) + Number(returnRetryResult?.orphanCount || 0) + const recycleNote = orphanTaskCount > 0 + ? `,另有 ${orphanTaskCount} 个孤儿号码待人工/上游清理` + : recycleResult?.refreshed || returnRetryResult?.returned + ? ',过期号码回收正常' + : '' + const finalMessage = `${summary}${recycleNote}` return { ok: failedResults.length === 0, status, - message: summary, + message: finalMessage, accountCount: results.length, checkedCount: checkedResults.length, okCount: okResults.length, diff --git a/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts b/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts index 17407898..151cd292 100644 --- a/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts +++ b/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts @@ -15,6 +15,12 @@ import { returnKuaishouCloudFulfillmentTask } from '../fulfillment/kuaishou-clou const STALE_BIND_URL_GRACE_MS = 15 * 60 * 1000 /** 单轮最多处理的任务数(health 每 2.5 分钟跑一次,防止一次扫太多) */ const RECYCLE_SCAN_LIMIT = 50 +/** "权限不足"类孤儿号码最多重试次数:上游判定号码不属于当前会话时重试无意义,3 次后停止打上游 */ +const ORPHAN_MAX_RETRIES = 3 +/** 孤儿计数超过该时长后自动重置,允许上游/账号恢复后重新尝试 */ +const ORPHAN_RESET_AFTER_MS = 60 * 60 * 1000 +/** 连续失败记录:taskId -> { count, lastAt } */ +const recycleFailRecords = new Map() type RecycleNumberResult = { scanned: number @@ -22,6 +28,47 @@ type RecycleNumberResult = { skippedMissingContext: number refreshed: number failed: number + /** 上游判定无权操作(账号重新登录换 token 后的孤儿号码),已停止重试的任务数 */ + orphanCount: number + orphanTaskIds: number[] +} + +/** 上游"权限不足/没有权限"类错误:号码归属当前账号会话,重试无法恢复 */ +function isOrphanNumberError(error: unknown) { + const message = error instanceof Error ? error.message : String(error || '') + return message.includes('权限不足') || message.includes('没有权限') +} + +function getRecycleFailCount(taskId: number) { + const record = recycleFailRecords.get(taskId) + if (!record) { + return 0 + } + if (Date.now() - record.lastAt > ORPHAN_RESET_AFTER_MS) { + recycleFailRecords.delete(taskId) + return 0 + } + return record.count +} + +function isRecycleOrphanQuotaReached(taskId: number) { + return getRecycleFailCount(taskId) >= ORPHAN_MAX_RETRIES +} + +function recordRecycleFailure(taskId: number, isOrphan: boolean) { + if (isOrphan) { + const record = recycleFailRecords.get(taskId) + recycleFailRecords.set(taskId, { + count: (record?.count || 0) + 1, + lastAt: Date.now(), + }) + } else { + recycleFailRecords.delete(taskId) + } +} + +function recordRecycleSuccess(taskId: number) { + recycleFailRecords.delete(taskId) } export async function recycleCloudtentaclesStaleNumbers(): Promise { @@ -32,6 +79,8 @@ export async function recycleCloudtentaclesStaleNumbers(): Promise( @@ -50,6 +99,15 @@ export async function recycleCloudtentaclesStaleNumbers(): Promise { - const result = { scanned: 0, returned: 0, failed: 0 } + const result = { + scanned: 0, + returned: 0, + failed: 0, + orphanCount: 0, + orphanTaskIds: [] as number[], + } const rows = await dbQuery( ` SELECT * @@ -117,6 +199,14 @@ export async function retryCloudtentaclesPendingReturns(): Promise<{ for (const task of rows.rows) { result.scanned += 1 + const taskId = Number(task.id || 0) + + if (isRecycleOrphanQuotaReached(taskId)) { + result.orphanCount += 1 + result.orphanTaskIds.push(taskId) + continue + } + try { if (!isKuaishouCloudTask(task)) { result.failed += 1 @@ -135,10 +225,26 @@ export async function retryCloudtentaclesPendingReturns(): Promise<{ actor: { source: 'system' }, }) result.returned += 1 + recordRecycleSuccess(taskId) } catch (error) { + const isOrphan = isOrphanNumberError(error) + recordRecycleFailure(taskId, isOrphan) + if (isOrphan && isRecycleOrphanQuotaReached(taskId)) { + result.orphanCount += 1 + result.orphanTaskIds.push(taskId) + logWarn( + '[scheduler/cloudtentacles-number-recycle]', + '发货后退号的孤儿号码达到重试上限,已停止自动重试(需人工或上游处理)', + { + taskId, + error: error instanceof Error ? error.message : String(error || ''), + } + ) + continue + } result.failed += 1 logWarn('[scheduler/cloudtentacles-number-recycle]', '发货后退号重试失败', { - taskId: Number(task.id || 0), + taskId, error: error instanceof Error ? error.message : String(error || ''), }) }