Files
order_site/apps/backend/src/services/scheduler/pdd-order-sync-job.ts
T
yml2213 0173efb277 feat(pdd): 店铺连续同步失败 3 次后熔断,停止自动重试
背景:生产上「木塔网游专营店」Cookie 已过期,每约 24 秒重试一次、每轮刷一条 ERROR 日志,
持续数天。Cookie 失效不会自愈,继续重试只刷日志、白白占用同步周期。

行为:
  - 失败 1~2 次:累计计数,继续重试;
  - 第 3 次:熔断,打一条显式 ERROR(含"更新 Cookie 会自动恢复"提示);
  - 熔断后:自动同步直接跳过,不再刷错误日志;
  - 恢复:更新 Cookie(后台编辑或浏览器插件)自动解除;手动同步成功一次亦可。
  熔断状态持久化在店铺配置里,容器重启后依然生效。

改动:
  - pdd-source-config-service:阈值常量 + consecutiveFailures/syncSuspendedAt/
    syncSuspendedReason 持久化字段 + 纯函数 resolvePddStoreFailureState /
    applyPddStoreBreakerState / isPddStoreSyncSuspended / shouldResetPddSyncBreaker。
  - pdd-sync-service:syncAllPddStores 跳过熔断店铺并上报 suspendedCount;
    syncStoreOrders 失败记录、成功清零(健康店铺不产生额外配置写)。
  - pdd-order-sync-job:上报熔断数,且存在熔断店铺时强制留在 INFO 级
    (否则"全部店铺已停同步"会因新单为 0 被降级成 debug 而不可见)。
  - 前端加「已熔断 · 停止重试」标签与类型字段。

顺带修掉一个隐患:saveAdminPddStore 收的是 Partial 载荷,normalizePddStoreItem
会为缺失字段产出 undefined,导致一次「只改店铺名」的保存就把熔断洗掉。
由 applyPddStoreBreakerState 回填既有状态解决。

验证:9 项熔断单测(含局部保存不洗状态、Cookie 变更解除熔断)。
2026-10-03 14:27:38 +08:00

73 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Pinduoduo Scheduled Order Sync Job
* 拼多多定时订单同步任务 (错峰智能调度与异常保护)
*/
import type { JsonObject } from '../../types/json.js'
import { logDebug, logInfo } from '../../utils/logger.js'
import { syncAllPddStores } from '../platforms/pdd/pdd-sync-service.js'
import { getPddSourcesConfig } from '../platforms/pdd/pdd-source-config-service.js'
export async function runPddOrderSyncJob(job: JsonObject) {
const config =
job.config && typeof job.config === 'object' && !Array.isArray(job.config)
? (job.config as JsonObject)
: {}
const sourcesConfig = getPddSourcesConfig()
const pageSize = Math.min(
100,
Math.max(1, Number(config.pageSize || sourcesConfig.pageSize || 50)),
)
const maxPages = Math.min(20, Math.max(1, Number(config.maxPages || sourcesConfig.maxPages || 5)))
const orderType = Number(config.orderType ?? sourcesConfig.orderType ?? 2)
const syncResult = await syncAllPddStores({ pageSize, maxPages, orderType })
if (!syncResult.enabled) {
return {
ok: true,
status: 'disabled',
message: '拼多多同步子服务已全局停用',
accountCount: syncResult.storeCount,
checkedCount: 0,
okCount: 0,
failedCount: 0,
suspendedCount: 0,
}
}
const okCount = syncResult.results.filter((r) => !r.error).length
const failedCount = syncResult.results.filter((r) => Boolean(r.error)).length
const totalUpserted = syncResult.results.reduce((acc, r) => acc + r.upsertedCount, 0)
const totalFetched = syncResult.results.reduce((acc, r) => acc + r.fetchedCount, 0)
const totalNewOrders = syncResult.results.reduce((acc, r) => acc + r.newOrderCount, 0)
// 有店铺被熔断时必须留在 info 级:否则"全部店铺已停止同步"会因新单为 0 被降级成 debug 而不可见。
const jobLog =
totalNewOrders > 0 || failedCount > 0 || syncResult.suspendedCount > 0 ? logInfo : logDebug
jobLog('[pdd-order-sync-job]', '拼多多定时拉单执行完成', {
storeCount: syncResult.storeCount,
syncedCount: syncResult.syncedCount,
suspendedCount: syncResult.suspendedCount,
okCount,
failedCount,
totalFetched,
totalUpserted,
totalNewOrders,
})
return {
ok: failedCount === 0,
status: failedCount > 0 ? (okCount > 0 ? 'partial_error' : 'failed') : 'ok',
message: `已同步 ${syncResult.syncedCount} 个店铺,拉取 ${totalFetched} 单,新单 ${totalNewOrders},入库/更新 ${totalUpserted} 单${failedCount > 0 ? `,失败 ${failedCount} 个店铺` : ''}${syncResult.suspendedCount > 0 ? `,${syncResult.suspendedCount} 个店铺已熔断待更新 Cookie` : ''}`,
accountCount: syncResult.storeCount,
checkedCount: syncResult.syncedCount,
okCount,
failedCount,
suspendedCount: syncResult.suspendedCount,
asset: totalNewOrders,
totalUpserted,
totalNewOrders,
}
}