优化孤儿号码回收:权限不足重试3次后停止打上游,1小时后自动重置,health 摘要展示孤儿数量
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<number, { count: number; lastAt: number }>()
|
||||
|
||||
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<RecycleNumberResult> {
|
||||
@@ -32,6 +79,8 @@ export async function recycleCloudtentaclesStaleNumbers(): Promise<RecycleNumber
|
||||
skippedMissingContext: 0,
|
||||
refreshed: 0,
|
||||
failed: 0,
|
||||
orphanCount: 0,
|
||||
orphanTaskIds: [],
|
||||
}
|
||||
|
||||
const rows = await dbQuery<TaskRow>(
|
||||
@@ -50,6 +99,15 @@ export async function recycleCloudtentaclesStaleNumbers(): Promise<RecycleNumber
|
||||
|
||||
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.skippedMissingContext += 1
|
||||
@@ -76,10 +134,26 @@ export async function recycleCloudtentaclesStaleNumbers(): Promise<RecycleNumber
|
||||
actor: { source: 'system' },
|
||||
})
|
||||
result.refreshed += 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 || ''),
|
||||
})
|
||||
}
|
||||
@@ -100,8 +174,16 @@ export async function retryCloudtentaclesPendingReturns(): Promise<{
|
||||
scanned: number
|
||||
returned: number
|
||||
failed: number
|
||||
orphanCount: number
|
||||
orphanTaskIds: number[]
|
||||
}> {
|
||||
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<TaskRow>(
|
||||
`
|
||||
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 || ''),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user