增加feifei履约配置界面
This commit is contained in:
@@ -58,6 +58,7 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
|
|||||||
version: "1",
|
version: "1",
|
||||||
},
|
},
|
||||||
kuaishouFeifei: {
|
kuaishouFeifei: {
|
||||||
|
enabled: true,
|
||||||
baseUrl: "http://skin-exchange.yiquyou.icu",
|
baseUrl: "http://skin-exchange.yiquyou.icu",
|
||||||
appKey: "",
|
appKey: "",
|
||||||
appSecret: "",
|
appSecret: "",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Router } from "express";
|
|||||||
|
|
||||||
import { requireAdminRoles } from "./session.js";
|
import { requireAdminRoles } from "./session.js";
|
||||||
import cloudtentaclesRouter from "./platform-config/cloudtentacles.js";
|
import cloudtentaclesRouter from "./platform-config/cloudtentacles.js";
|
||||||
|
import kuaishouFeifeiRouter from "./platform-config/kuaishou-feifei.js";
|
||||||
import kuaishouEticketRouter from "./platform-config/kuaishou-eticket.js";
|
import kuaishouEticketRouter from "./platform-config/kuaishou-eticket.js";
|
||||||
import ninetyoneRouter from "./platform-config/ninetyone.js";
|
import ninetyoneRouter from "./platform-config/ninetyone.js";
|
||||||
import notificationsRouter from "./platform-config/notifications.js";
|
import notificationsRouter from "./platform-config/notifications.js";
|
||||||
@@ -11,6 +12,7 @@ const router = Router();
|
|||||||
router.use("/platform-config", requireAdminRoles(["admin"]));
|
router.use("/platform-config", requireAdminRoles(["admin"]));
|
||||||
router.use("/platform-config", notificationsRouter);
|
router.use("/platform-config", notificationsRouter);
|
||||||
router.use("/platform-config", kuaishouEticketRouter);
|
router.use("/platform-config", kuaishouEticketRouter);
|
||||||
|
router.use("/platform-config", kuaishouFeifeiRouter);
|
||||||
router.use("/platform-config", ninetyoneRouter);
|
router.use("/platform-config", ninetyoneRouter);
|
||||||
router.use("/platform-config", cloudtentaclesRouter);
|
router.use("/platform-config", cloudtentaclesRouter);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAdminKuaishouFeifeiConfig,
|
||||||
|
matchAdminKuaishouFeifeiProduct,
|
||||||
|
updateAdminKuaishouFeifeiConfig,
|
||||||
|
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js";
|
||||||
|
import { createJsonHandler } from "../session.js";
|
||||||
|
import type { JsonRecord } from "../../../types/json.js";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/kuaishou-feifei",
|
||||||
|
createJsonHandler(() => getAdminKuaishouFeifeiConfig(), {
|
||||||
|
successMessage: "ok",
|
||||||
|
errorMessage: "读取 kuaishou-feifei 配置失败",
|
||||||
|
scope: "[admin/platform-config/kuaishou-feifei]",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/kuaishou-feifei",
|
||||||
|
createJsonHandler(
|
||||||
|
(req) => updateAdminKuaishouFeifeiConfig(req.body as JsonRecord),
|
||||||
|
{
|
||||||
|
successMessage: "kuaishou-feifei 配置已保存",
|
||||||
|
errorMessage: "保存 kuaishou-feifei 配置失败",
|
||||||
|
scope: "[admin/platform-config/kuaishou-feifei]",
|
||||||
|
audit: (_req, data) => {
|
||||||
|
const result = data as JsonRecord;
|
||||||
|
const source = result.source as JsonRecord | undefined;
|
||||||
|
return {
|
||||||
|
action: "platform_kuaishou_feifei_config_updated",
|
||||||
|
targetType: "platform_config",
|
||||||
|
targetId: "kuaishou_feifei",
|
||||||
|
data: {
|
||||||
|
filePath: String(result.filePath || "").trim(),
|
||||||
|
enabled: source?.enabled !== false,
|
||||||
|
ruleCount: Array.isArray(source?.productRules)
|
||||||
|
? source.productRules.length
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/kuaishou-feifei/match",
|
||||||
|
createJsonHandler(
|
||||||
|
(req) => matchAdminKuaishouFeifeiProduct(req.body as JsonRecord),
|
||||||
|
{
|
||||||
|
successMessage: "ok",
|
||||||
|
errorMessage: "匹配 kuaishou-feifei 商品失败",
|
||||||
|
scope: "[admin/platform-config/kuaishou-feifei/match]",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { getKuaishouFeifeiConfig } from '../../platforms/kuaishou-feifei/config.js'
|
||||||
|
import { resolveKuaishouFeifeiProductByName } from '../../platforms/kuaishou-feifei/product-rule-service.js'
|
||||||
|
import {
|
||||||
|
getKuaishouFeifeiConfigFilePath,
|
||||||
|
getKuaishouFeifeiSourceConfig,
|
||||||
|
hasKuaishouFeifeiConfigFile,
|
||||||
|
normalizeKuaishouFeifeiSourceConfig,
|
||||||
|
saveKuaishouFeifeiSourceConfig,
|
||||||
|
} from '../../platforms/kuaishou-feifei/source-config-service.js'
|
||||||
|
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
||||||
|
|
||||||
|
type JsonObject = Record<string, any>
|
||||||
|
|
||||||
|
export function getAdminKuaishouFeifeiConfig() {
|
||||||
|
const source = getAdminEditableKuaishouFeifeiConfig()
|
||||||
|
const effective = getKuaishouFeifeiConfig()
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath: getKuaishouFeifeiConfigFilePath(),
|
||||||
|
source,
|
||||||
|
effective: mapEffectiveKuaishouFeifeiConfig(effective),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAdminKuaishouFeifeiConfig(payload: JsonObject = {}) {
|
||||||
|
const saved = saveKuaishouFeifeiSourceConfig(payload)
|
||||||
|
const effective = getKuaishouFeifeiConfig()
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath: getKuaishouFeifeiConfigFilePath(),
|
||||||
|
source: saved,
|
||||||
|
effective: mapEffectiveKuaishouFeifeiConfig(effective),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchAdminKuaishouFeifeiProduct(payload: JsonObject = {}) {
|
||||||
|
const productName = String(payload.productName || '').trim()
|
||||||
|
const source = getAdminEditableKuaishouFeifeiConfig()
|
||||||
|
const match = resolveKuaishouFeifeiProductByName(productName, {
|
||||||
|
enabled: source.enabled,
|
||||||
|
rules: source.productRules,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
productName,
|
||||||
|
normalizedProductName: normalizeCloudtentaclesMatchName(productName),
|
||||||
|
matched: Boolean(match),
|
||||||
|
match,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdminEditableKuaishouFeifeiConfig() {
|
||||||
|
if (hasKuaishouFeifeiConfigFile()) {
|
||||||
|
return getKuaishouFeifeiSourceConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeKuaishouFeifeiSourceConfig(getKuaishouFeifeiConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouFeifeiConfig>) {
|
||||||
|
return {
|
||||||
|
enabled: config.enabled !== false,
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
|
timeoutMs: config.timeoutMs,
|
||||||
|
notifyUrl: config.notifyUrl,
|
||||||
|
hasAppKey: Boolean(config.appKey),
|
||||||
|
hasAppSecret: Boolean(config.appSecret),
|
||||||
|
productRuleCount: config.productRules.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { runtimeConfig } from '../../../config/runtime.js'
|
import { runtimeConfig } from '../../../config/runtime.js'
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import type { RuntimeConfig } from '../../../types/runtime-config.js'
|
import type { RuntimeConfig } from '../../../types/runtime-config.js'
|
||||||
|
import {
|
||||||
|
getKuaishouFeifeiSourceConfig,
|
||||||
|
hasKuaishouFeifeiConfigFile,
|
||||||
|
} from './source-config-service.js'
|
||||||
|
|
||||||
export const KUAISHOU_FEIFEI_EXECUTOR_KEY = 'kuaishou_feifei'
|
export const KUAISHOU_FEIFEI_EXECUTOR_KEY = 'kuaishou_feifei'
|
||||||
export const KUAISHOU_FEIFEI_PROFILE_KEY = 'kuaishou_feifei'
|
export const KUAISHOU_FEIFEI_PROFILE_KEY = 'kuaishou_feifei'
|
||||||
@@ -8,12 +12,14 @@ export const KUAISHOU_FEIFEI_PROFILE_KEY = 'kuaishou_feifei'
|
|||||||
type KuaishouFeifeiRuntimeConfig = RuntimeConfig['platforms']['kuaishouFeifei']
|
type KuaishouFeifeiRuntimeConfig = RuntimeConfig['platforms']['kuaishouFeifei']
|
||||||
|
|
||||||
export function getKuaishouFeifeiConfig(overrides: Partial<KuaishouFeifeiRuntimeConfig> = {}) {
|
export function getKuaishouFeifeiConfig(overrides: Partial<KuaishouFeifeiRuntimeConfig> = {}) {
|
||||||
const config = {
|
const runtimeValue = runtimeConfig.platforms?.kuaishouFeifei || {}
|
||||||
...(runtimeConfig.platforms?.kuaishouFeifei || {}),
|
const savedValue = hasKuaishouFeifeiConfigFile()
|
||||||
...overrides,
|
? getKuaishouFeifeiSourceConfig()
|
||||||
}
|
: null
|
||||||
|
const config = mergeKuaishouFeifeiConfig(runtimeValue, savedValue, overrides)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
enabled: config.enabled !== false,
|
||||||
baseUrl: String(config.baseUrl || 'http://skin-exchange.yiquyou.icu').trim().replace(/\/+$/, ''),
|
baseUrl: String(config.baseUrl || 'http://skin-exchange.yiquyou.icu').trim().replace(/\/+$/, ''),
|
||||||
appKey: String(config.appKey || '').trim(),
|
appKey: String(config.appKey || '').trim(),
|
||||||
appSecret: String(config.appSecret || '').trim(),
|
appSecret: String(config.appSecret || '').trim(),
|
||||||
@@ -26,6 +32,13 @@ export function getKuaishouFeifeiConfig(overrides: Partial<KuaishouFeifeiRuntime
|
|||||||
export function assertKuaishouFeifeiConfig() {
|
export function assertKuaishouFeifeiConfig() {
|
||||||
const config = getKuaishouFeifeiConfig()
|
const config = getKuaishouFeifeiConfig()
|
||||||
|
|
||||||
|
if (config.enabled === false) {
|
||||||
|
throw createHttpError('kuaishou-feifei 已停用', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'kuaishou_feifei_disabled',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!config.baseUrl) {
|
if (!config.baseUrl) {
|
||||||
throw createHttpError('kuaishou-feifei baseUrl 未配置', {
|
throw createHttpError('kuaishou-feifei baseUrl 未配置', {
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
@@ -42,3 +55,41 @@ export function assertKuaishouFeifeiConfig() {
|
|||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeKuaishouFeifeiConfig(
|
||||||
|
runtimeValue: Partial<KuaishouFeifeiRuntimeConfig>,
|
||||||
|
savedValue: Partial<KuaishouFeifeiRuntimeConfig> | null,
|
||||||
|
overrides: Partial<KuaishouFeifeiRuntimeConfig>,
|
||||||
|
): KuaishouFeifeiRuntimeConfig {
|
||||||
|
const base: KuaishouFeifeiRuntimeConfig = savedValue
|
||||||
|
? {
|
||||||
|
enabled: savedValue.enabled !== false,
|
||||||
|
baseUrl: savedValue.baseUrl || runtimeValue.baseUrl || 'http://skin-exchange.yiquyou.icu',
|
||||||
|
appKey: savedValue.appKey || runtimeValue.appKey || '',
|
||||||
|
appSecret: savedValue.appSecret || runtimeValue.appSecret || '',
|
||||||
|
timeoutMs: savedValue.timeoutMs || runtimeValue.timeoutMs || 10000,
|
||||||
|
notifyUrl: savedValue.notifyUrl || runtimeValue.notifyUrl || '',
|
||||||
|
productRules: Array.isArray(savedValue.productRules)
|
||||||
|
? savedValue.productRules
|
||||||
|
: runtimeValue.productRules || [],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
enabled: runtimeValue.enabled !== false,
|
||||||
|
baseUrl: runtimeValue.baseUrl || 'http://skin-exchange.yiquyou.icu',
|
||||||
|
appKey: runtimeValue.appKey || '',
|
||||||
|
appSecret: runtimeValue.appSecret || '',
|
||||||
|
timeoutMs: runtimeValue.timeoutMs || 10000,
|
||||||
|
notifyUrl: runtimeValue.notifyUrl || '',
|
||||||
|
productRules: runtimeValue.productRules || [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: overrides.enabled ?? base.enabled,
|
||||||
|
baseUrl: overrides.baseUrl ?? base.baseUrl,
|
||||||
|
appKey: overrides.appKey ?? base.appKey,
|
||||||
|
appSecret: overrides.appSecret ?? base.appSecret,
|
||||||
|
timeoutMs: overrides.timeoutMs ?? base.timeoutMs,
|
||||||
|
notifyUrl: overrides.notifyUrl ?? base.notifyUrl,
|
||||||
|
productRules: overrides.productRules ?? base.productRules,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { runtimeConfig } from '../../../config/runtime.js'
|
|
||||||
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
||||||
|
import { getKuaishouFeifeiConfig } from './config.js'
|
||||||
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
||||||
|
|
||||||
export type KuaishouFeifeiProductMatch = {
|
export type KuaishouFeifeiProductMatch = {
|
||||||
@@ -12,15 +12,19 @@ export type KuaishouFeifeiProductMatch = {
|
|||||||
|
|
||||||
export function resolveKuaishouFeifeiProductByName(
|
export function resolveKuaishouFeifeiProductByName(
|
||||||
productName: unknown,
|
productName: unknown,
|
||||||
|
options: {
|
||||||
|
rules?: KuaishouFeifeiProductRule[]
|
||||||
|
enabled?: boolean
|
||||||
|
} = {},
|
||||||
): KuaishouFeifeiProductMatch | null {
|
): KuaishouFeifeiProductMatch | null {
|
||||||
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
|
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
|
||||||
if (!normalizedProductName) {
|
if (!normalizedProductName) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules = listKuaishouFeifeiProductRules()
|
const rules = listKuaishouFeifeiProductRules(options)
|
||||||
const matched = rules.find((rule) =>
|
const matched = rules.find((rule) =>
|
||||||
normalizeCloudtentaclesMatchName(rule.productName) === normalizedProductName,
|
normalizeCloudtentaclesMatchName(rule.productName || rule.normalizedProductName) === normalizedProductName,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!matched) {
|
if (!matched) {
|
||||||
@@ -41,14 +45,25 @@ export function resolveKuaishouFeifeiProductByName(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function listKuaishouFeifeiProductRules(): KuaishouFeifeiProductRule[] {
|
function listKuaishouFeifeiProductRules(options: {
|
||||||
const rules = runtimeConfig.platforms?.kuaishouFeifei?.productRules
|
rules?: KuaishouFeifeiProductRule[]
|
||||||
|
enabled?: boolean
|
||||||
|
}): KuaishouFeifeiProductRule[] {
|
||||||
|
const config = options.rules ? null : getKuaishouFeifeiConfig()
|
||||||
|
if (options.enabled === false || config?.enabled === false) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = options.rules || config?.productRules
|
||||||
return (Array.isArray(rules) ? rules : [])
|
return (Array.isArray(rules) ? rules : [])
|
||||||
.map((rule) => ({
|
.map((rule) => ({
|
||||||
|
id: String(rule.id || '').trim(),
|
||||||
productName: String(rule.productName || '').trim(),
|
productName: String(rule.productName || '').trim(),
|
||||||
|
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||||
productCode: String(rule.productCode || '').trim(),
|
productCode: String(rule.productCode || '').trim(),
|
||||||
skuName: String(rule.skuName || '').trim(),
|
skuName: String(rule.skuName || '').trim(),
|
||||||
enabled: rule.enabled !== false,
|
enabled: rule.enabled !== false,
|
||||||
|
notes: String(rule.notes || '').trim(),
|
||||||
}))
|
}))
|
||||||
.filter((rule) => rule.enabled && rule.productName && rule.productCode)
|
.filter((rule) => rule.enabled && rule.productName && rule.productCode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import { normalizeKuaishouFeifeiSourceConfig } from './source-config-service.js'
|
||||||
|
|
||||||
|
test('normalizeKuaishouFeifeiSourceConfig keeps valid product rules and drops invalid ones', () => {
|
||||||
|
const config = normalizeKuaishouFeifeiSourceConfig({
|
||||||
|
enabled: true,
|
||||||
|
baseUrl: ' https://feifei.example.com/ ',
|
||||||
|
appKey: ' app-key ',
|
||||||
|
appSecret: ' secret ',
|
||||||
|
timeoutMs: 0,
|
||||||
|
productRules: [
|
||||||
|
{
|
||||||
|
productName: '套装-Alan Walker',
|
||||||
|
productCode: 'FF-1001',
|
||||||
|
skuName: 'Alan Walker 套装',
|
||||||
|
notes: '测试规则',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
productName: '套装-Alan Walker',
|
||||||
|
productCode: 'FF-1002',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
productName: '缺少编码',
|
||||||
|
productCode: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(config.enabled, true)
|
||||||
|
assert.equal(config.baseUrl, 'https://feifei.example.com/')
|
||||||
|
assert.equal(config.appKey, 'app-key')
|
||||||
|
assert.equal(config.appSecret, 'secret')
|
||||||
|
assert.equal(config.timeoutMs, 10000)
|
||||||
|
assert.equal(config.productRules.length, 1)
|
||||||
|
assert.equal(config.productRules[0].productName, '套装-Alan Walker')
|
||||||
|
assert.equal(config.productRules[0].productCode, 'FF-1002')
|
||||||
|
assert.equal(config.productRules[0].normalizedProductName, '套装 alan walker')
|
||||||
|
})
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||||
|
import { readJsonFile, writeJsonFile } from '../../../utils/json-file-store.js'
|
||||||
|
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
||||||
|
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
||||||
|
|
||||||
|
const KUAISHOU_FEIFEI_CONFIG_FILE_PATH = path.join(
|
||||||
|
PROJECT_ROOT,
|
||||||
|
'data',
|
||||||
|
'kuaishou-feifei-config.json',
|
||||||
|
)
|
||||||
|
|
||||||
|
type JsonObject = Record<string, any>
|
||||||
|
|
||||||
|
export type KuaishouFeifeiSourceConfig = {
|
||||||
|
enabled: boolean
|
||||||
|
baseUrl: string
|
||||||
|
appKey: string
|
||||||
|
appSecret: string
|
||||||
|
timeoutMs: number
|
||||||
|
notifyUrl: string
|
||||||
|
productRules: KuaishouFeifeiProductRule[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKuaishouFeifeiConfigFilePath() {
|
||||||
|
return KUAISHOU_FEIFEI_CONFIG_FILE_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasKuaishouFeifeiConfigFile() {
|
||||||
|
return fs.existsSync(KUAISHOU_FEIFEI_CONFIG_FILE_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKuaishouFeifeiSourceConfig(): KuaishouFeifeiSourceConfig {
|
||||||
|
return readJsonFile(
|
||||||
|
KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||||
|
createDefaultKuaishouFeifeiSourceConfig,
|
||||||
|
normalizeKuaishouFeifeiSourceConfig,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveKuaishouFeifeiSourceConfig(rawValue: unknown): KuaishouFeifeiSourceConfig {
|
||||||
|
return writeJsonFile(
|
||||||
|
KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||||
|
rawValue,
|
||||||
|
normalizeKuaishouFeifeiSourceConfig,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeKuaishouFeifeiSourceConfig(rawValue: unknown): KuaishouFeifeiSourceConfig {
|
||||||
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: source.enabled !== false,
|
||||||
|
baseUrl: String(source.baseUrl || 'http://skin-exchange.yiquyou.icu').trim(),
|
||||||
|
appKey: String(source.appKey || '').trim(),
|
||||||
|
appSecret: String(source.appSecret || '').trim(),
|
||||||
|
timeoutMs: normalizePositiveInteger(source.timeoutMs, 10000),
|
||||||
|
notifyUrl: String(source.notifyUrl || '').trim(),
|
||||||
|
productRules: normalizeKuaishouFeifeiProductRules(source.productRules),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeKuaishouFeifeiProductRules(value: unknown): KuaishouFeifeiProductRule[] {
|
||||||
|
const rules = Array.isArray(value) ? value : []
|
||||||
|
const normalizedRules = rules
|
||||||
|
.map((item) => normalizeKuaishouFeifeiProductRule(item))
|
||||||
|
.filter((item): item is KuaishouFeifeiProductRule => Boolean(item))
|
||||||
|
|
||||||
|
return dedupeProductRules(normalizedRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKuaishouFeifeiProductRule(rawValue: unknown): KuaishouFeifeiProductRule | null {
|
||||||
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
|
const productName = String(source.productName || source.name || '').trim()
|
||||||
|
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
|
||||||
|
const productCode = String(source.productCode || source.code || '').trim()
|
||||||
|
|
||||||
|
if (!productName || !normalizedProductName || !productCode) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(source.id || normalizedProductName).trim() || normalizedProductName,
|
||||||
|
enabled: source.enabled !== false,
|
||||||
|
productName,
|
||||||
|
normalizedProductName,
|
||||||
|
productCode,
|
||||||
|
skuName: String(source.skuName || source.productName || productName).trim(),
|
||||||
|
notes: String(source.notes || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeProductRules(rules: KuaishouFeifeiProductRule[]) {
|
||||||
|
const map = new Map<string, KuaishouFeifeiProductRule>()
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
const key = normalizeCloudtentaclesMatchName(rule.productName)
|
||||||
|
if (!key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set(key, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultKuaishouFeifeiSourceConfig(): KuaishouFeifeiSourceConfig {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
baseUrl: 'http://skin-exchange.yiquyou.icu',
|
||||||
|
appKey: '',
|
||||||
|
appSecret: '',
|
||||||
|
timeoutMs: 10000,
|
||||||
|
notifyUrl: '',
|
||||||
|
productRules: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePositiveInteger(value: unknown, fallback: number) {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is JsonObject {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Object]'
|
||||||
|
}
|
||||||
@@ -6,10 +6,13 @@ export type AdminDefaultUser = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type KuaishouFeifeiProductRule = {
|
export type KuaishouFeifeiProductRule = {
|
||||||
|
id?: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
|
normalizedProductName?: string;
|
||||||
productCode: string;
|
productCode: string;
|
||||||
skuName?: string;
|
skuName?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
notes?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RuntimeConfig = {
|
export type RuntimeConfig = {
|
||||||
@@ -98,6 +101,7 @@ export type RuntimeConfig = {
|
|||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
kuaishouFeifei: {
|
kuaishouFeifei: {
|
||||||
|
enabled: boolean;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
appKey: string;
|
appKey: string;
|
||||||
appSecret: string;
|
appSecret: string;
|
||||||
|
|||||||
Vendored
+2
@@ -47,6 +47,8 @@ declare module 'vue' {
|
|||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export * from './notifications'
|
|||||||
export * from './scheduled-jobs'
|
export * from './scheduled-jobs'
|
||||||
export * from './ninetyone'
|
export * from './ninetyone'
|
||||||
export * from './kuaishou-eticket'
|
export * from './kuaishou-eticket'
|
||||||
|
export * from './kuaishou-feifei'
|
||||||
export * from './cloudtentacles'
|
export * from './cloudtentacles'
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { apiGet, apiPost } from '@/lib/http'
|
||||||
|
import type {
|
||||||
|
AdminKuaishouFeifeiConfig,
|
||||||
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
|
AdminKuaishouFeifeiMatchResult,
|
||||||
|
} from '@/types/admin'
|
||||||
|
|
||||||
|
export function fetchAdminKuaishouFeifeiConfig() {
|
||||||
|
return apiGet<AdminKuaishouFeifeiConfigResponse>(
|
||||||
|
'/api/v1/admin/platform-config/kuaishou-feifei',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveAdminKuaishouFeifeiConfig(payload: AdminKuaishouFeifeiConfig) {
|
||||||
|
return apiPost<AdminKuaishouFeifeiConfigResponse>(
|
||||||
|
'/api/v1/admin/platform-config/kuaishou-feifei',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchAdminKuaishouFeifeiProduct(productName: string) {
|
||||||
|
return apiPost<AdminKuaishouFeifeiMatchResult>(
|
||||||
|
'/api/v1/admin/platform-config/kuaishou-feifei/match',
|
||||||
|
{ productName },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -50,6 +50,12 @@ export type {
|
|||||||
AdminKuaishouEticketShopInfoResult,
|
AdminKuaishouEticketShopInfoResult,
|
||||||
AdminKuaishouEticketDetailResult,
|
AdminKuaishouEticketDetailResult,
|
||||||
AdminKuaishouEticketConsumeResult,
|
AdminKuaishouEticketConsumeResult,
|
||||||
|
AdminKuaishouFeifeiProductRule,
|
||||||
|
AdminKuaishouFeifeiConfig,
|
||||||
|
AdminKuaishouFeifeiEffectiveConfig,
|
||||||
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
|
AdminKuaishouFeifeiProductMatch,
|
||||||
|
AdminKuaishouFeifeiMatchResult,
|
||||||
AdminCloudtentaclesSourceItem,
|
AdminCloudtentaclesSourceItem,
|
||||||
AdminCloudtentaclesSourcesConfig,
|
AdminCloudtentaclesSourcesConfig,
|
||||||
AdminCloudtentaclesSourceConfigResponse,
|
AdminCloudtentaclesSourceConfigResponse,
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ export type {
|
|||||||
AdminKuaishouEticketConsumeResult,
|
AdminKuaishouEticketConsumeResult,
|
||||||
} from './kuaishou-eticket'
|
} from './kuaishou-eticket'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AdminKuaishouFeifeiProductRule,
|
||||||
|
AdminKuaishouFeifeiConfig,
|
||||||
|
AdminKuaishouFeifeiEffectiveConfig,
|
||||||
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
|
AdminKuaishouFeifeiProductMatch,
|
||||||
|
AdminKuaishouFeifeiMatchResult,
|
||||||
|
} from './kuaishou-feifei'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AdminCloudtentaclesSourceItem,
|
AdminCloudtentaclesSourceItem,
|
||||||
AdminCloudtentaclesSourcesConfig,
|
AdminCloudtentaclesSourcesConfig,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
export interface AdminKuaishouFeifeiProductRule {
|
||||||
|
id: string
|
||||||
|
enabled: boolean
|
||||||
|
productName: string
|
||||||
|
normalizedProductName: string
|
||||||
|
productCode: string
|
||||||
|
skuName: string
|
||||||
|
notes: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouFeifeiConfig {
|
||||||
|
enabled: boolean
|
||||||
|
baseUrl: string
|
||||||
|
appKey: string
|
||||||
|
appSecret: string
|
||||||
|
timeoutMs: number
|
||||||
|
notifyUrl: string
|
||||||
|
productRules: AdminKuaishouFeifeiProductRule[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouFeifeiEffectiveConfig {
|
||||||
|
enabled: boolean
|
||||||
|
baseUrl: string
|
||||||
|
timeoutMs: number
|
||||||
|
notifyUrl: string
|
||||||
|
hasAppKey: boolean
|
||||||
|
hasAppSecret: boolean
|
||||||
|
productRuleCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouFeifeiConfigResponse {
|
||||||
|
filePath: string
|
||||||
|
source: AdminKuaishouFeifeiConfig
|
||||||
|
effective: AdminKuaishouFeifeiEffectiveConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouFeifeiProductMatch {
|
||||||
|
matchMode: string
|
||||||
|
productName: string
|
||||||
|
normalizedProductName: string
|
||||||
|
productCode: string
|
||||||
|
skuName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouFeifeiMatchResult {
|
||||||
|
productName: string
|
||||||
|
normalizedProductName: string
|
||||||
|
matched: boolean
|
||||||
|
match: AdminKuaishouFeifeiProductMatch | null
|
||||||
|
}
|
||||||
@@ -1,15 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import AdminKuaishouCloudFulfillmentView from './kuaishou-cloud/AdminKuaishouCloudFulfillmentView.vue'
|
import AdminKuaishouCloudFulfillmentView from './kuaishou-cloud/AdminKuaishouCloudFulfillmentView.vue'
|
||||||
|
import AdminKuaishouFeifeiFulfillmentView from './kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue'
|
||||||
|
|
||||||
|
const activeFulfillmentTab = ref('kuaishou-feifei')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="hub-panel">
|
<section class="hub-panel">
|
||||||
<AdminPageHeader
|
<AdminPageHeader
|
||||||
title="履约配置中心"
|
title="履约配置中心"
|
||||||
description="维护 91 卡券快手商品到 Cloud 履约资源的映射规则。"
|
description="维护 91 卡券快手商品到 Cloud 与 kuaishou-feifei 的履约映射规则。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminKuaishouCloudFulfillmentView />
|
<el-tabs v-model="activeFulfillmentTab" class="fulfillment-tabs">
|
||||||
|
<el-tab-pane label="kuaishou-feifei" name="kuaishou-feifei">
|
||||||
|
<AdminKuaishouFeifeiFulfillmentView />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="kuaishou-cloud" name="kuaishou-cloud">
|
||||||
|
<AdminKuaishouCloudFulfillmentView />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -18,4 +30,8 @@ import AdminKuaishouCloudFulfillmentView from './kuaishou-cloud/AdminKuaishouClo
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fulfillment-tabs {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+432
@@ -0,0 +1,432 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { Check, Delete, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
|
import {
|
||||||
|
fetchAdminKuaishouFeifeiConfig,
|
||||||
|
matchAdminKuaishouFeifeiProduct,
|
||||||
|
saveAdminKuaishouFeifeiConfig,
|
||||||
|
} from '@/services/admin'
|
||||||
|
import type {
|
||||||
|
AdminKuaishouFeifeiConfig,
|
||||||
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
|
AdminKuaishouFeifeiEffectiveConfig,
|
||||||
|
AdminKuaishouFeifeiMatchResult,
|
||||||
|
AdminKuaishouFeifeiProductRule,
|
||||||
|
} from '@/types/admin'
|
||||||
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
|
|
||||||
|
type EditableRule = AdminKuaishouFeifeiProductRule
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const saving = ref(false)
|
||||||
|
const matching = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const filePath = ref('')
|
||||||
|
const matchInput = ref('')
|
||||||
|
const matchResult = ref<AdminKuaishouFeifeiMatchResult | null>(null)
|
||||||
|
const effective = ref<AdminKuaishouFeifeiEffectiveConfig>({
|
||||||
|
enabled: true,
|
||||||
|
baseUrl: '',
|
||||||
|
timeoutMs: 10000,
|
||||||
|
notifyUrl: '',
|
||||||
|
hasAppKey: false,
|
||||||
|
hasAppSecret: false,
|
||||||
|
productRuleCount: 0,
|
||||||
|
})
|
||||||
|
const form = reactive<AdminKuaishouFeifeiConfig>({
|
||||||
|
enabled: true,
|
||||||
|
baseUrl: '',
|
||||||
|
appKey: '',
|
||||||
|
appSecret: '',
|
||||||
|
timeoutMs: 10000,
|
||||||
|
notifyUrl: '',
|
||||||
|
productRules: [],
|
||||||
|
})
|
||||||
|
const rules = ref<EditableRule[]>([])
|
||||||
|
|
||||||
|
const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length)
|
||||||
|
const credentialReady = computed(() => Boolean(form.appKey.trim() && form.appSecret.trim()))
|
||||||
|
const effectiveReady = computed(() => effective.value.enabled && effective.value.hasAppKey && effective.value.hasAppSecret)
|
||||||
|
|
||||||
|
onMounted(loadConfig)
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
if (!hasAdminRole('admin')) {
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||||
|
hydrateConfig(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 配置失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
saving.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
|
||||||
|
hydrateConfig(response.data)
|
||||||
|
showSuccess(`kuaishou-feifei 配置已保存,启用规则 ${enabledRuleCount.value} 条`)
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 配置失败'
|
||||||
|
showError(errorMessage.value)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMatchTest() {
|
||||||
|
const productName = matchInput.value.trim()
|
||||||
|
if (!productName) {
|
||||||
|
showError('请输入 91 商品名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matching.value = true
|
||||||
|
matchResult.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await matchAdminKuaishouFeifeiProduct(productName)
|
||||||
|
matchResult.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : '匹配 kuaishou-feifei 商品失败')
|
||||||
|
} finally {
|
||||||
|
matching.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
||||||
|
filePath.value = data.filePath || ''
|
||||||
|
effective.value = data.effective
|
||||||
|
form.enabled = data.source.enabled !== false
|
||||||
|
form.baseUrl = data.source.baseUrl || ''
|
||||||
|
form.appKey = data.source.appKey || ''
|
||||||
|
form.appSecret = data.source.appSecret || ''
|
||||||
|
form.timeoutMs = Number(data.source.timeoutMs || 10000) || 10000
|
||||||
|
form.notifyUrl = data.source.notifyUrl || ''
|
||||||
|
rules.value = Array.isArray(data.source.productRules)
|
||||||
|
? data.source.productRules.map((rule) => normalizeEditableRule(rule))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayload(): AdminKuaishouFeifeiConfig {
|
||||||
|
return {
|
||||||
|
enabled: form.enabled,
|
||||||
|
baseUrl: form.baseUrl.trim(),
|
||||||
|
appKey: form.appKey.trim(),
|
||||||
|
appSecret: form.appSecret.trim(),
|
||||||
|
timeoutMs: Number(form.timeoutMs || 10000) || 10000,
|
||||||
|
notifyUrl: form.notifyUrl.trim(),
|
||||||
|
productRules: rules.value.map((rule) => normalizeEditableRule(rule)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRule() {
|
||||||
|
rules.value.unshift(
|
||||||
|
normalizeEditableRule({
|
||||||
|
id: createRuleId(),
|
||||||
|
enabled: true,
|
||||||
|
productName: matchInput.value.trim(),
|
||||||
|
normalizedProductName: '',
|
||||||
|
productCode: '',
|
||||||
|
skuName: matchInput.value.trim(),
|
||||||
|
notes: '',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRule(index: number) {
|
||||||
|
rules.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEditableRule(rule: Partial<AdminKuaishouFeifeiProductRule>): EditableRule {
|
||||||
|
const productName = String(rule.productName || '').trim()
|
||||||
|
const productCode = String(rule.productCode || '').trim()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(rule.id || createRuleId()).trim(),
|
||||||
|
enabled: rule.enabled !== false,
|
||||||
|
productName,
|
||||||
|
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||||
|
productCode,
|
||||||
|
skuName: String(rule.skuName || productName || productCode).trim(),
|
||||||
|
notes: String(rule.notes || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRuleId() {
|
||||||
|
return `feifei-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-loading="loading" class="feifei-panel">
|
||||||
|
<el-alert
|
||||||
|
v-if="errorMessage"
|
||||||
|
type="error"
|
||||||
|
:title="errorMessage"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-item">
|
||||||
|
<span>运行状态</span>
|
||||||
|
<strong>{{ effectiveReady ? '已就绪' : '待配置' }}</strong>
|
||||||
|
<p>{{ form.enabled ? '启用中' : '已停用' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span>商品规则</span>
|
||||||
|
<strong>{{ enabledRuleCount }}</strong>
|
||||||
|
<p>共 {{ rules.length }} 条</p>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span>凭据</span>
|
||||||
|
<strong>{{ credentialReady ? '已填写' : '缺失' }}</strong>
|
||||||
|
<p>生效配置 {{ effective.hasAppKey && effective.hasAppSecret ? '可用' : '待补全' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="config-section">
|
||||||
|
<div class="section-head">
|
||||||
|
<div>
|
||||||
|
<h3>平台连接</h3>
|
||||||
|
<p>{{ filePath || 'data/kuaishou-feifei-config.json' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="section-actions">
|
||||||
|
<el-button :icon="RefreshRight" @click="loadConfig">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Check" :loading="saving" @click="saveConfig">
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form label-position="top" class="config-form">
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.enabled"
|
||||||
|
inline-prompt
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="停用"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Base URL">
|
||||||
|
<el-input v-model="form.baseUrl" placeholder="http://skin-exchange.yiquyou.icu" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="App Key">
|
||||||
|
<el-input v-model="form.appKey" placeholder="kuaishou-feifei app key" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="App Secret">
|
||||||
|
<el-input
|
||||||
|
v-model="form.appSecret"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="kuaishou-feifei app secret"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="超时毫秒">
|
||||||
|
<el-input-number v-model="form.timeoutMs" :min="1000" :step="1000" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="通知地址">
|
||||||
|
<el-input v-model="form.notifyUrl" placeholder="可选,feifei 回调通知地址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="config-section">
|
||||||
|
<div class="section-head">
|
||||||
|
<div>
|
||||||
|
<h3>商品规则</h3>
|
||||||
|
<p>91 商品名命中后会创建 kuaishou-feifei 履约任务。</p>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" plain :icon="Plus" @click="addRule">新增规则</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="match-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="matchInput"
|
||||||
|
clearable
|
||||||
|
placeholder="输入 91 商品名,例如 套装-Alan Walker"
|
||||||
|
@keyup.enter="runMatchTest"
|
||||||
|
/>
|
||||||
|
<el-button :icon="Search" :loading="matching" @click="runMatchTest">匹配测试</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="matchResult" class="match-result" :class="{ 'is-matched': matchResult.matched }">
|
||||||
|
<strong>{{ matchResult.matched ? '已命中' : '未命中' }}</strong>
|
||||||
|
<span v-if="matchResult.match">
|
||||||
|
{{ matchResult.match.productName }} -> {{ matchResult.match.productCode }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ matchResult.normalizedProductName || matchResult.productName }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-if="rules.length === 0" description="暂无 kuaishou-feifei 商品规则" />
|
||||||
|
|
||||||
|
<div v-else class="rule-list">
|
||||||
|
<article v-for="(rule, index) in rules" :key="rule.id" class="rule-item">
|
||||||
|
<div class="rule-head">
|
||||||
|
<el-switch
|
||||||
|
v-model="rule.enabled"
|
||||||
|
inline-prompt
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="停用"
|
||||||
|
/>
|
||||||
|
<el-button type="danger" plain :icon="Delete" @click="removeRule(index)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form label-position="top" class="rule-form">
|
||||||
|
<el-form-item label="91 商品名">
|
||||||
|
<el-input v-model="rule.productName" placeholder="套装-Alan Walker" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="feifei 商品编码">
|
||||||
|
<el-input v-model="rule.productCode" placeholder="product_code" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="展示名称">
|
||||||
|
<el-input v-model="rule.skuName" placeholder="后台展示名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="rule.notes" placeholder="可选" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.feifei-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item,
|
||||||
|
.config-section,
|
||||||
|
.rule-item {
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item span,
|
||||||
|
.summary-item p,
|
||||||
|
.section-head p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item strong {
|
||||||
|
display: block;
|
||||||
|
margin: 6px 0;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item p,
|
||||||
|
.section-head p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-actions,
|
||||||
|
.match-bar,
|
||||||
|
.rule-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form,
|
||||||
|
.rule-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-bar {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-result {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-result.is-matched {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
background: var(--el-color-success-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-head {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.summary-grid,
|
||||||
|
.config-form,
|
||||||
|
.rule-form {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head,
|
||||||
|
.match-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user