diff --git a/apps/backend/data/khhao-sources.json b/apps/backend/data/khhao-sources.json index b2bed778..0588940d 100644 --- a/apps/backend/data/khhao-sources.json +++ b/apps/backend/data/khhao-sources.json @@ -3,5 +3,11 @@ "baseUrl": "https://admin.khhao.com", "username": "order_check", "password": "order_check_test1", - "maxCaptchaAttempts": 3 + "maxCaptchaAttempts": 3, + "autoSync": { + "enabled": true, + "intervalMinutes": 3, + "pages": 2, + "pageSize": 20 + } } diff --git a/apps/backend/data/khhao-sync-state.json b/apps/backend/data/khhao-sync-state.json new file mode 100644 index 00000000..f6dd28b5 --- /dev/null +++ b/apps/backend/data/khhao-sync-state.json @@ -0,0 +1,11 @@ +{ + "syncFromCreatedAt": "2026-05-02T10:53:28.550Z", + "lastRunStartedAt": "2026-05-02T12:36:55.519Z", + "lastRunFinishedAt": "2026-05-02T12:36:56.704Z", + "lastRunStatus": "success", + "lastErrorMessage": "", + "fetchedCount": 40, + "syncedCount": 0, + "ignoredCount": 40, + "lastOrderCreatedAt": "2026-05-02T12:36:45.000Z" +} diff --git a/apps/backend/src/index.js b/apps/backend/src/index.js index 3029e691..fd92a0a3 100644 --- a/apps/backend/src/index.js +++ b/apps/backend/src/index.js @@ -8,6 +8,7 @@ import claimsRouter from './routes/claims.js' import webhooksRouter from './routes/webhooks.js' import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js' import { ensureFulfillmentCatalogBootstrapped } from './services/bootstrap/fulfillment-bootstrap-service.js' +import { startKhhaoAutoSyncLoop, stopKhhaoAutoSyncLoop } from './services/platforms/khhao/auto-sync-service.js' import { closeLocalOcrWorker, warmupLocalOcrWorker } from './services/session/ocr.js' import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session/session.js' import { buildSuccessPayload } from './utils/http.js' @@ -160,6 +161,7 @@ async function bootstrapCoreServices() { attempt: startupState.core.attemptCount, }) + startKhhaoAutoSyncLoop() void bootstrapBrowser() void bootstrapOcr() break @@ -368,6 +370,12 @@ async function shutdown(signal) { logError('[shutdown]', 'failed to close local OCR worker', error) } + try { + stopKhhaoAutoSyncLoop() + } catch (error) { + logError('[shutdown]', 'failed to stop khhao auto sync loop', error) + } + if (!server.listening) { return } diff --git a/apps/backend/src/services/admin/admin-platform-config-service.js b/apps/backend/src/services/admin/admin-platform-config-service.js index 94525171..1053eabf 100644 --- a/apps/backend/src/services/admin/admin-platform-config-service.js +++ b/apps/backend/src/services/admin/admin-platform-config-service.js @@ -13,6 +13,7 @@ import { mapKhhaoOrderPreviewList } from '../platforms/khhao/order-mapper-servic import { queryKhhaoOrderList } from '../platforms/khhao/order-query-service.js' import { syncKhhaoOrders } from '../platforms/khhao/order-sync-service.js' import { getKhhaoSourceConfig, getKhhaoSourcesFilePath, saveKhhaoSourceConfig } from '../platforms/khhao/source-config-service.js' +import { ensureKhhaoSyncBaseline, getKhhaoSyncState, getKhhaoSyncStateFilePath } from '../platforms/khhao/sync-state-service.js' import { loginKhhaoSession } from '../platforms/khhao/session-service.js' import { normalizeAgisoMessageTemplate } from '../platforms/agiso/xianyu/message-service.js' import { @@ -186,15 +187,33 @@ function applyOptionalStringField(target, key, source) { export function getAdminKhhaoSourceConfig() { const config = getKhhaoSourceConfig() + const syncState = getKhhaoSyncState() return { filePath: getKhhaoSourcesFilePath(), + stateFilePath: getKhhaoSyncStateFilePath(), source: { enabled: config.enabled !== false, baseUrl: String(config.baseUrl || '').trim(), username: String(config.username || '').trim(), password: String(config.password || '').trim(), maxCaptchaAttempts: Number(config.maxCaptchaAttempts || 3) || 3, + autoSync: { + enabled: config.autoSync?.enabled === true, + intervalMinutes: Number(config.autoSync?.intervalMinutes || 3) || 3, + pages: Number(config.autoSync?.pages || 2) || 2, + pageSize: Number(config.autoSync?.pageSize || 20) || 20, + }, + }, + syncState: { + syncFromCreatedAt: String(syncState.syncFromCreatedAt || '').trim(), + lastRunStartedAt: String(syncState.lastRunStartedAt || '').trim(), + lastRunFinishedAt: String(syncState.lastRunFinishedAt || '').trim(), + lastRunStatus: String(syncState.lastRunStatus || '').trim(), + lastErrorMessage: String(syncState.lastErrorMessage || '').trim(), + fetchedCount: Number(syncState.fetchedCount || 0), + syncedCount: Number(syncState.syncedCount || 0), + ignoredCount: Number(syncState.ignoredCount || 0), }, } } @@ -207,16 +226,40 @@ export function updateAdminKhhaoSourceConfig(payload = /** @type {AdminKhhaoSour username: String(payload.username || '').trim(), password: String(payload.password || '').trim(), maxCaptchaAttempts: Number(payload.maxCaptchaAttempts || 3) || 3, + autoSync: { + enabled: payload.autoSync?.enabled === true, + intervalMinutes: Number(payload.autoSync?.intervalMinutes || 3) || 3, + pages: Number(payload.autoSync?.pages || 2) || 2, + pageSize: Number(payload.autoSync?.pageSize || 20) || 20, + }, }) + const syncState = saved.autoSync?.enabled ? ensureKhhaoSyncBaseline() : getKhhaoSyncState() return { filePath: getKhhaoSourcesFilePath(), + stateFilePath: getKhhaoSyncStateFilePath(), source: { enabled: saved.enabled !== false, baseUrl: String(saved.baseUrl || '').trim(), username: String(saved.username || '').trim(), password: String(saved.password || '').trim(), maxCaptchaAttempts: Number(saved.maxCaptchaAttempts || 3) || 3, + autoSync: { + enabled: saved.autoSync?.enabled === true, + intervalMinutes: Number(saved.autoSync?.intervalMinutes || 3) || 3, + pages: Number(saved.autoSync?.pages || 2) || 2, + pageSize: Number(saved.autoSync?.pageSize || 20) || 20, + }, + }, + syncState: { + syncFromCreatedAt: String(syncState.syncFromCreatedAt || '').trim(), + lastRunStartedAt: String(syncState.lastRunStartedAt || '').trim(), + lastRunFinishedAt: String(syncState.lastRunFinishedAt || '').trim(), + lastRunStatus: String(syncState.lastRunStatus || '').trim(), + lastErrorMessage: String(syncState.lastErrorMessage || '').trim(), + fetchedCount: Number(syncState.fetchedCount || 0), + syncedCount: Number(syncState.syncedCount || 0), + ignoredCount: Number(syncState.ignoredCount || 0), }, } } diff --git a/apps/backend/src/services/platforms/khhao/auto-sync-service.js b/apps/backend/src/services/platforms/khhao/auto-sync-service.js new file mode 100644 index 00000000..c12de8a5 --- /dev/null +++ b/apps/backend/src/services/platforms/khhao/auto-sync-service.js @@ -0,0 +1,139 @@ +// @ts-check + +import { getKhhaoSourceConfig } from './source-config-service.js' +import { ensureKhhaoSyncBaseline, getKhhaoSyncState, saveKhhaoSyncState } from './sync-state-service.js' +import { syncKhhaoOrders } from './order-sync-service.js' +import { logError, logInfo, logWarn } from '../../../utils/logger.js' +import { nowIso } from '../../../utils/time.js' + +let timer = null +let running = false + +export function startKhhaoAutoSyncLoop() { + scheduleNextRun(5_000) +} + +export function stopKhhaoAutoSyncLoop() { + if (timer) { + clearTimeout(timer) + timer = null + } +} + +async function runKhhaoAutoSyncCycle() { + const source = getKhhaoSourceConfig() + + if (!source.enabled || !source.autoSync?.enabled) { + scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes)) + return + } + + if (running) { + logWarn('[khhao/auto-sync]', '检测到上一次自动同步仍在运行,本轮跳过') + scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes)) + return + } + + if (!String(source.username || '').trim() || !String(source.password || '').trim()) { + logWarn('[khhao/auto-sync]', 'khhao 自动同步已启用,但缺少账号或密码') + scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes)) + return + } + + running = true + const baselineState = ensureKhhaoSyncBaseline() + const startedAt = nowIso() + + saveKhhaoSyncState({ + ...baselineState, + lastRunStartedAt: startedAt, + lastRunStatus: 'running', + lastErrorMessage: '', + }) + + try { + const result = await syncKhhaoOrders({ + baseUrl: source.baseUrl, + username: source.username, + password: source.password, + maxCaptchaAttempts: source.maxCaptchaAttempts, + pages: source.autoSync.pages, + limit: source.autoSync.pageSize, + createdAfter: baselineState.syncFromCreatedAt, + }) + + const latestCreatedAt = resolveLatestOrderCreatedAt(result.results) + + saveKhhaoSyncState({ + ...getKhhaoSyncState(), + syncFromCreatedAt: baselineState.syncFromCreatedAt, + lastRunStartedAt: startedAt, + lastRunFinishedAt: nowIso(), + lastRunStatus: 'success', + lastErrorMessage: '', + fetchedCount: result.fetchedCount, + syncedCount: result.syncedCount, + ignoredCount: result.ignoredCount, + lastOrderCreatedAt: latestCreatedAt, + }) + + logInfo('[khhao/auto-sync]', 'khhao 自动同步完成', { + fetchedCount: result.fetchedCount, + syncedCount: result.syncedCount, + ignoredCount: result.ignoredCount, + syncFromCreatedAt: baselineState.syncFromCreatedAt, + }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error || 'khhao 自动同步失败') + saveKhhaoSyncState({ + ...getKhhaoSyncState(), + syncFromCreatedAt: baselineState.syncFromCreatedAt, + lastRunStartedAt: startedAt, + lastRunFinishedAt: nowIso(), + lastRunStatus: 'failed', + lastErrorMessage: message, + }) + logError('[khhao/auto-sync]', 'khhao 自动同步失败', error) + } finally { + running = false + scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes)) + } +} + +function scheduleNextRun(delayMs) { + stopKhhaoAutoSyncLoop() + timer = setTimeout(() => { + void runKhhaoAutoSyncCycle() + }, delayMs) +} + +function resolveIntervalMs(value) { + const minutes = Number(value) + const normalized = Number.isInteger(minutes) && minutes > 0 ? minutes : 3 + return normalized * 60 * 1000 +} + +function resolveLatestOrderCreatedAt(results = []) { + let latestText = '' + let latestTime = 0 + + for (const item of Array.isArray(results) ? results : []) { + const text = String(item?.rawOrderCreatedAt || item?.orderCreatedAt || '').trim() + const timestamp = parseKhhaoOrderCreatedAt(text) + if (Number.isFinite(timestamp) && timestamp > latestTime) { + latestTime = timestamp + latestText = new Date(timestamp).toISOString() + } + } + + return latestText +} + +function parseKhhaoOrderCreatedAt(value) { + const text = String(value || '').trim() + if (!text) { + return NaN + } + + return Date.parse(`${text.replace(' ', 'T')}+08:00`) +} diff --git a/apps/backend/src/services/platforms/khhao/order-sync-service.js b/apps/backend/src/services/platforms/khhao/order-sync-service.js index 6a706265..540b8253 100644 --- a/apps/backend/src/services/platforms/khhao/order-sync-service.js +++ b/apps/backend/src/services/platforms/khhao/order-sync-service.js @@ -12,7 +12,9 @@ import { loginKhhaoSession } from './session-service.js' * password?: string * page?: number | string * limit?: number | string + * pages?: number | string * maxCaptchaAttempts?: number | string + * createdAfter?: string * }} [payload] */ export async function syncKhhaoOrders(payload = {}) { @@ -26,25 +28,58 @@ export async function syncKhhaoOrders(payload = {}) { const queryResult = await queryKhhaoOrderList({ baseUrl: payload.baseUrl, - page: payload.page, - limit: payload.limit, + page: 1, + limit: resolvePageSize(payload.limit), session, }) + const createdAfter = String(payload.createdAfter || '').trim() + const pages = Math.max(1, Math.min(Number(payload.pages || payload.page || 1) || 1, 5)) const results = [] + const allItems = [...queryResult.items] - for (const item of queryResult.items) { + for (let page = 2; page <= pages; page += 1) { + const nextPage = await queryKhhaoOrderList({ + baseUrl: payload.baseUrl, + page, + limit: resolvePageSize(payload.limit), + session, + }) + allItems.push(...nextPage.items) + } + + for (const item of allItems) { const preview = mapKhhaoOrderPreview(item) + + if (createdAfter && !isKhhaoOrderAfter(preview.orderCreatedAt, createdAfter)) { + results.push({ + platformOrderId: preview.platformOrderId, + provider: 'khhao', + platform: preview.platform || 'unknown', + shopId: preview.shopId, + skuCode: preview.skuCode, + itemTitle: preview.itemTitle, + rawOrderCreatedAt: preview.orderCreatedAt, + ignored: true, + ignoreReason: 'before_sync_baseline', + orderId: null, + orderItemCount: 0, + taskCount: 0, + }) + continue + } + const sourceEvent = mapKhhaoOrderToSourceEvent(item) const upsertResult = await upsertOrderFromSource(sourceEvent, { sourceLabel: 'khhao-sync' }) - results.push({ + results.push({ platformOrderId: preview.platformOrderId, provider: sourceEvent.provider, platform: sourceEvent.platform, shopId: sourceEvent.shopId, skuCode: preview.skuCode, itemTitle: preview.itemTitle, + rawOrderCreatedAt: preview.orderCreatedAt, ignored: Boolean(upsertResult.ignored), ignoreReason: String(upsertResult.ignoreReason || '').trim(), orderId: Number(upsertResult.order?.id || 0) || null, @@ -55,12 +90,42 @@ export async function syncKhhaoOrders(payload = {}) { return { baseUrl: session.baseUrl, - page: queryResult.page, - limit: queryResult.limit, + page: 1, + limit: resolvePageSize(payload.limit), total: queryResult.total, - fetchedCount: queryResult.items.length, + fetchedCount: allItems.length, syncedCount: results.filter((item) => !item.ignored && item.orderId).length, ignoredCount: results.filter((item) => item.ignored).length, results, } } + +function resolvePageSize(value) { + const parsed = Number(value) + if (!Number.isInteger(parsed) || parsed <= 0) { + return 20 + } + + return Math.min(parsed, 50) +} + +function isKhhaoOrderAfter(orderCreatedAt, baselineIso) { + const orderTime = parseKhhaoOrderCreatedAt(orderCreatedAt) + const baselineTime = Date.parse(String(baselineIso || '').trim()) + + if (!Number.isFinite(orderTime) || !Number.isFinite(baselineTime)) { + return true + } + + return orderTime > baselineTime +} + +function parseKhhaoOrderCreatedAt(value) { + const text = String(value || '').trim() + if (!text) { + return NaN + } + + const isoText = text.replace(' ', 'T') + return Date.parse(`${isoText}+08:00`) +} diff --git a/apps/backend/src/services/platforms/khhao/source-config-service.js b/apps/backend/src/services/platforms/khhao/source-config-service.js index db03bc58..450f6d3e 100644 --- a/apps/backend/src/services/platforms/khhao/source-config-service.js +++ b/apps/backend/src/services/platforms/khhao/source-config-service.js @@ -44,6 +44,7 @@ function normalizeKhhaoSourceConfig(rawValue) { username: String(source.username || '').trim(), password: String(source.password || '').trim(), maxCaptchaAttempts: normalizePositiveInteger(source.maxCaptchaAttempts, 3), + autoSync: normalizeAutoSyncConfig(source.autoSync), } } @@ -54,6 +55,18 @@ function createDefaultKhhaoSourceConfig() { username: '', password: '', maxCaptchaAttempts: 3, + autoSync: normalizeAutoSyncConfig({}), + } +} + +function normalizeAutoSyncConfig(rawValue) { + const source = isPlainObject(rawValue) ? rawValue : {} + + return { + enabled: typeof source.enabled === 'boolean' ? source.enabled : false, + intervalMinutes: normalizePositiveInteger(source.intervalMinutes, 3), + pages: normalizePositiveInteger(source.pages, 2), + pageSize: normalizePositiveInteger(source.pageSize, 20), } } diff --git a/apps/backend/src/services/platforms/khhao/sync-state-service.js b/apps/backend/src/services/platforms/khhao/sync-state-service.js new file mode 100644 index 00000000..9ab31797 --- /dev/null +++ b/apps/backend/src/services/platforms/khhao/sync-state-service.js @@ -0,0 +1,92 @@ +// @ts-check + +import fs from 'node:fs' +import path from 'node:path' + +import { PROJECT_ROOT } from '../../../config/runtime.js' +import { nowIso } from '../../../utils/time.js' + +const KHHAO_SYNC_STATE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'khhao-sync-state.json') + +export function getKhhaoSyncStateFilePath() { + return KHHAO_SYNC_STATE_FILE_PATH +} + +export function getKhhaoSyncState() { + return loadKhhaoSyncStateFromFile() +} + +export function saveKhhaoSyncState(rawValue) { + const normalized = normalizeKhhaoSyncState(rawValue) + fs.mkdirSync(path.dirname(KHHAO_SYNC_STATE_FILE_PATH), { recursive: true }) + fs.writeFileSync(KHHAO_SYNC_STATE_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8') + return normalized +} + +export function ensureKhhaoSyncBaseline() { + const current = getKhhaoSyncState() + if (String(current.syncFromCreatedAt || '').trim()) { + return current + } + + return saveKhhaoSyncState({ + ...current, + syncFromCreatedAt: nowIso(), + }) +} + +function loadKhhaoSyncStateFromFile() { + if (!fs.existsSync(KHHAO_SYNC_STATE_FILE_PATH)) { + return createDefaultKhhaoSyncState() + } + + try { + return normalizeKhhaoSyncState(JSON.parse(fs.readFileSync(KHHAO_SYNC_STATE_FILE_PATH, 'utf8'))) + } catch { + return createDefaultKhhaoSyncState() + } +} + +function createDefaultKhhaoSyncState() { + return { + syncFromCreatedAt: '', + lastRunStartedAt: '', + lastRunFinishedAt: '', + lastRunStatus: 'idle', + lastErrorMessage: '', + fetchedCount: 0, + syncedCount: 0, + ignoredCount: 0, + lastOrderCreatedAt: '', + } +} + +function normalizeKhhaoSyncState(rawValue) { + const source = isPlainObject(rawValue) ? rawValue : {} + + return { + syncFromCreatedAt: String(source.syncFromCreatedAt || '').trim(), + lastRunStartedAt: String(source.lastRunStartedAt || '').trim(), + lastRunFinishedAt: String(source.lastRunFinishedAt || '').trim(), + lastRunStatus: normalizeStatus(source.lastRunStatus), + lastErrorMessage: String(source.lastErrorMessage || '').trim(), + fetchedCount: normalizeNonNegativeInteger(source.fetchedCount), + syncedCount: normalizeNonNegativeInteger(source.syncedCount), + ignoredCount: normalizeNonNegativeInteger(source.ignoredCount), + lastOrderCreatedAt: String(source.lastOrderCreatedAt || '').trim(), + } +} + +function normalizeStatus(value) { + const normalized = String(value || '').trim() + return normalized || 'idle' +} + +function normalizeNonNegativeInteger(value) { + const parsed = Number(value) + return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0 +} + +function isPlainObject(value) { + return Object.prototype.toString.call(value) === '[object Object]' +} diff --git a/apps/backend/src/types/admin-write-inputs.js b/apps/backend/src/types/admin-write-inputs.js index ca97d39a..e172b73a 100644 --- a/apps/backend/src/types/admin-write-inputs.js +++ b/apps/backend/src/types/admin-write-inputs.js @@ -74,6 +74,12 @@ export {} * username?: string * password?: string * maxCaptchaAttempts?: number | string + * autoSync?: { + * enabled?: boolean + * intervalMinutes?: number | string + * pages?: number | string + * pageSize?: number | string + * } * }} AdminKhhaoSourceConfigInput */ diff --git a/apps/frontend/src/services/admin.ts b/apps/frontend/src/services/admin.ts index ac41ea2a..162104b7 100644 --- a/apps/frontend/src/services/admin.ts +++ b/apps/frontend/src/services/admin.ts @@ -9,6 +9,7 @@ import type { AdminKhhaoLoginTestResult, AdminKhhaoOrderQueryResult, AdminKhhaoSourceConfig, + AdminKhhaoSyncState, AdminKhhaoOrderSyncResult, AdminInventoryItemListItem, AdminInventorySkuSuggestion, @@ -98,6 +99,8 @@ export function fetchAdminKhhaoSourceConfig() { return apiGet<{ filePath: string source: AdminKhhaoSourceConfig + stateFilePath: string + syncState: AdminKhhaoSyncState }>('/api/v1/admin/platform-config/khhao-source') } @@ -105,6 +108,8 @@ export function saveAdminKhhaoSourceConfig(payload: AdminKhhaoSourceConfig) { return apiPost<{ filePath: string source: AdminKhhaoSourceConfig + stateFilePath: string + syncState: AdminKhhaoSyncState }>('/api/v1/admin/platform-config/khhao-source', payload as unknown as Record) } diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index 85373beb..31693748 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -103,6 +103,23 @@ export interface AdminKhhaoSourceConfig { username: string password: string maxCaptchaAttempts: number + autoSync: { + enabled: boolean + intervalMinutes: number + pages: number + pageSize: number + } +} + +export interface AdminKhhaoSyncState { + syncFromCreatedAt: string + lastRunStartedAt: string + lastRunFinishedAt: string + lastRunStatus: string + lastErrorMessage: string + fetchedCount: number + syncedCount: number + ignoredCount: number } export interface AdminKhhaoOrderPreview { diff --git a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue index 64ea8c59..9f8462d9 100644 --- a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue @@ -17,6 +17,7 @@ import type { AdminAgisoShopConfigItem, AdminKhhaoLoginTestResult, AdminKhhaoOrderQueryResult, + AdminKhhaoSyncState, AdminKhhaoOrderSyncResult, } from '@/types/admin' import { hasAdminRole } from '@/utils/admin-auth' @@ -47,6 +48,7 @@ const activePlatform = ref('agiso') const agisoFilePath = ref('') const khhaoFilePath = ref('') +const khhaoStateFilePath = ref('') const defaults = ref(createEmptyDefaults()) const shops = ref([]) const observedShops = ref([]) @@ -59,6 +61,10 @@ const khhaoForm = ref({ page: 1, limit: 10, maxCaptchaAttempts: 3, + autoSyncEnabled: false, + autoSyncIntervalMinutes: 3, + autoSyncPages: 2, + autoSyncPageSize: 20, }) const khhaoTesting = ref(false) const khhaoQuerying = ref(false) @@ -67,6 +73,7 @@ const khhaoResultError = ref('') const khhaoLoginResult = ref(null) const khhaoQueryResult = ref(null) const khhaoSyncResult = ref(null) +const khhaoAutoSyncState = ref(null) const agisoStats = computed(() => ({ configuredShopCount: shops.value.length, @@ -77,8 +84,8 @@ const agisoStats = computed(() => ({ const khhaoStats = computed(() => ({ hasCredential: Boolean(khhaoForm.value.username.trim() && khhaoForm.value.password.trim()), queryCount: khhaoQueryResult.value?.itemCount || 0, - syncedCount: khhaoSyncResult.value?.syncedCount || 0, - ignoredCount: khhaoSyncResult.value?.ignoredCount || 0, + syncedCount: khhaoAutoSyncState.value?.syncedCount || khhaoSyncResult.value?.syncedCount || 0, + ignoredCount: khhaoAutoSyncState.value?.ignoredCount || khhaoSyncResult.value?.ignoredCount || 0, })) function createEmptyDefaults(): EditableDefaults { @@ -141,6 +148,8 @@ async function loadConfigs() { expandedShopIds.value = [] khhaoFilePath.value = khhaoResponse.data.filePath + khhaoStateFilePath.value = khhaoResponse.data.stateFilePath + khhaoAutoSyncState.value = khhaoResponse.data.syncState khhaoForm.value = { baseUrl: khhaoResponse.data.source.baseUrl || 'https://admin.khhao.com', username: khhaoResponse.data.source.username || '', @@ -148,6 +157,10 @@ async function loadConfigs() { page: 1, limit: 10, maxCaptchaAttempts: khhaoResponse.data.source.maxCaptchaAttempts || 3, + autoSyncEnabled: khhaoResponse.data.source.autoSync?.enabled === true, + autoSyncIntervalMinutes: khhaoResponse.data.source.autoSync?.intervalMinutes || 3, + autoSyncPages: khhaoResponse.data.source.autoSync?.pages || 2, + autoSyncPageSize: khhaoResponse.data.source.autoSync?.pageSize || 20, } } catch (error) { errorMessage.value = error instanceof Error ? error.message : '读取平台配置失败' @@ -294,8 +307,16 @@ async function handleKhhaoSaveSource() { username: khhaoForm.value.username.trim(), password: khhaoForm.value.password.trim(), maxCaptchaAttempts: Number(khhaoForm.value.maxCaptchaAttempts || 3), + autoSync: { + enabled: Boolean(khhaoForm.value.autoSyncEnabled), + intervalMinutes: Number(khhaoForm.value.autoSyncIntervalMinutes || 3), + pages: Number(khhaoForm.value.autoSyncPages || 2), + pageSize: Number(khhaoForm.value.autoSyncPageSize || 20), + }, }) khhaoFilePath.value = response.data.filePath + khhaoStateFilePath.value = response.data.stateFilePath + khhaoAutoSyncState.value = response.data.syncState showSuccess('khhao 来源配置已保存') } catch (error) { khhaoResultError.value = error instanceof Error ? error.message : 'khhao 来源配置保存失败' @@ -610,6 +631,10 @@ onMounted(loadConfigs) 配置文件 {{ khhaoFilePath || '-' }} +
+ 状态文件 + {{ khhaoStateFilePath || '-' }} +
职责 管理 khhao 的来源地址、登录账号、密码与验证码重试次数,并执行测试登录、查询订单与手动同步。 @@ -660,6 +685,29 @@ onMounted(loadConfigs) 验证码重试次数 + + + + + + + +

{{ khhaoResultError }}

@@ -689,6 +737,33 @@ onMounted(loadConfigs)

完成履约配置后可在这里手动同步。

+ +
+
+ 自动同步状态 + {{ khhaoAutoSyncState.lastRunStatus || 'idle' }} +
+
+ 同步基线 + {{ formatAdminDateTime(khhaoAutoSyncState.syncFromCreatedAt) }} +
+
+ 最近开始 + {{ formatAdminDateTime(khhaoAutoSyncState.lastRunStartedAt) }} +
+
+ 最近完成 + {{ formatAdminDateTime(khhaoAutoSyncState.lastRunFinishedAt) }} +
+
+ 最近结果 + 拉取 {{ khhaoAutoSyncState.fetchedCount }} 条,成功 {{ khhaoAutoSyncState.syncedCount }} 条,忽略 {{ khhaoAutoSyncState.ignoredCount }} 条 +
+
+ 最近错误 + {{ khhaoAutoSyncState.lastErrorMessage }} +
+
@@ -1036,6 +1111,14 @@ onMounted(loadConfigs) color: #475467; } +.checkbox-box { + min-height: 42px; + padding: 0 14px; + border-radius: 14px; + border: 1px solid rgba(86, 108, 138, 0.18); + background: rgba(255, 255, 255, 0.96); +} + .result-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -1068,6 +1151,10 @@ onMounted(loadConfigs) color: #64748b; } +.compact-meta { + margin-top: 16px; +} + .data-table { width: 100%; border-collapse: collapse;