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 变更解除熔断)。
This commit is contained in:
yml2213
2026-10-03 14:27:38 +08:00
parent 90de6b9419
commit 0173efb277
6 changed files with 454 additions and 15 deletions
@@ -32,6 +32,7 @@ export async function runPddOrderSyncJob(job: JsonObject) {
checkedCount: 0,
okCount: 0,
failedCount: 0,
suspendedCount: 0,
}
}
@@ -41,10 +42,13 @@ export async function runPddOrderSyncJob(job: JsonObject) {
const totalFetched = syncResult.results.reduce((acc, r) => acc + r.fetchedCount, 0)
const totalNewOrders = syncResult.results.reduce((acc, r) => acc + r.newOrderCount, 0)
const jobLog = totalNewOrders > 0 || failedCount > 0 ? logInfo : logDebug
// 有店铺被熔断时必须留在 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,
@@ -55,11 +59,12 @@ export async function runPddOrderSyncJob(job: JsonObject) {
return {
ok: failedCount === 0,
status: failedCount > 0 ? (okCount > 0 ? 'partial_error' : 'failed') : 'ok',
message: `已同步 ${syncResult.syncedCount} 个店铺,拉取 ${totalFetched} 单,新单 ${totalNewOrders},入库/更新 ${totalUpserted} 单${failedCount > 0 ? `,失败 ${failedCount} 个店铺` : ''}`,
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,