feat(pdd): 支持根据数据库已有订单自适应翻页拉取,防高峰漏单
1. 新增数据库批量排查已有订单接口 findExistingPlatformOrderIds,利用 B-Tree 索引毫秒级判定当前页订单是否存在; 2. 优化 syncStoreOrders 支持自适应翻页拉取:当第 1 页全部为新订单时,自适应向后递增翻页拉取(支持 maxPages 默认 5 页,平滑限速 300ms); 3. 翻页终止自适应判定:当拉取到包含数据库已有旧订单的页面时立即 break 停止翻页,兼顾高性能与不漏单; 4. 扩展全局配置支持 maxPages(自适应最大翻页页数),支持后台管理员在 UI 设置与立即拉单参数联动; 5. 新增单元测试 pdd-sync-service.test.ts,验证全部新订单自适应翻页与命中已有订单提前终止行为。
This commit is contained in:
@@ -86,6 +86,29 @@ export async function findLatestOrderByAnyPlatformOrderId(
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
|
||||
export async function findExistingPlatformOrderIds({
|
||||
provider = 'pdd',
|
||||
platform = 'pdd',
|
||||
platformOrderIds,
|
||||
}: {
|
||||
provider?: string
|
||||
platform: string
|
||||
platformOrderIds: string[]
|
||||
}): Promise<Set<string>> {
|
||||
if (!platformOrderIds || platformOrderIds.length === 0) {
|
||||
return new Set<string>()
|
||||
}
|
||||
const result = await query<{ platform_order_id: string }>(
|
||||
`
|
||||
SELECT platform_order_id
|
||||
FROM orders
|
||||
WHERE provider = $1 AND platform = $2 AND platform_order_id = ANY($3::text[])
|
||||
`,
|
||||
[provider, platform, platformOrderIds],
|
||||
)
|
||||
return new Set(result.rows.map((r) => String(r.platform_order_id)))
|
||||
}
|
||||
export async function createOrder(input: OrderCreateInput): Promise<OrderRow | null> {
|
||||
return createOrderWithExecutor(query, input)
|
||||
}
|
||||
|
||||
@@ -180,11 +180,13 @@ router.post(
|
||||
const body = (req.body || {}) as {
|
||||
shopId?: string
|
||||
pageSize?: number
|
||||
maxPages?: number
|
||||
orderType?: number
|
||||
}
|
||||
const shopId = String(body.shopId || '').trim()
|
||||
const config = getPddSourcesConfig()
|
||||
const pageSize = Math.min(100, Math.max(1, Number(body.pageSize || config.pageSize || 50)))
|
||||
const maxPages = Math.min(20, Math.max(1, Number(body.maxPages || config.maxPages || 5)))
|
||||
const orderType = Number(body.orderType ?? config.orderType ?? 2)
|
||||
|
||||
if (shopId) {
|
||||
@@ -195,10 +197,10 @@ router.post(
|
||||
errorCode: 'pdd_store_not_found',
|
||||
})
|
||||
}
|
||||
return syncStoreOrders(store, { pageSize, orderType })
|
||||
return syncStoreOrders(store, { pageSize, maxPages, orderType })
|
||||
}
|
||||
|
||||
return syncAllPddStores({ pageSize, orderType })
|
||||
return syncAllPddStores({ pageSize, maxPages, orderType })
|
||||
},
|
||||
{
|
||||
successMessage: '拼多多同步已触发',
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface PddSourcesConfig {
|
||||
pollIntervalSeconds: number
|
||||
orderType: number // 0: 全部, 1: 待发货, 2: 已发货 (默认 2)
|
||||
pageSize: number // 单次拉取数量 (默认 50)
|
||||
maxPages?: number // 自适应翻页最大页数 (默认 5)
|
||||
stores: PddStoreConfigItem[]
|
||||
}
|
||||
|
||||
@@ -227,11 +228,17 @@ export function normalizePddSourcesConfig(rawValue: unknown): PddSourcesConfig {
|
||||
? Math.min(100, Math.max(10, rawPageSize))
|
||||
: 50
|
||||
|
||||
const rawMaxPages = Number(source.maxPages)
|
||||
const maxPages = Number.isInteger(rawMaxPages)
|
||||
? Math.min(20, Math.max(1, rawMaxPages))
|
||||
: 5
|
||||
|
||||
return {
|
||||
enabled: source.enabled !== false,
|
||||
pollIntervalSeconds,
|
||||
orderType,
|
||||
pageSize,
|
||||
maxPages,
|
||||
stores,
|
||||
}
|
||||
}
|
||||
@@ -242,6 +249,7 @@ function createDefaultPddSourcesConfig(): PddSourcesConfig {
|
||||
pollIntervalSeconds: 20,
|
||||
orderType: 2,
|
||||
pageSize: 50,
|
||||
maxPages: 5,
|
||||
stores: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { syncStoreOrders } from './pdd-sync-service.js'
|
||||
import { PddClient } from './pdd-client.js'
|
||||
import type { PddStoreConfigItem } from './pdd-source-config-service.js'
|
||||
|
||||
const testStore: PddStoreConfigItem = {
|
||||
shopId: 'test_store_1',
|
||||
shopName: '测试拼多多店铺',
|
||||
cookie: 'pdd_user_id=123; PASS_ID=abc;',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
test('syncStoreOrders performs adaptive pagination when page 1 contains only new orders', async () => {
|
||||
const pagesQueried: number[] = []
|
||||
|
||||
// Mock client queryRecentOrders
|
||||
const originalQuery = PddClient.prototype.queryRecentOrders
|
||||
PddClient.prototype.queryRecentOrders = async function (options) {
|
||||
pagesQueried.push(options.pageNumber || 1)
|
||||
if (options.pageNumber === 1) {
|
||||
// Page 1: 50 new orders
|
||||
return {
|
||||
total: 100,
|
||||
items: Array.from({ length: 50 }, (_, i) => ({
|
||||
order_sn: `sn_page1_${i + 1}`,
|
||||
order_status: 2,
|
||||
order_amount: 1000,
|
||||
goods_id: 123,
|
||||
goods_name: '测试商品',
|
||||
})),
|
||||
}
|
||||
} else if (options.pageNumber === 2) {
|
||||
// Page 2: 50 orders, contains existing order
|
||||
return {
|
||||
total: 100,
|
||||
items: Array.from({ length: 50 }, (_, i) => ({
|
||||
order_sn: `sn_page2_${i + 1}`,
|
||||
order_status: 2,
|
||||
order_amount: 1000,
|
||||
goods_id: 123,
|
||||
goods_name: '测试商品',
|
||||
})),
|
||||
}
|
||||
}
|
||||
return { total: 100, items: [] }
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await syncStoreOrders(testStore, {
|
||||
pageSize: 50,
|
||||
maxPages: 5,
|
||||
checkExistingOrderIds: async (orderSns: string[]) => {
|
||||
// Page 1 has 0 existing in DB. Page 2 has sn_page2_50 in DB.
|
||||
const existing = new Set<string>()
|
||||
if (orderSns.includes('sn_page2_50')) {
|
||||
existing.add('sn_page2_50')
|
||||
}
|
||||
return existing
|
||||
},
|
||||
upsertFn: async () => {
|
||||
return {
|
||||
ignored: false,
|
||||
order: {} as any,
|
||||
orderItems: [],
|
||||
tasks: [],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(result.pagesFetched, 2, 'Should have fetched exactly 2 pages')
|
||||
assert.deepEqual(pagesQueried, [1, 2], 'Should have queried page 1 then page 2')
|
||||
assert.equal(result.fetchedCount, 100, 'Total fetched items should be 100')
|
||||
assert.equal(result.upsertedCount, 100, 'Total upserted should be 100')
|
||||
} finally {
|
||||
PddClient.prototype.queryRecentOrders = originalQuery
|
||||
}
|
||||
})
|
||||
|
||||
test('syncStoreOrders stops on page 1 if page 1 already contains existing orders', async () => {
|
||||
const pagesQueried: number[] = []
|
||||
|
||||
const originalQuery = PddClient.prototype.queryRecentOrders
|
||||
PddClient.prototype.queryRecentOrders = async function (options) {
|
||||
pagesQueried.push(options.pageNumber || 1)
|
||||
return {
|
||||
total: 100,
|
||||
items: Array.from({ length: 50 }, (_, i) => ({
|
||||
order_sn: `sn_page1_${i + 1}`,
|
||||
order_status: 2,
|
||||
order_amount: 1000,
|
||||
goods_id: 123,
|
||||
goods_name: '测试商品',
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await syncStoreOrders(testStore, {
|
||||
pageSize: 50,
|
||||
maxPages: 5,
|
||||
checkExistingOrderIds: async () => {
|
||||
// Page 1 has an existing order in DB!
|
||||
return new Set(['sn_page1_10'])
|
||||
},
|
||||
upsertFn: async () => {
|
||||
return {
|
||||
ignored: false,
|
||||
order: {} as any,
|
||||
orderItems: [],
|
||||
tasks: [],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(result.pagesFetched, 1, 'Should stop at page 1')
|
||||
assert.deepEqual(pagesQueried, [1], 'Should query page 1 only')
|
||||
assert.equal(result.fetchedCount, 50)
|
||||
} finally {
|
||||
PddClient.prototype.queryRecentOrders = originalQuery
|
||||
}
|
||||
})
|
||||
|
||||
test('syncStoreOrders stops on page 1 if explicit pageNumber > 1 is specified', async () => {
|
||||
const pagesQueried: number[] = []
|
||||
|
||||
const originalQuery = PddClient.prototype.queryRecentOrders
|
||||
PddClient.prototype.queryRecentOrders = async function (options) {
|
||||
pagesQueried.push(options.pageNumber || 1)
|
||||
return {
|
||||
total: 100,
|
||||
items: Array.from({ length: 50 }, (_, i) => ({
|
||||
order_sn: `sn_page2_${i + 1}`,
|
||||
order_status: 2,
|
||||
order_amount: 1000,
|
||||
goods_id: 123,
|
||||
goods_name: '测试商品',
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await syncStoreOrders(testStore, {
|
||||
pageNumber: 2,
|
||||
pageSize: 50,
|
||||
checkExistingOrderIds: async () => new Set(), // even if all are new
|
||||
upsertFn: async () => ({ ignored: false, order: {} as any, orderItems: [], tasks: [] }),
|
||||
})
|
||||
|
||||
assert.equal(result.pagesFetched, 1, 'Should only fetch the explicit page')
|
||||
assert.deepEqual(pagesQueried, [2], 'Should only query page 2')
|
||||
} finally {
|
||||
PddClient.prototype.queryRecentOrders = originalQuery
|
||||
}
|
||||
})
|
||||
@@ -5,11 +5,13 @@
|
||||
* Core Workflow / 核心流程:
|
||||
* 1. Pull recent orders from PDD MMS API using anti-content engine.
|
||||
* 调用基于纯算 anti-content 的 MMS 客户端拉取近期待发货订单。
|
||||
* 2. Map PDD payload to platform SourceOrderEvent.
|
||||
* 2. Adaptive pagination: If all orders on the first page are brand-new, auto-page to pull older orders until hitting existing records.
|
||||
* 自适应翻页拉取:当第一页(及后续页)全为新订单时,自适应继续翻页拉取,直至命中数据库已有订单或达到上限。
|
||||
* 3. Map PDD payload to platform SourceOrderEvent.
|
||||
* 将原始报文转化为 order_site 统一标准进单事件。
|
||||
* 3. In-memory ingestion via upsertOrderFromSource().
|
||||
* 4. In-memory ingestion via upsertOrderFromSource().
|
||||
* 内存直调平台核心 upsertOrderFromSource(),自动入库并触发打手大厅工单分发。
|
||||
* 4. Staggered multi-store scheduling & risk control.
|
||||
* 5. Staggered multi-store scheduling & risk control.
|
||||
* 多店铺错峰拉取与优雅异常处理。
|
||||
*/
|
||||
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
updatePddStoreCheckStatus,
|
||||
type PddStoreConfigItem,
|
||||
} from './pdd-source-config-service.js'
|
||||
import { findExistingPlatformOrderIds } from '../../../repositories/order-repo.js'
|
||||
import { notifyPddCookieExpired } from '../../notification/domain-notifications.js'
|
||||
import { upsertOrderFromSource } from '../../order/order-service.js'
|
||||
import { logError, logInfo, logWarn } from '../../../utils/logger.js'
|
||||
@@ -27,9 +30,12 @@ import { logError, logInfo, logWarn } from '../../../utils/logger.js'
|
||||
export interface SyncStoreOrdersOptions {
|
||||
pageNumber?: number | undefined
|
||||
pageSize?: number | undefined
|
||||
maxPages?: number | undefined
|
||||
orderType?: number | undefined // 2: 已发货 (默认), 1: 待发货, 0: 全部
|
||||
startTime?: number | undefined
|
||||
endTime?: number | undefined
|
||||
checkExistingOrderIds?: ((orderSns: string[]) => Promise<Set<string>>) | undefined
|
||||
upsertFn?: typeof upsertOrderFromSource | undefined
|
||||
}
|
||||
|
||||
export interface SyncStoreResult {
|
||||
@@ -40,6 +46,7 @@ export interface SyncStoreResult {
|
||||
upsertedCount: number
|
||||
ignoredCount: number
|
||||
failedCount: number
|
||||
pagesFetched: number
|
||||
error?: string | undefined
|
||||
}
|
||||
|
||||
@@ -51,8 +58,8 @@ export interface SyncAllStoresResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize orders for a single PDD store.
|
||||
* 同步单个拼多多店铺的订单并触发进单流程。
|
||||
* Synchronize orders for a single PDD store with adaptive pagination.
|
||||
* 同步单个拼多多店铺的订单并触发进单流程(支持自适应翻页拉取)。
|
||||
*/
|
||||
export async function syncStoreOrders(
|
||||
store: PddStoreConfigItem,
|
||||
@@ -71,6 +78,7 @@ export async function syncStoreOrders(
|
||||
upsertedCount: 0,
|
||||
ignoredCount: 0,
|
||||
failedCount: 0,
|
||||
pagesFetched: 0,
|
||||
error: 'cookie_missing',
|
||||
}
|
||||
}
|
||||
@@ -78,82 +86,182 @@ export async function syncStoreOrders(
|
||||
const client = new PddClient(store)
|
||||
const sourcesConfig = getPddSourcesConfig()
|
||||
const pageSize = Math.min(100, Math.max(1, Number(options.pageSize || sourcesConfig.pageSize || 50)))
|
||||
const pageNumber = Math.max(1, Number(options.pageNumber || 1))
|
||||
const initialPage = Math.max(1, Number(options.pageNumber || 1))
|
||||
const orderType = options.orderType ?? sourcesConfig.orderType ?? 2 // 2: 已发货 (Shipped, default)
|
||||
|
||||
try {
|
||||
const queryOptions: QueryOrdersOptions = {
|
||||
pageNumber,
|
||||
pageSize,
|
||||
orderType,
|
||||
}
|
||||
if (options.startTime !== undefined) {
|
||||
queryOptions.startTime = options.startTime
|
||||
}
|
||||
if (options.endTime !== undefined) {
|
||||
queryOptions.endTime = options.endTime
|
||||
}
|
||||
// Determine whether to allow adaptive multi-page pulling
|
||||
// 若传入明确的指定页码且 > 1,则仅拉取该指定页;否则执行自适应翻页(默认上限 5 页,最大支持 20 页)
|
||||
const isExplicitSinglePage = options.pageNumber !== undefined && options.pageNumber > 1
|
||||
const maxPages = isExplicitSinglePage
|
||||
? 1
|
||||
: Math.min(20, Math.max(1, Number(options.maxPages || sourcesConfig.maxPages || 5)))
|
||||
|
||||
const queryResult = await client.queryRecentOrders(queryOptions)
|
||||
let currentPage = initialPage
|
||||
let pagesFetched = 0
|
||||
let totalReported = 0
|
||||
let totalFetched = 0
|
||||
let totalUpserted = 0
|
||||
let totalIgnored = 0
|
||||
let totalFailed = 0
|
||||
|
||||
try {
|
||||
while (pagesFetched < maxPages) {
|
||||
const queryOptions: QueryOrdersOptions = {
|
||||
pageNumber: currentPage,
|
||||
pageSize,
|
||||
orderType,
|
||||
}
|
||||
if (options.startTime !== undefined) {
|
||||
queryOptions.startTime = options.startTime
|
||||
}
|
||||
if (options.endTime !== undefined) {
|
||||
queryOptions.endTime = options.endTime
|
||||
}
|
||||
|
||||
const queryResult = await client.queryRecentOrders(queryOptions)
|
||||
totalReported = queryResult.total
|
||||
const items = queryResult.items || []
|
||||
pagesFetched++
|
||||
|
||||
if (items.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
totalFetched += items.length
|
||||
|
||||
// 提取本页订单号并批量排查数据库已有状态
|
||||
const pageOrderSns = items
|
||||
.map((item) => String(item.order_sn || '').trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const checkExisting =
|
||||
options.checkExistingOrderIds ||
|
||||
((sns: string[]) =>
|
||||
findExistingPlatformOrderIds({
|
||||
provider: 'pdd',
|
||||
platform: 'pdd',
|
||||
platformOrderIds: sns,
|
||||
}))
|
||||
const existingOrderSns = await checkExisting(pageOrderSns)
|
||||
|
||||
const existingCountOnPage = existingOrderSns.size
|
||||
const newCountOnPage = pageOrderSns.length - existingCountOnPage
|
||||
|
||||
const doUpsert = options.upsertFn || upsertOrderFromSource
|
||||
|
||||
// 执行当前页订单进单入库
|
||||
for (const rawItem of items) {
|
||||
try {
|
||||
const sourceEvent = mapPddOrderToSourceEvent(rawItem, { shopId, shopName })
|
||||
const result = await doUpsert(sourceEvent, {
|
||||
sourceLabel: `pdd_${shopId}`,
|
||||
})
|
||||
|
||||
if (result.ignored) {
|
||||
totalIgnored++
|
||||
} else {
|
||||
totalUpserted++
|
||||
}
|
||||
} catch (upsertErr) {
|
||||
totalFailed++
|
||||
logError('[pdd-sync]', `拼多多订单进单失败: ${rawItem.order_sn}`, {
|
||||
shopId,
|
||||
orderSn: rawItem.order_sn,
|
||||
error: upsertErr instanceof Error ? upsertErr.message : String(upsertErr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logInfo(
|
||||
'[pdd-sync]',
|
||||
`店铺 ${shopName} 第 ${currentPage} 页处理完成 (本页拉取: ${items.length}, 新单: ${newCountOnPage}, 库中已有: ${existingCountOnPage})`,
|
||||
{
|
||||
shopId,
|
||||
currentPage,
|
||||
pageItems: items.length,
|
||||
newCountOnPage,
|
||||
existingCountOnPage,
|
||||
},
|
||||
)
|
||||
|
||||
// 自适应翻页终止条件判断:
|
||||
// 1. 若外部指定单页拉取,立即终止
|
||||
if (isExplicitSinglePage) {
|
||||
break
|
||||
}
|
||||
|
||||
// 2. 若当前页中已经出现了数据库中已存在的旧订单,说明拉单时间线已触及上次同步断点
|
||||
// (拼多多按时间倒序排列,后续页码必然全为更早的已存历史订单,无需继续翻页)
|
||||
if (existingCountOnPage > 0) {
|
||||
break
|
||||
}
|
||||
|
||||
// 3. 若本页订单数不足 pageSize,说明拼多多后台当前条件已无更多订单
|
||||
if (items.length < pageSize) {
|
||||
break
|
||||
}
|
||||
|
||||
// 4. 若已拉取条数达到店铺总订单数上限,终止
|
||||
if (totalFetched >= totalReported) {
|
||||
break
|
||||
}
|
||||
|
||||
// 5. 若已达最大翻页限制,终止并记录日志
|
||||
if (pagesFetched >= maxPages) {
|
||||
logInfo('[pdd-sync]', `店铺 ${shopName} 达到自适应翻页上限 (${maxPages}页),停止继续翻页`, {
|
||||
shopId,
|
||||
pagesFetched,
|
||||
totalFetched,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// 本页全为全新订单!说明有积压新单未拉完,自适应翻页拉取下一页
|
||||
logInfo(
|
||||
'[pdd-sync]',
|
||||
`店铺 ${shopName} 第 ${currentPage} 页全为新订单 (${newCountOnPage}条),自适应翻页拉取第 ${currentPage + 1} 页...`,
|
||||
{ shopId, nextPage: currentPage + 1 },
|
||||
)
|
||||
currentPage++
|
||||
|
||||
// 翻页平滑限速 300ms,避免触发拼多多频控
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
}
|
||||
|
||||
// Update persistent check status to valid / 更新持久化校验状态为有效
|
||||
await updatePddStoreCheckStatus(shopId, {
|
||||
cookieValid: true,
|
||||
cookieCheckMessage: `同步正常 (在售/历史: ${queryResult.total})`,
|
||||
cookieCheckMessage: `同步正常 (在售/历史: ${totalReported})`,
|
||||
lastCheckedAt: new Date().toISOString(),
|
||||
totalOrders: queryResult.total,
|
||||
totalOrders: totalReported,
|
||||
}).catch(() => null)
|
||||
|
||||
const { total, items } = queryResult
|
||||
let upsertedCount = 0
|
||||
let ignoredCount = 0
|
||||
let failedCount = 0
|
||||
|
||||
for (const rawItem of items) {
|
||||
try {
|
||||
const sourceEvent = mapPddOrderToSourceEvent(rawItem, { shopId, shopName })
|
||||
const result = await upsertOrderFromSource(sourceEvent, {
|
||||
sourceLabel: `pdd_${shopId}`,
|
||||
})
|
||||
|
||||
if (result.ignored) {
|
||||
ignoredCount++
|
||||
} else {
|
||||
upsertedCount++
|
||||
}
|
||||
} catch (upsertErr) {
|
||||
failedCount++
|
||||
logError('[pdd-sync]', `拼多多订单进单失败: ${rawItem.order_sn}`, {
|
||||
shopId,
|
||||
orderSn: rawItem.order_sn,
|
||||
error: upsertErr instanceof Error ? upsertErr.message : String(upsertErr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logInfo('[pdd-sync]', `店铺 ${shopName} 同步完成`, {
|
||||
logInfo('[pdd-sync]', `店铺 ${shopName} 同步完成 (共拉取 ${pagesFetched} 页)`, {
|
||||
shopId,
|
||||
total,
|
||||
fetchedCount: items.length,
|
||||
upsertedCount,
|
||||
ignoredCount,
|
||||
failedCount,
|
||||
total: totalReported,
|
||||
pagesFetched,
|
||||
fetchedCount: totalFetched,
|
||||
upsertedCount: totalUpserted,
|
||||
ignoredCount: totalIgnored,
|
||||
failedCount: totalFailed,
|
||||
})
|
||||
|
||||
return {
|
||||
shopId,
|
||||
shopName,
|
||||
total,
|
||||
fetchedCount: items.length,
|
||||
upsertedCount,
|
||||
ignoredCount,
|
||||
failedCount,
|
||||
total: totalReported,
|
||||
fetchedCount: totalFetched,
|
||||
upsertedCount: totalUpserted,
|
||||
ignoredCount: totalIgnored,
|
||||
failedCount: totalFailed,
|
||||
pagesFetched,
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err)
|
||||
logError('[pdd-sync]', `店铺 ${shopName} 拉取订单异常`, {
|
||||
shopId,
|
||||
error: errMsg,
|
||||
pagesFetched,
|
||||
})
|
||||
|
||||
// If login expired or risk-controlled, mark cookie as invalid and notify admins
|
||||
@@ -177,14 +285,16 @@ export async function syncStoreOrders(
|
||||
message: errMsg,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
shopId,
|
||||
shopName,
|
||||
total: 0,
|
||||
fetchedCount: 0,
|
||||
upsertedCount: 0,
|
||||
ignoredCount: 0,
|
||||
failedCount: 0,
|
||||
total: totalReported,
|
||||
fetchedCount: totalFetched,
|
||||
upsertedCount: totalUpserted,
|
||||
ignoredCount: totalIgnored,
|
||||
failedCount: totalFailed,
|
||||
pagesFetched,
|
||||
error: errMsg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@ export async function runPddOrderSyncJob(job: 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, orderType })
|
||||
const syncResult = await syncAllPddStores({ pageSize, maxPages, orderType })
|
||||
|
||||
if (!syncResult.enabled) {
|
||||
return {
|
||||
|
||||
@@ -98,6 +98,7 @@ export function PddPlatformPanel() {
|
||||
pollIntervalSeconds: 20,
|
||||
orderType: 2,
|
||||
pageSize: 50,
|
||||
maxPages: 5,
|
||||
stores: [],
|
||||
})
|
||||
|
||||
@@ -168,6 +169,7 @@ export function PddPlatformPanel() {
|
||||
try {
|
||||
const response = await triggerAdminPddSync({
|
||||
pageSize: config.pageSize || 50,
|
||||
maxPages: config.maxPages || 5,
|
||||
orderType: config.orderType ?? 2,
|
||||
})
|
||||
const data = response?.data as AdminPddSyncAllResponse | undefined
|
||||
@@ -201,12 +203,14 @@ export function PddPlatformPanel() {
|
||||
const response = await triggerAdminPddSync({
|
||||
shopId: store.shopId,
|
||||
pageSize: config.pageSize || 50,
|
||||
maxPages: config.maxPages || 5,
|
||||
orderType: config.orderType ?? 2,
|
||||
})
|
||||
const data = response?.data as AdminPddSyncResult | undefined
|
||||
if (data) {
|
||||
const pagesText = data.pagesFetched && data.pagesFetched > 1 ? `(自适应拉取 ${data.pagesFetched} 页)` : ""
|
||||
showSuccess(
|
||||
`店铺【${store.shopName}】同步完成:抓取 ${data.fetchedCount} 笔,入库/更新 ${data.upsertedCount} 笔`,
|
||||
`店铺【${store.shopName}】同步完成${pagesText}:抓取 ${data.fetchedCount} 笔,入库/更新 ${data.upsertedCount} 笔`,
|
||||
)
|
||||
} else {
|
||||
showSuccess(`店铺【${store.shopName}】同步已触发`)
|
||||
@@ -786,8 +790,8 @@ export function PddPlatformPanel() {
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Row gutter={[24, 16]} align="middle">
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Row gutter={[16, 16]} align="middle">
|
||||
<Col xs={24} sm={12} md={4}>
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text strong>自动拉单调度状态</Text>
|
||||
<Space align="center">
|
||||
@@ -802,7 +806,7 @@ export function PddPlatformPanel() {
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Col xs={24} sm={12} md={5}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>抓取订单状态</Text>
|
||||
<Select
|
||||
@@ -818,7 +822,7 @@ export function PddPlatformPanel() {
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Col xs={24} sm={12} md={5}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>错峰轮询间隔</Text>
|
||||
<InputNumber
|
||||
@@ -837,14 +841,14 @@ export function PddPlatformPanel() {
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Col xs={24} sm={12} md={5}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>单次拉单上限 (pageSize)</Text>
|
||||
<Text strong>单页拉单数 (pageSize)</Text>
|
||||
<InputNumber
|
||||
min={10}
|
||||
max={100}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="单/次"
|
||||
addonAfter="单/页"
|
||||
value={config.pageSize ?? 50}
|
||||
onChange={(val) =>
|
||||
setConfig((prev) => ({
|
||||
@@ -855,6 +859,25 @@ export function PddPlatformPanel() {
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={5}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>自适应翻页上限 (maxPages)</Text>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={20}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="页"
|
||||
value={config.maxPages ?? 5}
|
||||
onChange={(val) =>
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
maxPages: Number(val || 5),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Alert
|
||||
@@ -863,8 +886,9 @@ export function PddPlatformPanel() {
|
||||
style={{ marginTop: 14 }}
|
||||
message={
|
||||
<span>
|
||||
当前拉单规则:默认抓取<strong>【{orderTypeDesc}】</strong>订单,单次拉取最新
|
||||
<strong>【{config.pageSize || 50}】</strong>单并持久化记录校验状态。
|
||||
当前拉单规则:默认抓取<strong>【{orderTypeDesc}】</strong>订单,单页拉取最新
|
||||
<strong>【{config.pageSize || 50}】</strong>单;已启用<strong>【自适应翻页拉取】</strong>(上限
|
||||
<strong>【{config.maxPages || 5}】</strong>页),当第 1 页全为新订单时自动向后翻页直至命中数据库已有订单,避免高峰期漏单。
|
||||
</span>
|
||||
}
|
||||
description="系统自动保存每家店铺上次 Cookie 的测活状态与时间戳。若在定时轮询或手动测试中检测到 Cookie 失效(或触发登录过期),系统将自动更新状态并向 Bark / Wpush 发送内部告警通知。"
|
||||
|
||||
@@ -42,7 +42,7 @@ export function testAdminPddCookie(payload: {
|
||||
}
|
||||
|
||||
export function triggerAdminPddSync(
|
||||
payload: { shopId?: string; pageSize?: number; orderType?: number } = {},
|
||||
payload: { shopId?: string; pageSize?: number; maxPages?: number; orderType?: number } = {},
|
||||
) {
|
||||
return apiPost<AdminPddSyncAllResponse | AdminPddSyncResult>(
|
||||
'/api/v1/admin/platform-config/pdd/sync',
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface AdminPddConfig {
|
||||
pollIntervalSeconds: number
|
||||
orderType?: number // 0: 全部, 1: 待发货, 2: 已发货 (默认 2)
|
||||
pageSize?: number // 单次拉取单数 (默认 50)
|
||||
maxPages?: number // 自适应最大翻页数 (默认 5)
|
||||
stores: AdminPddStoreItem[]
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ export interface AdminPddSyncResult {
|
||||
upsertedCount: number
|
||||
ignoredCount: number
|
||||
failedCount: number
|
||||
pagesFetched?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user