增加平台持久化

This commit is contained in:
yml2213
2026-05-02 18:26:01 +08:00
parent 8829652e9a
commit e94fff95fb
16 changed files with 988 additions and 29 deletions
@@ -6,6 +6,42 @@ export function mapKhhaoOrderPreviewList(items = []) {
return (Array.isArray(items) ? items : []).map((item) => mapKhhaoOrderPreview(item))
}
export function mapKhhaoOrderToSourceEvent(item = {}) {
const preview = mapKhhaoOrderPreview(item)
const orderStatus = resolveKhhaoOrderStatus(preview.status)
const payStatus = resolveKhhaoPayStatus(preview.status)
return {
provider: 'khhao',
platform: preview.platform || 'unknown',
shopId: preview.shopId,
shopName: preview.shopName,
platformOrderId: preview.platformOrderId,
orderStatus,
payStatus,
buyerId: '',
buyerName: String(preview.raw.name || '').trim(),
receiverContact: '',
totalAmount: preview.totalAmountFen,
currency: 'CNY',
paidAt: payStatus === 'paid' ? normalizePaidAt(preview.orderCreatedAt) : null,
rawPayload: preview.raw,
items: [
{
itemId: preview.itemId,
externalItemId: preview.itemId,
externalSkuCode: preview.skuCode || preview.itemId,
externalSkuName: preview.itemTitle || preview.skuCode || preview.itemId,
skuCode: preview.skuCode || preview.itemId,
skuName: preview.itemTitle || preview.skuCode || preview.itemId,
quantity: preview.quantity,
snapshot: preview.raw,
spec: preview.raw,
},
],
}
}
export function mapKhhaoOrderPreview(item = {}) {
const raw = isPlainObject(item) ? item : {}
const platform = resolveKhhaoPlatform(raw.pingtai)
@@ -40,6 +76,34 @@ export function resolveKhhaoPlatform(value) {
return normalized ? 'unknown' : ''
}
export function resolveKhhaoOrderStatus(value) {
const normalized = String(value || '').trim()
if (normalized === '2') {
return 'paid'
}
if (normalized === '0') {
return 'created'
}
return 'unknown'
}
export function resolveKhhaoPayStatus(value) {
const normalized = String(value || '').trim()
if (normalized === '2') {
return 'paid'
}
if (normalized === '0') {
return 'unpaid'
}
return 'unknown'
}
function normalizeQuantity(value) {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
@@ -49,6 +113,11 @@ function stripHtmlTags(value) {
return String(value || '').replace(/<[^>]+>/g, '').trim()
}
function normalizePaidAt(value) {
const text = String(value || '').trim()
return text ? text.replace(' ', 'T') : null
}
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -0,0 +1,66 @@
// @ts-check
import { upsertOrderFromSource } from '../../order/order-service.js'
import { mapKhhaoOrderPreview, mapKhhaoOrderToSourceEvent } from './order-mapper-service.js'
import { queryKhhaoOrderList } from './order-query-service.js'
import { loginKhhaoSession } from './session-service.js'
/**
* @param {{
* baseUrl?: string
* username?: string
* password?: string
* page?: number | string
* limit?: number | string
* maxCaptchaAttempts?: number | string
* }} [payload]
*/
export async function syncKhhaoOrders(payload = {}) {
const session = await loginKhhaoSession({
baseUrl: payload.baseUrl,
username: payload.username,
password: payload.password,
maxCaptchaAttempts: payload.maxCaptchaAttempts,
requestId: `admin-khhao-sync-orders:${String(payload.username || '').trim() || 'anonymous'}`,
})
const queryResult = await queryKhhaoOrderList({
baseUrl: payload.baseUrl,
page: payload.page,
limit: payload.limit,
session,
})
const results = []
for (const item of queryResult.items) {
const preview = mapKhhaoOrderPreview(item)
const sourceEvent = mapKhhaoOrderToSourceEvent(item)
const upsertResult = await upsertOrderFromSource(sourceEvent, { sourceLabel: 'khhao-sync' })
results.push({
platformOrderId: preview.platformOrderId,
provider: sourceEvent.provider,
platform: sourceEvent.platform,
shopId: sourceEvent.shopId,
skuCode: preview.skuCode,
itemTitle: preview.itemTitle,
ignored: Boolean(upsertResult.ignored),
ignoreReason: String(upsertResult.ignoreReason || '').trim(),
orderId: Number(upsertResult.order?.id || 0) || null,
orderItemCount: Array.isArray(upsertResult.orderItems) ? upsertResult.orderItems.length : 0,
taskCount: Array.isArray(upsertResult.tasks) ? upsertResult.tasks.length : 0,
})
}
return {
baseUrl: session.baseUrl,
page: queryResult.page,
limit: queryResult.limit,
total: queryResult.total,
fetchedCount: queryResult.items.length,
syncedCount: results.filter((item) => !item.ignored && item.orderId).length,
ignoredCount: results.filter((item) => item.ignored).length,
results,
}
}
@@ -0,0 +1,67 @@
// @ts-check
import fs from 'node:fs'
import path from 'node:path'
import { PROJECT_ROOT } from '../../../config/runtime.js'
const KHHAO_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'khhao-sources.json')
export function getKhhaoSourcesFilePath() {
return KHHAO_SOURCES_FILE_PATH
}
export function getKhhaoSourceConfig() {
return loadKhhaoSourceConfigFromFile()
}
export function saveKhhaoSourceConfig(rawValue) {
const normalized = normalizeKhhaoSourceConfig(rawValue)
fs.mkdirSync(path.dirname(KHHAO_SOURCES_FILE_PATH), { recursive: true })
fs.writeFileSync(KHHAO_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
return normalized
}
function loadKhhaoSourceConfigFromFile() {
if (!fs.existsSync(KHHAO_SOURCES_FILE_PATH)) {
return createDefaultKhhaoSourceConfig()
}
try {
const rawText = fs.readFileSync(KHHAO_SOURCES_FILE_PATH, 'utf8')
return normalizeKhhaoSourceConfig(JSON.parse(rawText))
} catch {
return createDefaultKhhaoSourceConfig()
}
}
function normalizeKhhaoSourceConfig(rawValue) {
const source = isPlainObject(rawValue) ? rawValue : {}
return {
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
baseUrl: String(source.baseUrl || 'https://admin.khhao.com').trim() || 'https://admin.khhao.com',
username: String(source.username || '').trim(),
password: String(source.password || '').trim(),
maxCaptchaAttempts: normalizePositiveInteger(source.maxCaptchaAttempts, 3),
}
}
function createDefaultKhhaoSourceConfig() {
return {
enabled: true,
baseUrl: 'https://admin.khhao.com',
username: '',
password: '',
maxCaptchaAttempts: 3,
}
}
function normalizePositiveInteger(value, fallback) {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
}
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}