feat(pdd-cookie): 持久化保存Cookie校验状态时间并在失效时发送内部告警通知
This commit is contained in:
@@ -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 校验执行完成',
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<PddStoreConfigItem | null> {
|
||||
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<PddSourcesConfig> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
const [togglingShopId, setTogglingShopId] = useState<string | null>(null)
|
||||
|
||||
// Test results cache by shopId / 店铺 CK 校验结果缓存
|
||||
// Test results cache by shopId / 店铺 CK 校验结果缓存 (用于刚点击测试后的即时响应)
|
||||
const [testResults, setTestResults] = useState<Record<string, ExtendedTestResult>>({})
|
||||
|
||||
// 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<AdminPddStoreItem> = [
|
||||
@@ -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 <Badge status="default" text={<Text type="secondary">未校验</Text>} />
|
||||
}
|
||||
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 (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
有效 (在售/历史: {testRes.totalOrders ?? 0})
|
||||
有效 (在售: {totalOrders ?? '-'})
|
||||
</Tag>
|
||||
{testRes.testedAt && (
|
||||
{timeDisplay && (
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
{testRes.testedAt} 测活
|
||||
{timeDisplay} 校验
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Tooltip title={testRes.message || 'Cookie 失效或风控拦截'}>
|
||||
<Tag icon={<CloseCircleOutlined />} color="error" style={{ cursor: 'pointer' }}>
|
||||
失效 (点击查看)
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
{testRes.testedAt && (
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
{testRes.testedAt} 测活
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
|
||||
if (isValid === false) {
|
||||
return (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Tooltip title={message || 'Cookie 失效或登录已过期,已发送内部告警通知'}>
|
||||
<Tag icon={<CloseCircleOutlined />} color="error" style={{ cursor: 'pointer' }}>
|
||||
失效 (已告警)
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
{timeDisplay && (
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
{timeDisplay} 校验
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
return <Badge status="default" text={<Text type="secondary">未校验</Text>} />
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -503,7 +546,7 @@ export function PddPlatformPanel() {
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size={6} wrap={false}>
|
||||
<Tooltip title="通过 Pure TS 0as 引擎实时测试 MMS 商家 Cookie 有效性">
|
||||
<Tooltip title="通过 Pure TS 0as 引擎实时测试 MMS 商家 Cookie 有效性并持久化校验状态">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ThunderboltOutlined />}
|
||||
@@ -628,17 +671,26 @@ export function PddPlatformPanel() {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
已启用 <strong style={{ color: '#52c41a' }}>{enabledStores}</strong> 家 / 待机{' '}
|
||||
{totalStores - enabledStores} 家
|
||||
</Text>
|
||||
{validStores > 0 && (
|
||||
<Tag color="success" style={{ margin: 0, fontSize: 11 }}>
|
||||
CK 有效 {validStores}
|
||||
</Tag>
|
||||
)}
|
||||
<Space size={4}>
|
||||
{validStores > 0 && (
|
||||
<Tag color="success" style={{ margin: 0, fontSize: 11 }}>
|
||||
CK有效 {validStores}
|
||||
</Tag>
|
||||
)}
|
||||
{invalidStores > 0 && (
|
||||
<Tag color="error" style={{ margin: 0, fontSize: 11 }}>
|
||||
CK失效 {invalidStores}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -812,10 +864,10 @@ export function PddPlatformPanel() {
|
||||
message={
|
||||
<span>
|
||||
当前拉单规则:默认抓取<strong>【{orderTypeDesc}】</strong>订单,单次拉取最新
|
||||
<strong>【{config.pageSize || 50}】</strong>单并错峰均摊。
|
||||
<strong>【{config.pageSize || 50}】</strong>单并持久化记录校验状态。
|
||||
</span>
|
||||
}
|
||||
description="当拼多多买家下单并完成虚拟发货(或服务市场工具发货为已发货)后,系统将自动拉取入库并派发至打手大厅;您也可以随时点击右侧【立即拉单】即时手动触发增量抓取。"
|
||||
description="系统自动保存每家店铺上次 Cookie 的测活状态与时间戳。若在定时轮询或手动测试中检测到 Cookie 失效(或触发登录过期),系统将自动更新状态并向 Bark / Wpush 发送内部告警通知。"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ export interface AdminPddStoreItem {
|
||||
proxy?: string
|
||||
etag?: string
|
||||
enabled: boolean
|
||||
lastCheckedAt?: string
|
||||
cookieValid?: boolean
|
||||
cookieCheckMessage?: string
|
||||
totalOrders?: number
|
||||
}
|
||||
|
||||
export interface AdminPddConfig {
|
||||
|
||||
Reference in New Issue
Block a user