diff --git a/apps/backend/src/routes/admin/platform-config/pdd.ts b/apps/backend/src/routes/admin/platform-config/pdd.ts index 5d71792f..02f54923 100644 --- a/apps/backend/src/routes/admin/platform-config/pdd.ts +++ b/apps/backend/src/routes/admin/platform-config/pdd.ts @@ -12,7 +12,9 @@ import { savePddStore, deletePddStore, getPddStoreById, + updatePddStoreCheckStatus, } from '../../../services/platforms/pdd/pdd-source-config-service.js' +import { notifyPddCookieExpired } from '../../../services/notification/domain-notifications.js' import { syncAllPddStores, syncStoreOrders, @@ -120,17 +122,11 @@ router.post( let cookie = String(body.cookie || '').trim() let userAgent = body.userAgent + const existingStore = body.shopId ? getPddStoreById(body.shopId) : null - if (!cookie && body.shopId) { - const store = getPddStoreById(body.shopId) - if (!store) { - throw createHttpError(`店铺不存在: ${body.shopId}`, { - statusCode: 404, - errorCode: 'pdd_store_not_found', - }) - } - cookie = store.cookie - userAgent = userAgent || store.userAgent + if (!cookie && existingStore) { + cookie = existingStore.cookie + userAgent = userAgent || existingStore.userAgent } if (!cookie) { @@ -140,7 +136,29 @@ router.post( }) } - return testPddCookie({ cookie, userAgent }) + const result = await testPddCookie({ cookie, userAgent }) + + // Persist check status and notify if invalid + // 持久化测试状态并在失效时触发告警通知 + if (existingStore) { + const now = new Date().toISOString() + await updatePddStoreCheckStatus(existingStore.shopId, { + cookieValid: result.valid, + cookieCheckMessage: result.message, + lastCheckedAt: now, + totalOrders: result.totalOrders, + }) + + if (!result.valid) { + notifyPddCookieExpired({ + shopId: existingStore.shopId, + shopName: existingStore.shopName, + message: result.message, + }) + } + } + + return result }, { successMessage: 'Cookie 校验执行完成', diff --git a/apps/backend/src/services/notification/domain-notifications.ts b/apps/backend/src/services/notification/domain-notifications.ts index 5fe6ba82..fa000d47 100644 --- a/apps/backend/src/services/notification/domain-notifications.ts +++ b/apps/backend/src/services/notification/domain-notifications.ts @@ -292,3 +292,39 @@ function markNotificationCooldown(cooldownKey: unknown) { notificationCooldownMap.set(key, Date.now()) } + + +export interface PddCookieExpiredPayload { + shopId: string + shopName?: string | undefined + message?: string | undefined + cooldownSeconds?: number | undefined +} + +/** + * Send alert notification when a PDD store's Cookie expires or fails validation. + * 拼多多店铺 Cookie 失效或校验未通过时的内部告警通知。 + */ +export function notifyPddCookieExpired({ + shopId, + shopName, + message = '商家后台 Cookie 已失效或登录过期', + cooldownSeconds = 600, +}: PddCookieExpiredPayload) { + const normalizedShopId = String(shopId || '').trim() + const normalizedShopName = String(shopName || normalizedShopId).trim() + + return notifyInternalSafely({ + title: `拼多多 Cookie 已失效:${normalizedShopName}`, + body: [ + `店铺:${normalizedShopName} (${normalizedShopId})`, + `原因:${String(message || '').trim() || '商家后台 Cookie 已失效,无法自动同步新订单'}`, + '请登录拼多多商家后台(mms.pinduoduo.com)重新复制并更新 Cookie,以免漏单。', + ] + .filter(Boolean) + .join('\n'), + category: 'pdd_cookie_expired', + cooldownKey: ['pdd_cookie_expired', normalizedShopId || 'default'].join(':'), + cooldownMs: Number(cooldownSeconds || 600) * 1000, + }) +} diff --git a/apps/backend/src/services/platforms/pdd/pdd-source-config-service.ts b/apps/backend/src/services/platforms/pdd/pdd-source-config-service.ts index f5873cc4..77225744 100644 --- a/apps/backend/src/services/platforms/pdd/pdd-source-config-service.ts +++ b/apps/backend/src/services/platforms/pdd/pdd-source-config-service.ts @@ -22,6 +22,10 @@ export interface PddStoreConfigItem { proxy?: string | undefined etag?: string | undefined enabled: boolean + lastCheckedAt?: string | undefined + cookieValid?: boolean | undefined + cookieCheckMessage?: string | undefined + totalOrders?: number | undefined } export interface PddSourcesConfig { @@ -108,6 +112,38 @@ export async function savePddStore( * Delete single store configuration by shopId. * 删除指定店铺配置。 */ + +/** + * Update and persist check status for a specific store. + * 更新并持久化保存指定店铺的 CK 测活状态与时间戳。 + */ +export async function updatePddStoreCheckStatus( + shopId: unknown, + status: { + cookieValid: boolean + cookieCheckMessage: string + lastCheckedAt: string + totalOrders?: number | undefined + }, +): Promise { + const key = String(shopId || '').trim() + if (!key) return null + + const config = getPddSourcesConfig() + const store = config.stores.find((s) => s.shopId === key) + if (!store) return null + + store.cookieValid = status.cookieValid + store.cookieCheckMessage = status.cookieCheckMessage + store.lastCheckedAt = status.lastCheckedAt + if (status.totalOrders !== undefined) { + store.totalOrders = status.totalOrders + } + + await savePddSourcesConfig(config) + return store +} + export async function deletePddStore(shopId: unknown): Promise { const key = String(shopId || '').trim() if (!key) { @@ -141,6 +177,14 @@ export function normalizePddStoreItem(rawValue: unknown): PddStoreConfigItem { const userAgent = String(source.userAgent || '').trim() || PurePddAntiContent.DEFAULT_UA const proxy = String(source.proxy || '').trim() const etag = String(source.etag || '').trim() + const lastCheckedAt = source.lastCheckedAt ? String(source.lastCheckedAt).trim() : undefined + const cookieValid = typeof source.cookieValid === 'boolean' ? source.cookieValid : undefined + const cookieCheckMessage = source.cookieCheckMessage + ? String(source.cookieCheckMessage).trim() + : undefined + const totalOrders = Number.isInteger(Number(source.totalOrders)) + ? Number(source.totalOrders) + : undefined return { shopId, @@ -150,6 +194,10 @@ export function normalizePddStoreItem(rawValue: unknown): PddStoreConfigItem { proxy: proxy || undefined, etag: etag || undefined, enabled: source.enabled !== false, + lastCheckedAt, + cookieValid, + cookieCheckMessage, + totalOrders, } } diff --git a/apps/backend/src/services/platforms/pdd/pdd-sync-service.ts b/apps/backend/src/services/platforms/pdd/pdd-sync-service.ts index 45ee9f71..e94040a6 100644 --- a/apps/backend/src/services/platforms/pdd/pdd-sync-service.ts +++ b/apps/backend/src/services/platforms/pdd/pdd-sync-service.ts @@ -15,7 +15,12 @@ import { PddClient, type QueryOrdersOptions } from './pdd-client.js' import { mapPddOrderToSourceEvent } from './pdd-order-mapper.js' -import { getPddSourcesConfig, type PddStoreConfigItem } from './pdd-source-config-service.js' +import { + getPddSourcesConfig, + updatePddStoreCheckStatus, + type PddStoreConfigItem, +} from './pdd-source-config-service.js' +import { notifyPddCookieExpired } from '../../notification/domain-notifications.js' import { upsertOrderFromSource } from '../../order/order-service.js' import { logError, logInfo, logWarn } from '../../../utils/logger.js' @@ -91,6 +96,14 @@ export async function syncStoreOrders( const queryResult = await client.queryRecentOrders(queryOptions) + // Update persistent check status to valid / 更新持久化校验状态为有效 + await updatePddStoreCheckStatus(shopId, { + cookieValid: true, + cookieCheckMessage: `同步正常 (在售/历史: ${queryResult.total})`, + lastCheckedAt: new Date().toISOString(), + totalOrders: queryResult.total, + }).catch(() => null) + const { total, items } = queryResult let upsertedCount = 0 let ignoredCount = 0 @@ -142,6 +155,28 @@ export async function syncStoreOrders( shopId, error: errMsg, }) + + // If login expired or risk-controlled, mark cookie as invalid and notify admins + // 若登录过期或风控拦截,标记为失效并推送告警通知 + if ( + errMsg.includes('40001') || + errMsg.includes('登录') || + errMsg.includes('失效') || + errMsg.includes('未登录') || + errMsg.includes('40002') + ) { + await updatePddStoreCheckStatus(shopId, { + cookieValid: false, + cookieCheckMessage: errMsg, + lastCheckedAt: new Date().toISOString(), + }).catch(() => null) + + notifyPddCookieExpired({ + shopId, + shopName, + message: errMsg, + }) + } return { shopId, shopName, diff --git a/apps/frontend/src/pages/admin/platform/PddPlatformPanel.tsx b/apps/frontend/src/pages/admin/platform/PddPlatformPanel.tsx index 40176277..1eca7fcf 100644 --- a/apps/frontend/src/pages/admin/platform/PddPlatformPanel.tsx +++ b/apps/frontend/src/pages/admin/platform/PddPlatformPanel.tsx @@ -63,6 +63,25 @@ import type { const { Text, Paragraph } = Typography +/** + * Format ISO date string into readable short format + * 格式化 ISO 时间字符串为月-日 时:分 + */ +function formatDisplayTime(dateStr?: string): string { + if (!dateStr) return '' + try { + const d = new Date(dateStr) + if (isNaN(d.getTime())) return String(dateStr) + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const hours = String(d.getHours()).padStart(2, '0') + const minutes = String(d.getMinutes()).padStart(2, '0') + return `${month}-${day} ${hours}:${minutes}` + } catch { + return String(dateStr) + } +} + /** * Extended test result with execution timestamp * 带时间戳的 CK 校验结果结构 @@ -88,7 +107,7 @@ export function PddPlatformPanel() { const [syncingShopId, setSyncingShopId] = useState(null) const [togglingShopId, setTogglingShopId] = useState(null) - // Test results cache by shopId / 店铺 CK 校验结果缓存 + // Test results cache by shopId / 店铺 CK 校验结果缓存 (用于刚点击测试后的即时响应) const [testResults, setTestResults] = useState>({}) // Modal State / 弹窗表单状态 @@ -163,6 +182,8 @@ export function PddPlatformPanel() { } else { showSuccess('全量店铺同步完成') } + // Refresh persistent statuses + await loadConfig() } catch (err) { showError(err instanceof Error ? err.message : '触发全店铺同步失败') } finally { @@ -190,6 +211,8 @@ export function PddPlatformPanel() { } else { showSuccess(`店铺【${store.shopName}】同步已触发`) } + // Refresh persistent store status + await loadConfig() } catch (err) { showError(err instanceof Error ? err.message : `店铺【${store.shopName}】同步失败`) } finally { @@ -199,7 +222,7 @@ export function PddPlatformPanel() { /** * Live test MMS Cookie validity for a store - * 针对指定店铺实时测试 Cookie 有效性 + * 针对指定店铺实时测试 Cookie 有效性 (持久化保存结果,失效时向系统自动发送通知) */ async function handleTestCookie(store: AdminPddStoreItem) { setTestingShopId(store.shopId) @@ -216,12 +239,16 @@ export function PddPlatformPanel() { ...prev, [store.shopId]: { ...result, testedAt: nowStr }, })) + // Reload persisted state + await loadConfig() if (result.valid) { showSuccess( - `【${store.shopName}】Cookie 校验通过,在售/历史订单总数: ${result.totalOrders ?? 0}`, + `【${store.shopName}】Cookie 校验通过,在线在售/历史订单数: ${result.totalOrders ?? 0}`, ) } else { - showError(`【${store.shopName}】Cookie 校验未通过: ${result.message}`) + showError( + `【${store.shopName}】Cookie 校验未通过: ${result.message}(已触发内部告警通知)`, + ) } } } catch (err) { @@ -361,10 +388,17 @@ export function PddPlatformPanel() { } } - // Calculate statistics / 计算统计数据 + // Calculate statistics / 计算统计数据 (结合持久化历史状态与即时状态) const totalStores = config.stores.length const enabledStores = config.stores.filter((s) => s.enabled).length - const validStores = Object.values(testResults).filter((r) => r.valid).length + const validStores = config.stores.filter((s) => { + const rt = testResults[s.shopId] + return rt ? rt.valid : s.cookieValid === true + }).length + const invalidStores = config.stores.filter((s) => { + const rt = testResults[s.shopId] + return rt ? !rt.valid : s.cookieValid === false + }).length // Table columns definition / 表格列配置 const columns: TableColumnsType = [ @@ -460,40 +494,49 @@ export function PddPlatformPanel() { { title: 'Cookie 校验状态', key: 'testStatus', - width: 190, + width: 210, render: (_, record) => { + // Priority: local test cache, fallback to persisted store status + // 优先显示本次测试结果,刷新后自动读取持久化历史状态与时间戳 const testRes = testResults[record.shopId] - if (!testRes) { - return 未校验} /> - } - if (testRes.valid) { + const isValid = testRes ? testRes.valid : record.cookieValid + const message = testRes ? testRes.message : record.cookieCheckMessage + const totalOrders = testRes ? testRes.totalOrders : record.totalOrders + const timeDisplay = testRes?.testedAt || formatDisplayTime(record.lastCheckedAt) + + if (isValid === true) { return ( } color="success"> - 有效 (在售/历史: {testRes.totalOrders ?? 0}) + 有效 (在售: {totalOrders ?? '-'}) - {testRes.testedAt && ( + {timeDisplay && ( - {testRes.testedAt} 测活 + {timeDisplay} 校验 )} ) } - return ( - - - } color="error" style={{ cursor: 'pointer' }}> - 失效 (点击查看) - - - {testRes.testedAt && ( - - {testRes.testedAt} 测活 - - )} - - ) + + if (isValid === false) { + return ( + + + } color="error" style={{ cursor: 'pointer' }}> + 失效 (已告警) + + + {timeDisplay && ( + + {timeDisplay} 校验 + + )} + + ) + } + + return 未校验} /> }, }, { @@ -503,7 +546,7 @@ export function PddPlatformPanel() { fixed: 'right', render: (_, record) => ( - +