- bootstrap CORE_PROFILES + routing ROUTABLE/DEFAULT_PRIORITY 接入 affiliate_dash - planner 上下文 affiliateDash 块 + resolveDynamicAffiliateDashProfile - product-resolution: 91 productNo→sku 映射匹配(skuMapping) + snapshot.affiliateDash - prepare 前余额预检(getAffiliateDashWallet) - AFFILIATE_DASH_SKU_MAPPING_JSON env 注入 - admin 后端接口(GET/POST 配置、match、products、wallet)+ 前端 Tabs 面板 - 联调:模拟 91 进单命中 affiliate_dash 路由, 未命中回退;前后端 typecheck + 212 测试通过
346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
import path from 'node:path'
|
|
import process from 'node:process'
|
|
|
|
import type {
|
|
AdminDefaultUser,
|
|
KuaishouFeifeiProductRule,
|
|
RuntimeConfig,
|
|
} from '../types/runtime-config.js'
|
|
import { parseBoolean, parseInteger, parseJsonArray } from './runtime-env.js'
|
|
import { deepMerge } from './runtime-merge.js'
|
|
|
|
type RuntimeConfigPath = readonly [string, ...string[]]
|
|
type RuntimeConfigValue =
|
|
| string
|
|
| number
|
|
| boolean
|
|
| AdminDefaultUser[]
|
|
| KuaishouFeifeiProductRule[]
|
|
| string[]
|
|
| Record<string, string>
|
|
type RuntimeEnv = Record<string, string | undefined>
|
|
|
|
type EnvOverride = {
|
|
env: string
|
|
path: RuntimeConfigPath
|
|
read: (rawValue: unknown) => RuntimeConfigValue | null
|
|
}
|
|
|
|
export const ENV_OVERRIDES: readonly EnvOverride[] = [
|
|
integerEnv('PORT', ['server', 'port']),
|
|
stringEnv('DATA_ROOT', ['data', 'root'], path.resolve),
|
|
stringEnv('LOG_LEVEL', ['logging', 'level']),
|
|
stringEnv('LOG_INTEGRATION_LEVEL', ['logging', 'integrationLevel']),
|
|
integerEnv('LOG_RETENTION_DAYS', ['logging', 'retentionDays']),
|
|
stringEnv('DATABASE_URL', ['database', 'url']),
|
|
booleanEnv('DATABASE_SSL', ['database', 'ssl']),
|
|
integerEnv('DATABASE_MAX_CONNECTIONS', ['database', 'maxConnections']),
|
|
stringEnv('STORAGE_ENDPOINT', ['storage', 'endpoint']),
|
|
stringEnv('STORAGE_BUCKET', ['storage', 'bucket']),
|
|
stringEnv('STORAGE_ACCESS_KEY_ID', ['storage', 'accessKeyId']),
|
|
stringEnv('STORAGE_SECRET_ACCESS_KEY', ['storage', 'secretAccessKey']),
|
|
stringEnv('STORAGE_REGION', ['storage', 'region']),
|
|
integerEnv('STORAGE_MAX_UPLOAD_SIZE_MB', ['storage', 'maxUploadSizeMb']),
|
|
integerEnv('CLAIM_TOKEN_TTL_HOURS', ['orders', 'tokenTtlHours']),
|
|
stringEnv('ADMIN_SESSION_SECRET', ['admin', 'sessionSecret']),
|
|
integerEnv('ADMIN_SESSION_TTL_HOURS', ['admin', 'sessionTtlHours']),
|
|
adminUsersJsonEnv('ADMIN_DEFAULT_USERS_JSON', ['admin', 'defaultUsers']),
|
|
stringEnv('KAQUAN91_USER_ID', ['platforms', 'ninetyone', 'userId']),
|
|
stringEnv('KAQUAN91_SECRET', ['platforms', 'ninetyone', 'secret']),
|
|
stringEnv('KAQUAN91_VERSION', ['platforms', 'ninetyone', 'version']),
|
|
stringEnv('KAQUAN91_SHOP_ID', ['platforms', 'ninetyone', 'shopId']),
|
|
stringEnv('KAQUAN91_SHOP_NAME', ['platforms', 'ninetyone', 'shopName']),
|
|
integerEnv('KAQUAN91_TIMESTAMP_TOLERANCE_SECONDS', [
|
|
'platforms',
|
|
'ninetyone',
|
|
'timestampToleranceSeconds',
|
|
]),
|
|
stringEnv('KAQUAN91_CARDS_ENCODING', ['platforms', 'ninetyone', 'cardsEncoding']),
|
|
stringEnv('CLOUDTENTACLES_BASE_URL', ['platforms', 'cloudtentacles', 'baseUrl']),
|
|
integerEnv('CLOUDTENTACLES_TIMEOUT_MS', ['platforms', 'cloudtentacles', 'timeoutMs']),
|
|
stringEnv('CLOUDTENTACLES_SEND_SMS_PATH', ['platforms', 'cloudtentacles', 'sendSmsPath']),
|
|
stringEnv('CLOUDTENTACLES_LOGIN_PATH', ['platforms', 'cloudtentacles', 'loginPath']),
|
|
stringEnv('CLOUDTENTACLES_USER_INFO_PATH', ['platforms', 'cloudtentacles', 'userInfoPath']),
|
|
stringEnv('CLOUDTENTACLES_ASSET_PATH', ['platforms', 'cloudtentacles', 'assetPath']),
|
|
stringEnv('CLOUDTENTACLES_PERMISSION_PATH', ['platforms', 'cloudtentacles', 'permissionPath']),
|
|
stringEnv('CLOUDTENTACLES_CATEGORIES_PATH', ['platforms', 'cloudtentacles', 'categoriesPath']),
|
|
stringEnv('CLOUDTENTACLES_SKU_LIST_PATH', ['platforms', 'cloudtentacles', 'skuListPath']),
|
|
stringEnv('CLOUDTENTACLES_SKU_BUY_PATH', ['platforms', 'cloudtentacles', 'skuBuyPath']),
|
|
stringEnv('CLOUDTENTACLES_KNAPSACK_PATH', ['platforms', 'cloudtentacles', 'knapsackPath']),
|
|
stringEnv('CLOUDTENTACLES_VN_LIST_PATH', ['platforms', 'cloudtentacles', 'vnListPath']),
|
|
stringEnv('CLOUDTENTACLES_VN_APPOINT_PATH', ['platforms', 'cloudtentacles', 'vnAppointPath']),
|
|
stringEnv('CLOUDTENTACLES_VN_GENERATE_LOGIN_CODE_PATH', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'vnGenerateLoginCodePath',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_VN_VERIF_CODE_PATH', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'vnVerifCodePath',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_VN_VERIFY_LOGIN_CODE_PATH', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'vnVerifyLoginCodePath',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_VN_BIND_URL_PATH', ['platforms', 'cloudtentacles', 'vnBindUrlPath']),
|
|
stringEnv('CLOUDTENTACLES_VN_BIND_INFO_PATH', ['platforms', 'cloudtentacles', 'vnBindInfoPath']),
|
|
stringEnv('CLOUDTENTACLES_VN_BACK_PATH', ['platforms', 'cloudtentacles', 'vnBackPath']),
|
|
integerEnv('CLOUDTENTACLES_BIND_URL_TTL_SECONDS', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlTtlSeconds',
|
|
]),
|
|
integerEnv('CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeIntervalSeconds',
|
|
]),
|
|
integerEnv('CLOUDTENTACLES_BIND_URL_PROBE_TIMEOUT_MS', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeTimeoutMs',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_USER_AGENT', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeUserAgent',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_ENDPOINT', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeEndpoint',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_CHART_ID', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeChartId',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_SUB_CHART_ID', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeSubChartId',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_IDE_TOKEN', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeIdeToken',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_ACTIVITY_URL', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeActivityUrl',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_REFERER', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeReferer',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_BIND_URL_PROBE_EXTRA_COOKIE', [
|
|
'platforms',
|
|
'cloudtentacles',
|
|
'bindUrlProbeExtraCookie',
|
|
]),
|
|
stringEnv('CLOUDTENTACLES_PUBLIC_KEY_PEM', ['platforms', 'cloudtentacles', 'publicKeyPem']),
|
|
stringEnv('CLOUDTENTACLES_CLIENT_SOURCE', ['platforms', 'cloudtentacles', 'clientSource']),
|
|
stringEnv('CLOUDTENTACLES_DEVICE_ID', ['platforms', 'cloudtentacles', 'deviceId']),
|
|
integerEnv('CLOUDTENTACLES_DEVICE_TYPE', ['platforms', 'cloudtentacles', 'deviceType']),
|
|
stringEnv('KUASHOU_INDUSTRY_APP_KEY', ['platforms', 'kuaishouIndustry', 'appKey']),
|
|
stringEnv('KUASHOU_INDUSTRY_APP_SECRET', ['platforms', 'kuaishouIndustry', 'appSecret']),
|
|
stringEnv('KUASHOU_INDUSTRY_SIGN_SECRET', ['platforms', 'kuaishouIndustry', 'signSecret']),
|
|
stringEnv('KUASHOU_INDUSTRY_MESSAGE_SECRET', ['platforms', 'kuaishouIndustry', 'messageSecret']),
|
|
stringEnv('KUASHOU_INDUSTRY_PROVIDER', ['platforms', 'kuaishouIndustry', 'provider']),
|
|
stringEnv('KUASHOU_INDUSTRY_SHOP_ID', ['platforms', 'kuaishouIndustry', 'shopId']),
|
|
stringEnv('KUASHOU_INDUSTRY_SHOP_NAME', ['platforms', 'kuaishouIndustry', 'shopName']),
|
|
stringEnv('KUASHOU_INDUSTRY_VERSION', ['platforms', 'kuaishouIndustry', 'version']),
|
|
stringEnv('KUAISHOU_FEIFEI_BASE_URL', ['platforms', 'kuaishouFeifei', 'baseUrl']),
|
|
stringEnv('KUAISHOU_FEIFEI_APP_KEY', ['platforms', 'kuaishouFeifei', 'appKey']),
|
|
stringEnv('KUAISHOU_FEIFEI_APP_SECRET', ['platforms', 'kuaishouFeifei', 'appSecret']),
|
|
stringEnv('WORKER_SMS_PROVIDER', ['worker', 'sms', 'provider']),
|
|
stringEnv('WORKER_SMS_ACCESS_KEY_ID', ['worker', 'sms', 'accessKeyId']),
|
|
stringEnv('WORKER_SMS_ACCESS_KEY_SECRET', ['worker', 'sms', 'accessKeySecret']),
|
|
stringEnv('WORKER_SMS_SIGN_NAME', ['worker', 'sms', 'signName']),
|
|
stringEnv('WORKER_SMS_TEMPLATE_CODE', ['worker', 'sms', 'templateCode']),
|
|
integerEnv('WORKER_SMS_CODE_TTL_SECONDS', ['worker', 'sms', 'codeTtlSeconds']),
|
|
integerEnv('WORKER_SMS_COOLDOWN_SECONDS', ['worker', 'sms', 'cooldownSeconds']),
|
|
integerEnv('WORKER_SMS_DAILY_LIMIT', ['worker', 'sms', 'dailyLimit']),
|
|
integerEnv('KUAISHOU_FEIFEI_TIMEOUT_MS', ['platforms', 'kuaishouFeifei', 'timeoutMs']),
|
|
stringEnv('KUAISHOU_FEIFEI_NOTIFY_URL', ['platforms', 'kuaishouFeifei', 'notifyUrl']),
|
|
kuaishouFeifeiProductRulesEnv('KUAISHOU_FEIFEI_PRODUCT_RULES_JSON', [
|
|
'platforms',
|
|
'kuaishouFeifei',
|
|
'productRules',
|
|
]),
|
|
stringEnv('AFFILIATE_DASH_BASE_URL', ['platforms', 'affiliateDash', 'baseUrl']),
|
|
stringEnv('AFFILIATE_DASH_APP_KEY', ['platforms', 'affiliateDash', 'appKey']),
|
|
stringEnv('AFFILIATE_DASH_APP_SECRET', ['platforms', 'affiliateDash', 'appSecret']),
|
|
stringEnv('AFFILIATE_DASH_CALLBACK_SECRET', ['platforms', 'affiliateDash', 'callbackSecret']),
|
|
integerEnv('AFFILIATE_DASH_TIMEOUT_MS', ['platforms', 'affiliateDash', 'timeoutMs']),
|
|
stringEnv('AFFILIATE_DASH_NOTIFY_URL', ['platforms', 'affiliateDash', 'notifyUrl']),
|
|
integerEnv('AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS', [
|
|
'platforms',
|
|
'affiliateDash',
|
|
'timestampToleranceSeconds',
|
|
]),
|
|
affiliateDashSkuMappingEnv('AFFILIATE_DASH_SKU_MAPPING_JSON', [
|
|
'platforms',
|
|
'affiliateDash',
|
|
'skuMapping',
|
|
]),
|
|
corsOriginsEnv('CORS_ALLOWED_ORIGINS', ['cors', 'allowedOrigins']),
|
|
]
|
|
|
|
export function applyEnvOverrides(
|
|
baseConfig: RuntimeConfig,
|
|
env: RuntimeEnv = process.env,
|
|
): RuntimeConfig {
|
|
const nextConfig = deepMerge(baseConfig, {})
|
|
|
|
applyClaimBaseUrlOverride(nextConfig, env)
|
|
|
|
for (const override of ENV_OVERRIDES) {
|
|
const value = override.read(env[override.env])
|
|
if (value !== null) {
|
|
setConfigValue(nextConfig, override.path, value)
|
|
}
|
|
}
|
|
|
|
return nextConfig
|
|
}
|
|
|
|
function applyClaimBaseUrlOverride(config: RuntimeConfig, env: RuntimeEnv): void {
|
|
const claimBaseUrl = parseString(env.CLAIM_BASE_URL)
|
|
if (claimBaseUrl !== null) {
|
|
config.orders.claimBaseUrl = claimBaseUrl
|
|
return
|
|
}
|
|
|
|
const appBaseUrl = parseString(env.APP_BASE_URL)
|
|
if (appBaseUrl !== null) {
|
|
config.orders.claimBaseUrl = `${appBaseUrl.replace(/\/+$/, '')}/#/claim`
|
|
}
|
|
}
|
|
|
|
function stringEnv(
|
|
env: string,
|
|
configPath: RuntimeConfigPath,
|
|
transform: (value: string) => string = (value) => value,
|
|
): EnvOverride {
|
|
return {
|
|
env,
|
|
path: configPath,
|
|
read(rawValue) {
|
|
const value = parseString(rawValue)
|
|
return value === null ? null : transform(value)
|
|
},
|
|
}
|
|
}
|
|
|
|
function integerEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
|
return {
|
|
env,
|
|
path: configPath,
|
|
read: parseInteger,
|
|
}
|
|
}
|
|
|
|
function booleanEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
|
return {
|
|
env,
|
|
path: configPath,
|
|
read: parseBoolean,
|
|
}
|
|
}
|
|
|
|
function adminUsersJsonEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
|
return {
|
|
env,
|
|
path: configPath,
|
|
read(rawValue) {
|
|
return parseJsonArray<AdminDefaultUser>(rawValue)
|
|
},
|
|
}
|
|
}
|
|
|
|
function kuaishouFeifeiProductRulesEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
|
return {
|
|
env,
|
|
path: configPath,
|
|
read(rawValue) {
|
|
const parsed = parseJsonArray<KuaishouFeifeiProductRule>(rawValue)
|
|
return parsed === null ? null : parsed
|
|
},
|
|
}
|
|
}
|
|
|
|
function affiliateDashSkuMappingEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
|
return {
|
|
env,
|
|
path: configPath,
|
|
read(rawValue) {
|
|
const text = String(rawValue ?? '').trim()
|
|
if (!text) {
|
|
return null
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(text)
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
return parsed as Record<string, string>
|
|
}
|
|
} catch {
|
|
// 忽略非法 JSON
|
|
}
|
|
return null
|
|
},
|
|
}
|
|
}
|
|
|
|
function corsOriginsEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
|
|
return {
|
|
env,
|
|
path: configPath,
|
|
read(rawValue) {
|
|
const value = parseString(rawValue)
|
|
if (value === null) {
|
|
return null
|
|
}
|
|
if (value.trim() === '*') {
|
|
return ['*']
|
|
}
|
|
return value
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
},
|
|
}
|
|
}
|
|
|
|
function parseString(rawValue: unknown): string | null {
|
|
const value = String(rawValue || '').trim()
|
|
return value ? value : null
|
|
}
|
|
|
|
function setConfigValue(
|
|
config: RuntimeConfig,
|
|
configPath: RuntimeConfigPath,
|
|
value: RuntimeConfigValue,
|
|
): void {
|
|
let current: Record<string, unknown> = config as unknown as Record<string, unknown>
|
|
|
|
for (const key of configPath.slice(0, -1)) {
|
|
const nextValue = current[key]
|
|
if (!nextValue || typeof nextValue !== 'object' || Array.isArray(nextValue)) {
|
|
current[key] = {}
|
|
}
|
|
current = current[key] as Record<string, unknown>
|
|
}
|
|
|
|
const lastKey = configPath[configPath.length - 1]
|
|
if (!lastKey) {
|
|
return
|
|
}
|
|
|
|
current[lastKey] = value
|
|
}
|