增加feifei履约配置界面
This commit is contained in:
@@ -58,6 +58,7 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
|
||||
version: "1",
|
||||
},
|
||||
kuaishouFeifei: {
|
||||
enabled: true,
|
||||
baseUrl: "http://skin-exchange.yiquyou.icu",
|
||||
appKey: "",
|
||||
appSecret: "",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Router } from "express";
|
||||
|
||||
import { requireAdminRoles } from "./session.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 ninetyoneRouter from "./platform-config/ninetyone.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", notificationsRouter);
|
||||
router.use("/platform-config", kuaishouEticketRouter);
|
||||
router.use("/platform-config", kuaishouFeifeiRouter);
|
||||
router.use("/platform-config", ninetyoneRouter);
|
||||
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 { createHttpError } from '../../../utils/http.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_PROFILE_KEY = 'kuaishou_feifei'
|
||||
@@ -8,12 +12,14 @@ export const KUAISHOU_FEIFEI_PROFILE_KEY = 'kuaishou_feifei'
|
||||
type KuaishouFeifeiRuntimeConfig = RuntimeConfig['platforms']['kuaishouFeifei']
|
||||
|
||||
export function getKuaishouFeifeiConfig(overrides: Partial<KuaishouFeifeiRuntimeConfig> = {}) {
|
||||
const config = {
|
||||
...(runtimeConfig.platforms?.kuaishouFeifei || {}),
|
||||
...overrides,
|
||||
}
|
||||
const runtimeValue = runtimeConfig.platforms?.kuaishouFeifei || {}
|
||||
const savedValue = hasKuaishouFeifeiConfigFile()
|
||||
? getKuaishouFeifeiSourceConfig()
|
||||
: null
|
||||
const config = mergeKuaishouFeifeiConfig(runtimeValue, savedValue, overrides)
|
||||
|
||||
return {
|
||||
enabled: config.enabled !== false,
|
||||
baseUrl: String(config.baseUrl || 'http://skin-exchange.yiquyou.icu').trim().replace(/\/+$/, ''),
|
||||
appKey: String(config.appKey || '').trim(),
|
||||
appSecret: String(config.appSecret || '').trim(),
|
||||
@@ -26,6 +32,13 @@ export function getKuaishouFeifeiConfig(overrides: Partial<KuaishouFeifeiRuntime
|
||||
export function assertKuaishouFeifeiConfig() {
|
||||
const config = getKuaishouFeifeiConfig()
|
||||
|
||||
if (config.enabled === false) {
|
||||
throw createHttpError('kuaishou-feifei 已停用', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_feifei_disabled',
|
||||
})
|
||||
}
|
||||
|
||||
if (!config.baseUrl) {
|
||||
throw createHttpError('kuaishou-feifei baseUrl 未配置', {
|
||||
statusCode: 500,
|
||||
@@ -42,3 +55,41 @@ export function assertKuaishouFeifeiConfig() {
|
||||
|
||||
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 { getKuaishouFeifeiConfig } from './config.js'
|
||||
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
||||
|
||||
export type KuaishouFeifeiProductMatch = {
|
||||
@@ -12,15 +12,19 @@ export type KuaishouFeifeiProductMatch = {
|
||||
|
||||
export function resolveKuaishouFeifeiProductByName(
|
||||
productName: unknown,
|
||||
options: {
|
||||
rules?: KuaishouFeifeiProductRule[]
|
||||
enabled?: boolean
|
||||
} = {},
|
||||
): KuaishouFeifeiProductMatch | null {
|
||||
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
|
||||
if (!normalizedProductName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rules = listKuaishouFeifeiProductRules()
|
||||
const rules = listKuaishouFeifeiProductRules(options)
|
||||
const matched = rules.find((rule) =>
|
||||
normalizeCloudtentaclesMatchName(rule.productName) === normalizedProductName,
|
||||
normalizeCloudtentaclesMatchName(rule.productName || rule.normalizedProductName) === normalizedProductName,
|
||||
)
|
||||
|
||||
if (!matched) {
|
||||
@@ -41,14 +45,25 @@ export function resolveKuaishouFeifeiProductByName(
|
||||
}
|
||||
}
|
||||
|
||||
function listKuaishouFeifeiProductRules(): KuaishouFeifeiProductRule[] {
|
||||
const rules = runtimeConfig.platforms?.kuaishouFeifei?.productRules
|
||||
function listKuaishouFeifeiProductRules(options: {
|
||||
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 : [])
|
||||
.map((rule) => ({
|
||||
id: String(rule.id || '').trim(),
|
||||
productName: String(rule.productName || '').trim(),
|
||||
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||
productCode: String(rule.productCode || '').trim(),
|
||||
skuName: String(rule.skuName || '').trim(),
|
||||
enabled: rule.enabled !== false,
|
||||
notes: String(rule.notes || '').trim(),
|
||||
}))
|
||||
.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 = {
|
||||
id?: string;
|
||||
productName: string;
|
||||
normalizedProductName?: string;
|
||||
productCode: string;
|
||||
skuName?: string;
|
||||
enabled?: boolean;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
export type RuntimeConfig = {
|
||||
@@ -98,6 +101,7 @@ export type RuntimeConfig = {
|
||||
version: string;
|
||||
};
|
||||
kuaishouFeifei: {
|
||||
enabled: boolean;
|
||||
baseUrl: string;
|
||||
appKey: string;
|
||||
appSecret: string;
|
||||
|
||||
Reference in New Issue
Block a user