背景:生产上「木塔网游专营店」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 变更解除熔断)。
73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
/**
|
||
* 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,
|
||
}
|
||
}
|