增加平台持久化
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"baseUrl": "https://admin.khhao.com",
|
||||
"username": "order_check",
|
||||
"password": "order_check_test1",
|
||||
"maxCaptchaAttempts": 3
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]'
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -67,6 +67,16 @@ export {}
|
||||
* }} AdminAgisoShopConfigSaveInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* enabled?: boolean
|
||||
* baseUrl?: string
|
||||
* username?: string
|
||||
* password?: string
|
||||
* maxCaptchaAttempts?: number | string
|
||||
* }} AdminKhhaoSourceConfigInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* baseUrl?: string
|
||||
|
||||
Vendored
-7
@@ -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']
|
||||
|
||||
@@ -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<string, unknown>)
|
||||
}
|
||||
|
||||
export function testAdminKhhaoLogin(payload: {
|
||||
baseUrl?: string
|
||||
username: string
|
||||
password: string
|
||||
maxCaptchaAttempts?: number
|
||||
includeImageBase64?: boolean
|
||||
}) {
|
||||
return apiPost<AdminKhhaoLoginTestResult>('/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<AdminKhhaoOrderQueryResult>('/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<AdminKhhaoOrderSyncResult>('/api/v1/admin/platform-config/khhao/sync-orders', payload)
|
||||
}
|
||||
|
||||
export function fetchAdminFulfillmentBindingConfigs() {
|
||||
return apiGet<{
|
||||
filePath: string
|
||||
|
||||
@@ -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<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminKhhaoOrderQueryResult {
|
||||
baseUrl: string
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
itemCount: number
|
||||
items: AdminKhhaoOrderPreview[]
|
||||
rawItems: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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<AdminKhhaoOrderPreview[]>([])
|
||||
const importedKhhaoOrderIds = ref<string[]>([])
|
||||
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)
|
||||
</script>
|
||||
|
||||
@@ -681,6 +750,112 @@ onMounted(loadConfigs)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card table-card--dense">
|
||||
<div class="section-title-row section-title-row--tight">
|
||||
<div>
|
||||
<h3>khhao 订单取样导入</h3>
|
||||
<p>先用 khhao 账号查订单样本,再直接把商品导入成规则草稿,适合首批建规则。</p>
|
||||
</div>
|
||||
<span class="section-note-chip">khhao / kuaishou</span>
|
||||
</div>
|
||||
|
||||
<div class="khhao-toolbar">
|
||||
<label class="field-block field-wide">
|
||||
<span>Base URL</span>
|
||||
<input v-model="khhaoLookupForm.baseUrl" class="text-input" placeholder="https://admin.khhao.com" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>账号</span>
|
||||
<input v-model="khhaoLookupForm.username" class="text-input" placeholder="khhao 登录账号" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>密码</span>
|
||||
<input v-model="khhaoLookupForm.password" class="text-input" type="password" placeholder="khhao 登录密码" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>页码</span>
|
||||
<input v-model.number="khhaoLookupForm.page" class="text-input" type="number" min="1" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>每页条数</span>
|
||||
<input v-model.number="khhaoLookupForm.limit" class="text-input" type="number" min="1" max="50" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>验证码重试</span>
|
||||
<input v-model.number="khhaoLookupForm.maxCaptchaAttempts" class="text-input" type="number" min="1" max="5" />
|
||||
</label>
|
||||
|
||||
<div class="field-block field-block--action">
|
||||
<span>操作</span>
|
||||
<el-button :loading="khhaoLookupLoading" round type="primary" @click="lookupKhhaoProducts">查询 khhao 订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="toolbar-hint">这里直接读取 khhao 订单列表,不写库,只用于导入规则草稿。</p>
|
||||
<p v-if="khhaoLookupErrorMessage" class="error-copy lookup-error">{{ khhaoLookupErrorMessage }}</p>
|
||||
<div v-else-if="khhaoLookupLoading" class="empty-inline">正在查询 khhao 订单…</div>
|
||||
|
||||
<table v-else class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单</th>
|
||||
<th>店铺</th>
|
||||
<th>商品</th>
|
||||
<th>金额 / 状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in khhaoLookupResults" :key="`${item.platformOrderId}:${item.skuCode}:${item.itemId}`">
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.platformOrderId }}</strong>
|
||||
<span class="cell-subtle">{{ item.platformLabel || item.platform || '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.shopName || item.shopId || '-' }}</strong>
|
||||
<span class="cell-subtle">ID: {{ item.shopId || '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.itemTitle || '-' }}</strong>
|
||||
<span class="cell-subtle">SKU: {{ item.skuCode || '-' }}</span>
|
||||
<span class="cell-subtle">ItemId: {{ item.itemId || '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ (item.totalAmountFen / 100).toFixed(2) }}</strong>
|
||||
<span class="cell-subtle">{{ item.statusLabel || item.status || '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<el-button
|
||||
v-if="!isKhhaoProductImported(item.platformOrderId)"
|
||||
link
|
||||
type="primary"
|
||||
@click="importKhhaoProduct(item)"
|
||||
>
|
||||
导入到规则
|
||||
</el-button>
|
||||
<span v-else class="cell-subtle">已导入草稿</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="khhaoLookupResults.length === 0">
|
||||
<td colspan="5" class="empty-inline">还没有 khhao 查询结果。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="table-card table-card--dense">
|
||||
<div class="section-title-row section-title-row--tight">
|
||||
<div>
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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: '审计' })
|
||||
}
|
||||
|
||||
@@ -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<EditableDefaults>(createEmptyDefaults())
|
||||
const shops = ref<EditableShop[]>([])
|
||||
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
|
||||
const expandedShopIds = ref<string[]>([])
|
||||
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<AdminKhhaoLoginTestResult | null>(null)
|
||||
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
|
||||
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(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)
|
||||
</script>
|
||||
|
||||
@@ -270,6 +415,153 @@ onMounted(loadConfigs)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>khhao 数据来源接入</h3>
|
||||
<p>这里用于保存 khhao 来源凭据,并测试登录、查询订单和手动同步。后续履约规则可去“履约配置”页从查询结果里直接导入。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :loading="khhaoSaving" round @click="handleKhhaoSaveSource">保存来源配置</el-button>
|
||||
<el-button :loading="khhaoTesting" round @click="handleKhhaoTestLogin">测试登录</el-button>
|
||||
<el-button :loading="khhaoQuerying" round @click="handleKhhaoQueryOrders">查询订单</el-button>
|
||||
<el-button :loading="khhaoSyncing" round type="primary" @click="handleKhhaoSyncOrders">同步订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">配置文件</span>
|
||||
<code>{{ khhaoFilePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">说明</span>
|
||||
<span>khhao 的 Base URL、账号、密码和验证码重试次数会单独保存在该文件中,不与 Agiso 店铺配置混用。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shop-grid">
|
||||
<label class="field-block field-wide">
|
||||
<span>Base URL</span>
|
||||
<input v-model="khhaoForm.baseUrl" class="text-input" placeholder="https://admin.khhao.com" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>账号</span>
|
||||
<input v-model="khhaoForm.username" class="text-input" placeholder="khhao 登录账号" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>密码</span>
|
||||
<input v-model="khhaoForm.password" class="text-input" type="password" placeholder="khhao 登录密码" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>页码</span>
|
||||
<input v-model.number="khhaoForm.page" class="text-input" type="number" min="1" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>每页条数</span>
|
||||
<input v-model.number="khhaoForm.limit" class="text-input" type="number" min="1" max="50" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>验证码重试次数</span>
|
||||
<input v-model.number="khhaoForm.maxCaptchaAttempts" class="text-input" type="number" min="1" max="5" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p v-if="khhaoResultError" class="error-copy">{{ khhaoResultError }}</p>
|
||||
|
||||
<div v-if="khhaoLoginResult" class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">登录测试</span>
|
||||
<span>账号 {{ khhaoLoginResult.username }},第 {{ khhaoLoginResult.attempt }} 次成功,Cookie {{ khhaoLoginResult.session.cookieCount }} 个,验证码识别:{{ khhaoLoginResult.captcha.recognizedText || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">时间</span>
|
||||
<span>{{ formatAdminDateTime(khhaoLoginResult.loggedInAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="khhaoQueryResult">
|
||||
<div class="section-title-row compact-top">
|
||||
<div>
|
||||
<h3>khhao 查询结果</h3>
|
||||
<p>当前页 {{ khhaoQueryResult.page }},本页 {{ khhaoQueryResult.itemCount }} 条,总数 {{ khhaoQueryResult.total }}。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单</th>
|
||||
<th>店铺</th>
|
||||
<th>商品</th>
|
||||
<th>金额</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in khhaoQueryResult.items" :key="`${item.platformOrderId}:${item.skuCode}:${item.itemId}`">
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.platformOrderId }}</strong>
|
||||
<span class="cell-subtle">{{ item.platformLabel || item.platform || '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.shopName || item.shopId || '-' }}</strong>
|
||||
<span class="cell-subtle">ID: {{ item.shopId || '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.itemTitle || '-' }}</strong>
|
||||
<span class="cell-subtle">SKU: {{ item.skuCode || '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ (item.totalAmountFen / 100).toFixed(2) }}</td>
|
||||
<td>{{ item.statusLabel || item.status || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template v-if="khhaoSyncResult">
|
||||
<div class="section-title-row compact-top">
|
||||
<div>
|
||||
<h3>khhao 同步结果</h3>
|
||||
<p>拉取 {{ khhaoSyncResult.fetchedCount }} 条,成功 {{ khhaoSyncResult.syncedCount }} 条,忽略 {{ khhaoSyncResult.ignoredCount }} 条。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单</th>
|
||||
<th>店铺</th>
|
||||
<th>商品</th>
|
||||
<th>结果</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in khhaoSyncResult.results" :key="`${item.platformOrderId}:${item.skuCode}`">
|
||||
<td>{{ item.platformOrderId }}</td>
|
||||
<td>{{ item.shopId || '-' }}</td>
|
||||
<td>{{ item.itemTitle || item.skuCode || '-' }}</td>
|
||||
<td>
|
||||
<span v-if="item.ignored">已忽略:{{ item.ignoreReason || '-' }}</span>
|
||||
<span v-else>已入库,订单 {{ item.orderId || '-' }},任务 {{ item.taskCount }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
@@ -437,6 +729,10 @@ onMounted(loadConfigs)
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.compact-top {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.meta-card,
|
||||
.table-card,
|
||||
.empty-block,
|
||||
|
||||
Reference in New Issue
Block a user