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
@@ -18,6 +18,7 @@ import {
SafetyCertificateOutlined,
SaveOutlined,
ShopOutlined,
StopOutlined,
ThunderboltOutlined,
} from '@ant-design/icons'
import {
@@ -541,12 +542,25 @@ export function PddPlatformPanel() {
const totalOrders = testRes ? testRes.totalOrders : record.totalOrders
const timeDisplay = testRes?.testedAt || formatDisplayTime(record.lastCheckedAt)
// 连续失败已熔断:自动重试已经停了。必须显式提示,否则客服会以为只是
// "暂时失效",不知道新订单已经不再进来了。
const suspendedTag = record.syncSuspendedAt ? (
<Tooltip
title={`连续失败 ${record.consecutiveFailures ?? 3} 次后已停止自动重试(${formatDisplayTime(record.syncSuspendedAt)})。原因:${record.syncSuspendedReason || '未知'}。更新 Cookie 后会自动恢复。`}
>
<Tag icon={<StopOutlined />} color="volcano" style={{ cursor: 'pointer', margin: 0 }}>
已熔断 · 停止重试
</Tag>
</Tooltip>
) : null
if (isValid === true) {
return (
<Space direction="vertical" size={2}>
<Tag icon={<CheckCircleOutlined />} color="success">
有效 (在售: {totalOrders ?? '-'})
</Tag>
{suspendedTag}
{timeDisplay && (
<Text type="secondary" style={{ fontSize: '11px' }}>
{timeDisplay} 校验
@@ -569,6 +583,7 @@ export function PddPlatformPanel() {
失效 (已告警)
</Tag>
</Tooltip>
{suspendedTag}
{timeDisplay && (
<Text type="secondary" style={{ fontSize: '11px' }}>
{timeDisplay} 校验
@@ -583,7 +598,12 @@ export function PddPlatformPanel() {
)
}
return <Badge status="default" text={<Text type="secondary">未校验</Text>} />
return (
<Space direction="vertical" size={2}>
<Badge status="default" text={<Text type="secondary">未校验</Text>} />
{suspendedTag}
</Space>
)
},
},
{
@@ -17,6 +17,12 @@ export interface AdminPddStoreItem {
totalOrders?: number
syncToken?: string
lastExtensionSyncAt?: string
/** 连续同步失败次数;成功一次即清零。 */
consecutiveFailures?: number
/** 熔断时间(ISO)。存在即表示已停止自动重试,更新 Cookie 后自动恢复。 */
syncSuspendedAt?: string
/** 触发熔断时的最近一次失败原因。 */
syncSuspendedReason?: string
}
export interface AdminPddStoreSyncTokenResponse {
@@ -57,5 +63,7 @@ export interface AdminPddSyncAllResponse {
enabled: boolean
storeCount: number
syncedCount: number
/** 因连续失败被熔断、本轮跳过自动重试的店铺数。 */
suspendedCount?: number
results: AdminPddSyncResult[]
}