增加平台持久化

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
+7
View File
@@ -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
-7
View File
@@ -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']
+50
View File
@@ -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
+78
View File
@@ -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,