diff --git a/apps/backend/data/khhao-sources.json b/apps/backend/data/khhao-sources.json new file mode 100644 index 00000000..b2bed778 --- /dev/null +++ b/apps/backend/data/khhao-sources.json @@ -0,0 +1,7 @@ +{ + "enabled": true, + "baseUrl": "https://admin.khhao.com", + "username": "order_check", + "password": "order_check_test1", + "maxCaptchaAttempts": 3 +} diff --git a/apps/backend/data/order-fulfillment-bindings.json b/apps/backend/data/order-fulfillment-bindings.json index 922c9738..a19dec6f 100644 --- a/apps/backend/data/order-fulfillment-bindings.json +++ b/apps/backend/data/order-fulfillment-bindings.json @@ -1,4 +1,23 @@ [ + { + "provider": "khhao", + "platform": "kuaishou", + "shopId": "10", + "skuCode": "测试快手1", + "skuName": "测试1", + "profileKey": "manual_review", + "enabled": true, + "priority": 100, + "config": {}, + "match": { + "externalSkuCode": "830", + "externalItemId": "830", + "externalSkuName": "测试1", + "config": { + "resolvedSkuName": "测试1" + } + } + }, { "provider": "agiso", "platform": "xianyu", diff --git a/apps/backend/src/routes/admin/platform-config.js b/apps/backend/src/routes/admin/platform-config.js index aae12919..029b09c3 100644 --- a/apps/backend/src/routes/admin/platform-config.js +++ b/apps/backend/src/routes/admin/platform-config.js @@ -5,10 +5,13 @@ import { Router } from 'express' import { getAdminAgisoShopConfigs, getAdminFulfillmentBindingConfigs, + getAdminKhhaoSourceConfig, lookupAdminFulfillmentBindingOrder, queryAdminKhhaoOrders, + syncAdminKhhaoOrders, testAdminKhhaoLogin, updateAdminAgisoShopConfigs, + updateAdminKhhaoSourceConfig, updateAdminFulfillmentBindingConfigs, } from '../../services/admin/admin-platform-config-service.js' import { createJsonHandler, requireAdminRoles } from './shared.js' @@ -17,6 +20,7 @@ import { createJsonHandler, requireAdminRoles } from './shared.js' /** @typedef {import('../../types/admin-route-inputs.js').AdminFulfillmentBindingConfigRouteBody} AdminFulfillmentBindingConfigRouteBody */ /** @typedef {import('../../types/admin-route-inputs.js').AdminFulfillmentBindingLookupRouteBody} AdminFulfillmentBindingLookupRouteBody */ /** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoOrderQueryRouteBody} AdminKhhaoOrderQueryRouteBody */ +/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoSourceConfigRouteBody} AdminKhhaoSourceConfigRouteBody */ /** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoTestLoginRouteBody} AdminKhhaoTestLoginRouteBody */ /** @typedef {import('../../types/admin-write-models.js').AdminAgisoShopConfigSaveResponse} AdminAgisoShopConfigSaveResponse */ @@ -54,6 +58,37 @@ router.post('/platform-config/agiso-shops', createJsonHandler( }, )) +router.get('/platform-config/khhao-source', createJsonHandler( + () => getAdminKhhaoSourceConfig(), + { + successMessage: 'ok', + errorMessage: '读取 khhao 来源配置失败', + scope: '[admin/platform-config/khhao-source]', + }, +)) + +router.post('/platform-config/khhao-source', createJsonHandler( + (req) => updateAdminKhhaoSourceConfig(/** @type {AdminKhhaoSourceConfigRouteBody} */ (req.body)), + { + successMessage: 'khhao 来源配置已保存', + errorMessage: '保存 khhao 来源配置失败', + scope: '[admin/platform-config/khhao-source]', + audit: (_req, data) => { + const result = /** @type {{ filePath?: string, source?: { username?: string, enabled?: boolean } }} */ (data) + return { + action: 'platform_khhao_source_updated', + targetType: 'platform_config', + targetId: 'khhao_source', + data: { + filePath: String(result.filePath || '').trim(), + username: String(result.source?.username || '').trim(), + enabled: Boolean(result.source?.enabled), + }, + } + }, + }, +)) + router.post('/platform-config/khhao/test-login', createJsonHandler( (req) => testAdminKhhaoLogin(/** @type {AdminKhhaoTestLoginRouteBody} */ (req.body)), { @@ -102,6 +137,32 @@ router.post('/platform-config/khhao/query-orders', createJsonHandler( }, )) +router.post('/platform-config/khhao/sync-orders', createJsonHandler( + (req) => syncAdminKhhaoOrders(/** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)), + { + successMessage: 'khhao 订单同步成功', + errorMessage: 'khhao 订单同步失败', + scope: '[admin/platform-config/khhao/sync-orders]', + audit: (req, data) => { + const body = /** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body) + const result = /** @type {{ page?: number, limit?: number, fetchedCount?: number, syncedCount?: number, ignoredCount?: number, baseUrl?: string }} */ (data) + return { + action: 'platform_khhao_sync_orders', + targetType: 'platform_config', + targetId: String(body.username || '').trim() || 'khhao', + data: { + baseUrl: result.baseUrl || String(body.baseUrl || '').trim(), + page: Number(result.page || 1), + limit: Number(result.limit || 50), + fetchedCount: Number(result.fetchedCount || 0), + syncedCount: Number(result.syncedCount || 0), + ignoredCount: Number(result.ignoredCount || 0), + }, + } + }, + }, +)) + router.get('/platform-config/fulfillment-bindings', createJsonHandler( () => getAdminFulfillmentBindingConfigs(), { 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 7dd1092a..94525171 100644 --- a/apps/backend/src/services/admin/admin-platform-config-service.js +++ b/apps/backend/src/services/admin/admin-platform-config-service.js @@ -11,6 +11,8 @@ import { import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js' import { mapKhhaoOrderPreviewList } from '../platforms/khhao/order-mapper-service.js' 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 { loginKhhaoSession } from '../platforms/khhao/session-service.js' import { normalizeAgisoMessageTemplate } from '../platforms/agiso/xianyu/message-service.js' import { @@ -26,6 +28,7 @@ import { normalizeProductName } from '../order/product-match-service.js' import { resolveDisplayShopName } from './admin-read-shared-helpers.js' /** @typedef {import('../../types/admin-write-inputs.js').AdminAgisoShopConfigSaveInput} AdminAgisoShopConfigSaveInput */ +/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigInput */ /** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */ /** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */ /** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigSaveInput */ @@ -181,15 +184,53 @@ function applyOptionalStringField(target, key, source) { delete target[key] } +export function getAdminKhhaoSourceConfig() { + const config = getKhhaoSourceConfig() + + return { + filePath: getKhhaoSourcesFilePath(), + 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, + }, + } +} + +/** @param {AdminKhhaoSourceConfigInput} [payload] */ +export function updateAdminKhhaoSourceConfig(payload = /** @type {AdminKhhaoSourceConfigInput} */ ({})) { + const saved = saveKhhaoSourceConfig({ + enabled: payload.enabled !== false, + baseUrl: String(payload.baseUrl || '').trim() || 'https://admin.khhao.com', + username: String(payload.username || '').trim(), + password: String(payload.password || '').trim(), + maxCaptchaAttempts: Number(payload.maxCaptchaAttempts || 3) || 3, + }) + + return { + filePath: getKhhaoSourcesFilePath(), + 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, + }, + } +} + /** @param {AdminKhhaoTestLoginInput} [payload] */ export async function testAdminKhhaoLogin(payload = /** @type {AdminKhhaoTestLoginInput} */ ({})) { + const savedSource = getKhhaoSourceConfig() const session = await loginKhhaoSession({ - baseUrl: payload.baseUrl, - username: payload.username, - password: payload.password, - maxCaptchaAttempts: payload.maxCaptchaAttempts, + baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(), + username: String(payload.username || savedSource.username || '').trim(), + password: String(payload.password || savedSource.password || '').trim(), + maxCaptchaAttempts: payload.maxCaptchaAttempts || savedSource.maxCaptchaAttempts, includeImageBase64: payload.includeImageBase64, - requestId: `admin-khhao-test-login:${String(payload.username || '').trim() || 'anonymous'}`, + requestId: `admin-khhao-test-login:${String(payload.username || savedSource.username || '').trim() || 'anonymous'}`, }) return { @@ -212,16 +253,17 @@ export async function testAdminKhhaoLogin(payload = /** @type {AdminKhhaoTestLog /** @param {AdminKhhaoOrderQueryInput} [payload] */ export async function queryAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQueryInput} */ ({})) { + const savedSource = getKhhaoSourceConfig() const session = await loginKhhaoSession({ - baseUrl: payload.baseUrl, - username: payload.username, - password: payload.password, - maxCaptchaAttempts: payload.maxCaptchaAttempts, - requestId: `admin-khhao-query-orders:${String(payload.username || '').trim() || 'anonymous'}`, + baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(), + username: String(payload.username || savedSource.username || '').trim(), + password: String(payload.password || savedSource.password || '').trim(), + maxCaptchaAttempts: payload.maxCaptchaAttempts || savedSource.maxCaptchaAttempts, + requestId: `admin-khhao-query-orders:${String(payload.username || savedSource.username || '').trim() || 'anonymous'}`, }) const result = await queryKhhaoOrderList({ - baseUrl: payload.baseUrl, + baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(), page: payload.page, limit: payload.limit, session, @@ -239,6 +281,18 @@ export async function queryAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrder } } +/** @param {AdminKhhaoOrderQueryInput} [payload] */ +export async function syncAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQueryInput} */ ({})) { + const savedSource = getKhhaoSourceConfig() + return syncKhhaoOrders({ + ...payload, + baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(), + username: String(payload.username || savedSource.username || '').trim(), + password: String(payload.password || savedSource.password || '').trim(), + maxCaptchaAttempts: payload.maxCaptchaAttempts || savedSource.maxCaptchaAttempts, + }) +} + export async function getAdminFulfillmentBindingConfigs() { const bindings = getOrderFulfillmentBindingConfigs() const rowsResult = await query( diff --git a/apps/backend/src/services/order/order-service.js b/apps/backend/src/services/order/order-service.js index 46877bb2..5e4f1254 100644 --- a/apps/backend/src/services/order/order-service.js +++ b/apps/backend/src/services/order/order-service.js @@ -9,6 +9,10 @@ import { nowIso } from '../../utils/time.js' import { logWebhook } from '../../utils/logger.js' export async function upsertOrderFromWebhook(event) { + return upsertOrderFromSource(event, { sourceLabel: 'webhook' }) +} + +export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = {}) { const now = nowIso() const existing = await findOrderByPlatformOrderId({ provider: event.provider, @@ -17,7 +21,7 @@ export async function upsertOrderFromWebhook(event) { platformOrderId: event.platformOrderId, }) - logWebhook('[order-service]', '开始处理 webhook 订单 upsert', { + logWebhook('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, { provider: event.provider, platform: event.platform, shopId: event.shopId, @@ -37,7 +41,7 @@ export async function upsertOrderFromWebhook(event) { const configuredItems = resolvedItems.filter((item) => item.isConfigured) if (configuredItems.length === 0) { - logWebhook('[order-service]', 'Webhook 订单已忽略:未命中任何已配置履约商品', { + logWebhook('[order-service]', `${sourceLabel} 订单已忽略:未命中任何已配置履约商品`, { provider: event.provider, platform: event.platform, shopId: event.shopId, @@ -104,7 +108,7 @@ export async function upsertOrderFromWebhook(event) { const tasks = await syncDeliveryTasksForOrder(order, orderItems) const messageDeliveries = [] - logWebhook('[order-service]', 'Webhook 订单 upsert 完成', { + logWebhook('[order-service]', `${sourceLabel} 订单 upsert 完成`, { orderId: order.id, provider: order.provider, platform: order.platform, diff --git a/apps/backend/src/services/platforms/khhao/order-mapper-service.js b/apps/backend/src/services/platforms/khhao/order-mapper-service.js index d467540a..f1438710 100644 --- a/apps/backend/src/services/platforms/khhao/order-mapper-service.js +++ b/apps/backend/src/services/platforms/khhao/order-mapper-service.js @@ -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]' } diff --git a/apps/backend/src/services/platforms/khhao/order-sync-service.js b/apps/backend/src/services/platforms/khhao/order-sync-service.js new file mode 100644 index 00000000..6a706265 --- /dev/null +++ b/apps/backend/src/services/platforms/khhao/order-sync-service.js @@ -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, + } +} diff --git a/apps/backend/src/services/platforms/khhao/source-config-service.js b/apps/backend/src/services/platforms/khhao/source-config-service.js new file mode 100644 index 00000000..db03bc58 --- /dev/null +++ b/apps/backend/src/services/platforms/khhao/source-config-service.js @@ -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]' +} diff --git a/apps/backend/src/types/admin-route-inputs.js b/apps/backend/src/types/admin-route-inputs.js index 016fb5fc..10233076 100644 --- a/apps/backend/src/types/admin-route-inputs.js +++ b/apps/backend/src/types/admin-route-inputs.js @@ -30,6 +30,10 @@ export {} * @typedef {import('./admin-read-inputs.js').AdminWebhookEventListQueryInput} AdminWebhookEventRouteQuery */ +/** + * @typedef {import('./admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigRouteBody + */ + /** * @typedef {import('./admin-write-inputs.js').AdminAgisoShopConfigSaveInput} AdminAgisoShopConfigRouteBody */ diff --git a/apps/backend/src/types/admin-write-inputs.js b/apps/backend/src/types/admin-write-inputs.js index 6f77db2c..ca97d39a 100644 --- a/apps/backend/src/types/admin-write-inputs.js +++ b/apps/backend/src/types/admin-write-inputs.js @@ -67,6 +67,16 @@ export {} * }} AdminAgisoShopConfigSaveInput */ +/** + * @typedef {{ + * enabled?: boolean + * baseUrl?: string + * username?: string + * password?: string + * maxCaptchaAttempts?: number | string + * }} AdminKhhaoSourceConfigInput + */ + /** * @typedef {{ * baseUrl?: string diff --git a/apps/frontend/src/components.d.ts b/apps/frontend/src/components.d.ts index 7a7a34a3..21f1402c 100644 --- a/apps/frontend/src/components.d.ts +++ b/apps/frontend/src/components.d.ts @@ -13,20 +13,13 @@ declare module 'vue' { export interface GlobalComponents { AdminPaginationBar: typeof import('./components/admin/AdminPaginationBar.vue')['default'] AdminStatusTag: typeof import('./components/admin/AdminStatusTag.vue')['default'] - ElAlert: typeof import('element-plus/es')['ElAlert'] ElButton: typeof import('element-plus/es')['ElButton'] - ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElDialog: typeof import('element-plus/es')['ElDialog'] - ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] - ElOption: typeof import('element-plus/es')['ElOption'] - ElSelect: typeof import('element-plus/es')['ElSelect'] - ElSpace: typeof import('element-plus/es')['ElSpace'] - ElTag: typeof import('element-plus/es')['ElTag'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] TencentAuthCard: typeof import('./components/tencent/TencentAuthCard.vue')['default'] diff --git a/apps/frontend/src/services/admin.ts b/apps/frontend/src/services/admin.ts index c9435c5f..ac41ea2a 100644 --- a/apps/frontend/src/services/admin.ts +++ b/apps/frontend/src/services/admin.ts @@ -6,6 +6,10 @@ import type { AdminAuditLogItem, AdminFulfillmentLookupResult, AdminFulfillmentBindingConfigItem, + AdminKhhaoLoginTestResult, + AdminKhhaoOrderQueryResult, + AdminKhhaoSourceConfig, + AdminKhhaoOrderSyncResult, AdminInventoryItemListItem, AdminInventorySkuSuggestion, AdminMessageDeliveryListItem, @@ -90,6 +94,52 @@ export function saveAdminAgisoShopConfigs(payload: { }>('/api/v1/admin/platform-config/agiso-shops', payload) } +export function fetchAdminKhhaoSourceConfig() { + return apiGet<{ + filePath: string + source: AdminKhhaoSourceConfig + }>('/api/v1/admin/platform-config/khhao-source') +} + +export function saveAdminKhhaoSourceConfig(payload: AdminKhhaoSourceConfig) { + return apiPost<{ + filePath: string + source: AdminKhhaoSourceConfig + }>('/api/v1/admin/platform-config/khhao-source', payload as unknown as Record) +} + +export function testAdminKhhaoLogin(payload: { + baseUrl?: string + username: string + password: string + maxCaptchaAttempts?: number + includeImageBase64?: boolean +}) { + return apiPost('/api/v1/admin/platform-config/khhao/test-login', payload) +} + +export function queryAdminKhhaoOrders(payload: { + baseUrl?: string + username: string + password: string + page?: number + limit?: number + maxCaptchaAttempts?: number +}) { + return apiPost('/api/v1/admin/platform-config/khhao/query-orders', payload) +} + +export function syncAdminKhhaoOrders(payload: { + baseUrl?: string + username: string + password: string + page?: number + limit?: number + maxCaptchaAttempts?: number +}) { + return apiPost('/api/v1/admin/platform-config/khhao/sync-orders', payload) +} + export function fetchAdminFulfillmentBindingConfigs() { return apiGet<{ filePath: string diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index 67072bf6..85373beb 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -80,6 +80,84 @@ export interface AdminAgisoObservedShopItem { configured: boolean } +export interface AdminKhhaoLoginTestResult { + baseUrl: string + username: string + loggedInAt: string + attempt: number + responseMessage: string + captcha: { + recognizedText: string + imageBase64: string + } + session: { + cookieKeys: string[] + cookieCount: number + cookieHeaderMasked: string + } +} + +export interface AdminKhhaoSourceConfig { + enabled: boolean + baseUrl: string + username: string + password: string + maxCaptchaAttempts: number +} + +export interface AdminKhhaoOrderPreview { + provider: string + platform: string + platformLabel: string + platformOrderId: string + shopId: string + shopName: string + itemId: string + itemTitle: string + skuCode: string + quantity: number + totalAmountFen: number + status: string + statusLabel: string + orderCreatedAt: string + raw: Record +} + +export interface AdminKhhaoOrderQueryResult { + baseUrl: string + page: number + limit: number + total: number + itemCount: number + items: AdminKhhaoOrderPreview[] + rawItems: Record[] +} + +export interface AdminKhhaoOrderSyncItem { + platformOrderId: string + provider: string + platform: string + shopId: string + skuCode: string + itemTitle: string + ignored: boolean + ignoreReason: string + orderId: number | null + orderItemCount: number + taskCount: number +} + +export interface AdminKhhaoOrderSyncResult { + baseUrl: string + page: number + limit: number + total: number + fetchedCount: number + syncedCount: number + ignoredCount: number + results: AdminKhhaoOrderSyncItem[] +} + export interface AdminFulfillmentBindingConfigItem { provider: string platform: string diff --git a/apps/frontend/src/views/admin/AdminFulfillmentBindingsView.vue b/apps/frontend/src/views/admin/AdminFulfillmentBindingsView.vue index 881c671f..30e40279 100644 --- a/apps/frontend/src/views/admin/AdminFulfillmentBindingsView.vue +++ b/apps/frontend/src/views/admin/AdminFulfillmentBindingsView.vue @@ -4,6 +4,8 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue' import { showSuccess } from '@/lib/feedback' import { fetchAdminFulfillmentBindingConfigs, + fetchAdminKhhaoSourceConfig, + queryAdminKhhaoOrders, lookupAdminFulfillmentBindingOrder, saveAdminFulfillmentBindingConfigs, } from '@/services/admin' @@ -11,6 +13,7 @@ import type { AdminFulfillmentBindingConfigItem, AdminFulfillmentLookupItem, AdminFulfillmentLookupResult, + AdminKhhaoOrderPreview, AdminObservedProductItem, } from '@/types/admin' import { hasAdminRole } from '@/utils/admin-auth' @@ -82,6 +85,18 @@ const lookupForm = reactive({ shopId: '', platformOrderId: '', }) +const khhaoLookupLoading = ref(false) +const khhaoLookupErrorMessage = ref('') +const khhaoLookupResults = ref([]) +const importedKhhaoOrderIds = ref([]) +const khhaoLookupForm = reactive({ + baseUrl: 'https://admin.khhao.com', + username: '', + password: '', + page: 1, + limit: 10, + maxCaptchaAttempts: 3, +}) const ruleMetrics = computed(() => { const completedCount = bindings.value.filter(isEditableBindingComplete).length @@ -205,12 +220,19 @@ async function loadConfigs() { errorMessage.value = '' try { - const response = await fetchAdminFulfillmentBindingConfigs() + const [response, khhaoSourceResponse] = await Promise.all([ + fetchAdminFulfillmentBindingConfigs(), + fetchAdminKhhaoSourceConfig(), + ]) filePath.value = response.data.filePath const nextBindings = response.data.bindings.map(mapEditableBinding) bindings.value = nextBindings collapsedBindingIds.value = buildCollapsedIds(nextBindings) observedProducts.value = response.data.observedProducts + khhaoLookupForm.baseUrl = khhaoSourceResponse.data.source.baseUrl || 'https://admin.khhao.com' + khhaoLookupForm.username = khhaoSourceResponse.data.source.username || '' + khhaoLookupForm.password = khhaoSourceResponse.data.source.password || '' + khhaoLookupForm.maxCaptchaAttempts = khhaoSourceResponse.data.source.maxCaptchaAttempts || 3 } catch (error) { errorMessage.value = error instanceof Error ? error.message : '读取履约配置失败' } finally { @@ -269,6 +291,25 @@ function isLookupProductImported(lineId: string) { return importedLookupLineIds.value.includes(lineId) } +function importKhhaoProduct(item: AdminKhhaoOrderPreview) { + importObservedProduct({ + provider: item.provider || 'khhao', + platform: item.platform || 'unknown', + shopId: item.shopId, + externalSkuCode: item.skuCode, + externalItemId: item.itemId, + externalSkuName: item.itemTitle, + }) + + if (!importedKhhaoOrderIds.value.includes(item.platformOrderId)) { + importedKhhaoOrderIds.value = [...importedKhhaoOrderIds.value, item.platformOrderId] + } +} + +function isKhhaoProductImported(platformOrderId: string) { + return importedKhhaoOrderIds.value.includes(platformOrderId) +} + function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload { return { provider: item.provider.trim() || 'agiso', @@ -459,6 +500,34 @@ async function lookupOrderProducts() { } } +async function lookupKhhaoProducts() { + if (!khhaoLookupForm.username.trim() || !khhaoLookupForm.password.trim()) { + khhaoLookupErrorMessage.value = '请先填写 khhao 账号和密码' + return + } + + khhaoLookupLoading.value = true + khhaoLookupErrorMessage.value = '' + + try { + const response = await queryAdminKhhaoOrders({ + baseUrl: khhaoLookupForm.baseUrl.trim() || 'https://admin.khhao.com', + username: khhaoLookupForm.username.trim(), + password: khhaoLookupForm.password.trim(), + page: Number(khhaoLookupForm.page || 1), + limit: Number(khhaoLookupForm.limit || 10), + maxCaptchaAttempts: Number(khhaoLookupForm.maxCaptchaAttempts || 3), + }) + khhaoLookupResults.value = response.data.items + importedKhhaoOrderIds.value = [] + } catch (error) { + khhaoLookupResults.value = [] + khhaoLookupErrorMessage.value = error instanceof Error ? error.message : 'khhao 订单查询失败' + } finally { + khhaoLookupLoading.value = false + } +} + onMounted(loadConfigs) @@ -681,6 +750,112 @@ onMounted(loadConfigs) +
+
+
+

khhao 订单取样导入

+

先用 khhao 账号查订单样本,再直接把商品导入成规则草稿,适合首批建规则。

+
+ khhao / kuaishou +
+ +
+ + + + + + + + + + + + +
+ 操作 + 查询 khhao 订单 +
+
+ +

这里直接读取 khhao 订单列表,不写库,只用于导入规则草稿。

+

{{ khhaoLookupErrorMessage }}

+
正在查询 khhao 订单…
+ + + + + + + + + + + + + + + + + + + + + + + +
订单店铺商品金额 / 状态操作
+
+ {{ item.platformOrderId }} + {{ item.platformLabel || item.platform || '-' }} +
+
+
+ {{ item.shopName || item.shopId || '-' }} + ID: {{ item.shopId || '-' }} +
+
+
+ {{ item.itemTitle || '-' }} + SKU: {{ item.skuCode || '-' }} + ItemId: {{ item.itemId || '-' }} +
+
+
+ {{ (item.totalAmountFen / 100).toFixed(2) }} + {{ item.statusLabel || item.status || '-' }} +
+
+ + 导入到规则 + + 已导入草稿 +
还没有 khhao 查询结果。
+
+
@@ -1027,6 +1202,7 @@ onMounted(loadConfigs) } .lookup-toolbar, +.khhao-toolbar, .lookup-summary { display: grid; gap: 12px; @@ -1037,6 +1213,11 @@ onMounted(loadConfigs) align-items: end; } +.khhao-toolbar { + grid-template-columns: repeat(4, minmax(0, 1fr)); + align-items: end; +} + .field-block--action :deep(.el-button) { width: 100%; } diff --git a/apps/frontend/src/views/admin/AdminLayout.vue b/apps/frontend/src/views/admin/AdminLayout.vue index 048bc246..f30a14e5 100644 --- a/apps/frontend/src/views/admin/AdminLayout.vue +++ b/apps/frontend/src/views/admin/AdminLayout.vue @@ -38,7 +38,7 @@ const navItems = computed(() => { if (isAdmin.value) { baseItems.splice(1, 0, { to: '/admin/users', label: '用户' }) - baseItems.push({ to: '/admin/platform-shops', label: 'Agiso店铺' }) + baseItems.push({ to: '/admin/platform-shops', label: '平台配置' }) baseItems.push({ to: '/admin/platform-fulfillment', label: '履约配置' }) baseItems.push({ to: '/admin/audit-logs', label: '审计' }) } diff --git a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue index 87e83540..6db793c3 100644 --- a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue @@ -2,11 +2,22 @@ import { onMounted, ref } from 'vue' import { showError, showSuccess } from '@/lib/feedback' -import { fetchAdminAgisoShopConfigs, saveAdminAgisoShopConfigs } from '@/services/admin' +import { + fetchAdminAgisoShopConfigs, + fetchAdminKhhaoSourceConfig, + queryAdminKhhaoOrders, + saveAdminAgisoShopConfigs, + saveAdminKhhaoSourceConfig, + syncAdminKhhaoOrders, + testAdminKhhaoLogin, +} from '@/services/admin' import type { AdminAgisoMessagingDefaults, AdminAgisoObservedShopItem, AdminAgisoShopConfigItem, + AdminKhhaoLoginTestResult, + AdminKhhaoOrderQueryResult, + AdminKhhaoOrderSyncResult, } from '@/types/admin' import { hasAdminRole } from '@/utils/admin-auth' import { formatAdminDateTime } from '@/utils/admin-time' @@ -28,12 +39,29 @@ type EditableShop = { const loading = ref(true) const saving = ref(false) +const khhaoSaving = ref(false) const errorMessage = ref('') const filePath = ref('') +const khhaoFilePath = ref('') const defaults = ref(createEmptyDefaults()) const shops = ref([]) const observedShops = ref([]) const expandedShopIds = ref([]) +const khhaoForm = ref({ + baseUrl: 'https://admin.khhao.com', + username: '', + password: '', + page: 1, + limit: 10, + maxCaptchaAttempts: 3, +}) +const khhaoTesting = ref(false) +const khhaoQuerying = ref(false) +const khhaoSyncing = ref(false) +const khhaoResultError = ref('') +const khhaoLoginResult = ref(null) +const khhaoQueryResult = ref(null) +const khhaoSyncResult = ref(null) function createEmptyDefaults(): EditableDefaults { return { @@ -83,11 +111,23 @@ async function loadConfigs() { errorMessage.value = '' try { - const response = await fetchAdminAgisoShopConfigs() - filePath.value = response.data.filePath - defaults.value = mapEditableDefaults(response.data.defaults) - shops.value = response.data.shops.map(mapEditableShop) - observedShops.value = response.data.observedShops + const [agisoResponse, khhaoResponse] = await Promise.all([ + fetchAdminAgisoShopConfigs(), + fetchAdminKhhaoSourceConfig(), + ]) + filePath.value = agisoResponse.data.filePath + defaults.value = mapEditableDefaults(agisoResponse.data.defaults) + shops.value = agisoResponse.data.shops.map(mapEditableShop) + observedShops.value = agisoResponse.data.observedShops + khhaoFilePath.value = khhaoResponse.data.filePath + khhaoForm.value = { + baseUrl: khhaoResponse.data.source.baseUrl || 'https://admin.khhao.com', + username: khhaoResponse.data.source.username || '', + password: khhaoResponse.data.source.password || '', + page: 1, + limit: 10, + maxCaptchaAttempts: khhaoResponse.data.source.maxCaptchaAttempts || 3, + } expandedShopIds.value = [] } catch (error) { errorMessage.value = error instanceof Error ? error.message : '读取店铺配置失败' @@ -207,6 +247,111 @@ async function saveConfigs() { } } +function buildKhhaoPayload() { + return { + baseUrl: khhaoForm.value.baseUrl.trim() || 'https://admin.khhao.com', + username: khhaoForm.value.username.trim(), + password: khhaoForm.value.password.trim(), + page: Number(khhaoForm.value.page || 1), + limit: Number(khhaoForm.value.limit || 10), + maxCaptchaAttempts: Number(khhaoForm.value.maxCaptchaAttempts || 3), + } +} + +function ensureKhhaoCredentials() { + if (!khhaoForm.value.username.trim() || !khhaoForm.value.password.trim()) { + khhaoResultError.value = '请先填写 khhao 账号和密码' + return false + } + + return true +} + +async function handleKhhaoTestLogin() { + if (!ensureKhhaoCredentials()) { + return + } + + khhaoTesting.value = true + khhaoResultError.value = '' + + try { + const response = await testAdminKhhaoLogin({ + ...buildKhhaoPayload(), + includeImageBase64: false, + }) + khhaoLoginResult.value = response.data + showSuccess('khhao 登录测试成功') + } catch (error) { + khhaoResultError.value = error instanceof Error ? error.message : 'khhao 登录测试失败' + showError(khhaoResultError.value) + } finally { + khhaoTesting.value = false + } +} + +async function handleKhhaoSaveSource() { + khhaoSaving.value = true + khhaoResultError.value = '' + + try { + const response = await saveAdminKhhaoSourceConfig({ + enabled: true, + baseUrl: khhaoForm.value.baseUrl.trim() || 'https://admin.khhao.com', + username: khhaoForm.value.username.trim(), + password: khhaoForm.value.password.trim(), + maxCaptchaAttempts: Number(khhaoForm.value.maxCaptchaAttempts || 3), + }) + khhaoFilePath.value = response.data.filePath + showSuccess('khhao 来源配置已保存') + } catch (error) { + khhaoResultError.value = error instanceof Error ? error.message : 'khhao 来源配置保存失败' + showError(khhaoResultError.value) + } finally { + khhaoSaving.value = false + } +} + +async function handleKhhaoQueryOrders() { + if (!ensureKhhaoCredentials()) { + return + } + + khhaoQuerying.value = true + khhaoResultError.value = '' + + try { + const response = await queryAdminKhhaoOrders(buildKhhaoPayload()) + khhaoQueryResult.value = response.data + showSuccess(`khhao 订单查询成功,共返回 ${response.data.itemCount} 条`) + } catch (error) { + khhaoResultError.value = error instanceof Error ? error.message : 'khhao 订单查询失败' + showError(khhaoResultError.value) + } finally { + khhaoQuerying.value = false + } +} + +async function handleKhhaoSyncOrders() { + if (!ensureKhhaoCredentials()) { + return + } + + khhaoSyncing.value = true + khhaoResultError.value = '' + + try { + const response = await syncAdminKhhaoOrders(buildKhhaoPayload()) + khhaoSyncResult.value = response.data + showSuccess(`khhao 同步完成:拉取 ${response.data.fetchedCount} 条,成功 ${response.data.syncedCount} 条`) + } catch (error) { + khhaoResultError.value = error instanceof Error ? error.message : 'khhao 订单同步失败' + showError(khhaoResultError.value) + } finally { + khhaoSyncing.value = false + } +} + onMounted(loadConfigs) @@ -270,6 +415,153 @@ onMounted(loadConfigs)
+
+
+
+

khhao 数据来源接入

+

这里用于保存 khhao 来源凭据,并测试登录、查询订单和手动同步。后续履约规则可去“履约配置”页从查询结果里直接导入。

+
+
+ 保存来源配置 + 测试登录 + 查询订单 + 同步订单 +
+
+ +
+
+ 配置文件 + {{ khhaoFilePath || '-' }} +
+
+ 说明 + khhao 的 Base URL、账号、密码和验证码重试次数会单独保存在该文件中,不与 Agiso 店铺配置混用。 +
+
+ +
+ + + + + + + + + + + +
+ +

{{ khhaoResultError }}

+ +
+
+ 登录测试 + 账号 {{ khhaoLoginResult.username }},第 {{ khhaoLoginResult.attempt }} 次成功,Cookie {{ khhaoLoginResult.session.cookieCount }} 个,验证码识别:{{ khhaoLoginResult.captcha.recognizedText || '-' }} +
+
+ 时间 + {{ formatAdminDateTime(khhaoLoginResult.loggedInAt) }} +
+
+ + + + +
+
@@ -437,6 +729,10 @@ onMounted(loadConfigs) flex-wrap: wrap; } +.compact-top { + margin-top: 16px; +} + .meta-card, .table-card, .empty-block,