khhao 自动同步
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]'
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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<string, unknown>)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<PlatformTab>('agiso')
|
||||
|
||||
const agisoFilePath = ref('')
|
||||
const khhaoFilePath = ref('')
|
||||
const khhaoStateFilePath = ref('')
|
||||
const defaults = ref<EditableDefaults>(createEmptyDefaults())
|
||||
const shops = ref<EditableShop[]>([])
|
||||
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
|
||||
@@ -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<AdminKhhaoLoginTestResult | null>(null)
|
||||
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
|
||||
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(null)
|
||||
const khhaoAutoSyncState = ref<AdminKhhaoSyncState | null>(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)
|
||||
<span class="meta-label">配置文件</span>
|
||||
<code>{{ khhaoFilePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">状态文件</span>
|
||||
<code>{{ khhaoStateFilePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">职责</span>
|
||||
<span>管理 khhao 的来源地址、登录账号、密码与验证码重试次数,并执行测试登录、查询订单与手动同步。</span>
|
||||
@@ -660,6 +685,29 @@ onMounted(loadConfigs)
|
||||
<span>验证码重试次数</span>
|
||||
<input v-model.number="khhaoForm.maxCaptchaAttempts" class="text-input" type="number" min="1" max="5" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>自动同步开关</span>
|
||||
<label class="checkbox-line checkbox-box">
|
||||
<input v-model="khhaoForm.autoSyncEnabled" type="checkbox" />
|
||||
<span>启用自动同步</span>
|
||||
</label>
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>同步间隔(分钟)</span>
|
||||
<input v-model.number="khhaoForm.autoSyncIntervalMinutes" class="text-input" type="number" min="1" max="60" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>同步页数</span>
|
||||
<input v-model.number="khhaoForm.autoSyncPages" class="text-input" type="number" min="1" max="5" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>每页条数</span>
|
||||
<input v-model.number="khhaoForm.autoSyncPageSize" class="text-input" type="number" min="1" max="50" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p v-if="khhaoResultError" class="error-copy">{{ khhaoResultError }}</p>
|
||||
@@ -689,6 +737,33 @@ onMounted(loadConfigs)
|
||||
<p v-else>完成履约配置后可在这里手动同步。</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="khhaoAutoSyncState" class="meta-card compact-meta">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">自动同步状态</span>
|
||||
<span>{{ khhaoAutoSyncState.lastRunStatus || 'idle' }}</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">同步基线</span>
|
||||
<span>{{ formatAdminDateTime(khhaoAutoSyncState.syncFromCreatedAt) }}</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">最近开始</span>
|
||||
<span>{{ formatAdminDateTime(khhaoAutoSyncState.lastRunStartedAt) }}</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">最近完成</span>
|
||||
<span>{{ formatAdminDateTime(khhaoAutoSyncState.lastRunFinishedAt) }}</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">最近结果</span>
|
||||
<span>拉取 {{ khhaoAutoSyncState.fetchedCount }} 条,成功 {{ khhaoAutoSyncState.syncedCount }} 条,忽略 {{ khhaoAutoSyncState.ignoredCount }} 条</span>
|
||||
</div>
|
||||
<div v-if="khhaoAutoSyncState.lastErrorMessage" class="meta-line">
|
||||
<span class="meta-label">最近错误</span>
|
||||
<span class="error-copy">{{ khhaoAutoSyncState.lastErrorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="khhaoQueryResult" class="table-card">
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user