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:
yml2213
2026-09-12 16:25:37 +08:00
parent 1217da7393
commit ede9a87b97
9 changed files with 405 additions and 78 deletions
@@ -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
}