配置改为数据库存储
将运行时 JSON 配置迁入 app_config_entries,启动时从文件导入并删除源文件。
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import {
|
||||
listAppConfigEntries,
|
||||
upsertAppConfigEntry,
|
||||
} from '../../repositories/app-config-repo.js'
|
||||
import { readJsonFile } from '../../utils/json-file-store.js'
|
||||
|
||||
type NormalizeJsonValue<T> = (value: unknown) => T
|
||||
|
||||
const configCache = new Map<string, unknown>()
|
||||
let databaseCacheLoaded = false
|
||||
|
||||
export function hasAppConfigEntry(configKey: string, legacyFilePath = '') {
|
||||
const key = normalizeConfigKey(configKey)
|
||||
if (configCache.has(key)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return Boolean(legacyFilePath) && fs.existsSync(legacyFilePath)
|
||||
}
|
||||
|
||||
export function readAppConfigEntry<T>({
|
||||
configKey,
|
||||
legacyFilePath,
|
||||
fallback,
|
||||
normalize,
|
||||
}: {
|
||||
configKey: string
|
||||
legacyFilePath?: string | undefined
|
||||
fallback: T | (() => T)
|
||||
normalize: NormalizeJsonValue<T>
|
||||
}): T {
|
||||
const key = normalizeConfigKey(configKey)
|
||||
if (configCache.has(key)) {
|
||||
return normalize(cloneJsonValue(configCache.get(key)))
|
||||
}
|
||||
|
||||
if (!databaseCacheLoaded && legacyFilePath) {
|
||||
return readJsonFile(legacyFilePath, fallback, normalize)
|
||||
}
|
||||
|
||||
return normalize(resolveFallback(fallback))
|
||||
}
|
||||
|
||||
export async function saveAppConfigEntry<T>({
|
||||
configKey,
|
||||
value,
|
||||
legacyFilePath,
|
||||
normalize,
|
||||
}: {
|
||||
configKey: string
|
||||
value: unknown
|
||||
legacyFilePath?: string | undefined
|
||||
normalize: NormalizeJsonValue<T>
|
||||
}): Promise<T> {
|
||||
const key = normalizeConfigKey(configKey)
|
||||
const normalized = normalize(value)
|
||||
|
||||
await upsertAppConfigEntry({
|
||||
configKey: key,
|
||||
configJson: normalized,
|
||||
sourceFilePath: legacyFilePath,
|
||||
migratedFromFile: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
|
||||
configCache.set(key, cloneJsonValue(normalized))
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
export async function loadAppConfigCacheFromDatabase() {
|
||||
const rows = await listAppConfigEntries()
|
||||
configCache.clear()
|
||||
|
||||
for (const row of rows) {
|
||||
const key = normalizeConfigKey(row.config_key)
|
||||
if (!key) {
|
||||
continue
|
||||
}
|
||||
|
||||
configCache.set(key, cloneJsonValue(row.config_json))
|
||||
}
|
||||
|
||||
databaseCacheLoaded = true
|
||||
}
|
||||
|
||||
export function setAppConfigCacheEntry(configKey: string, value: unknown) {
|
||||
const key = normalizeConfigKey(configKey)
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
configCache.set(key, cloneJsonValue(value))
|
||||
}
|
||||
|
||||
function normalizeConfigKey(value: unknown) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function resolveFallback<T>(fallback: T | (() => T)): T {
|
||||
return typeof fallback === 'function' ? (fallback as () => T)() : fallback
|
||||
}
|
||||
|
||||
function cloneJsonValue<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value ?? null)) as T
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||
import { upsertAppConfigEntry } from '../../repositories/app-config-repo.js'
|
||||
import { logInfo, logWarn } from '../../utils/logger.js'
|
||||
import { normalizeFulfillmentRoutingConfig } from '../fulfillment/routing-config-service.js'
|
||||
import { normalizeNotificationConfig } from '../notification/config-service.js'
|
||||
import { normalizeCloudtentaclesOverrideRuleConfig } from '../order/cloudtentacles-override-rule-service.js'
|
||||
import { normalizeCloudtentaclesSourcesConfig } from '../platforms/cloudtentacles/source-config-service.js'
|
||||
import { normalizeSessionStatesFile } from '../platforms/cloudtentacles/session-state-service.js'
|
||||
import { normalizeKuaishouFeifeiSourceConfig } from '../platforms/kuaishou-feifei/source-config-service.js'
|
||||
import { normalizeKuaishouIndustrySourceConfig } from '../platforms/kuaishou-industry/source-config-service.js'
|
||||
import { normalizeScheduledJobsConfig } from '../scheduler/config-service.js'
|
||||
import { loadAppConfigCacheFromDatabase } from './app-config-store.js'
|
||||
|
||||
type JsonConfigMigrationItem = {
|
||||
configKey: string
|
||||
fileName: string
|
||||
normalize: (value: unknown) => unknown
|
||||
}
|
||||
|
||||
const DATA_DIR = path.join(PROJECT_ROOT, 'data')
|
||||
|
||||
const JSON_CONFIG_MIGRATION_ITEMS: JsonConfigMigrationItem[] = [
|
||||
{
|
||||
configKey: APP_CONFIG_KEYS.notification,
|
||||
fileName: 'notification-config.json',
|
||||
normalize: normalizeNotificationConfig,
|
||||
},
|
||||
{
|
||||
configKey: APP_CONFIG_KEYS.scheduledJobs,
|
||||
fileName: 'scheduled-jobs.json',
|
||||
normalize: normalizeScheduledJobsConfig,
|
||||
},
|
||||
{
|
||||
configKey: APP_CONFIG_KEYS.fulfillmentRouting,
|
||||
fileName: 'fulfillment-routing-config.json',
|
||||
normalize: normalizeFulfillmentRoutingConfig,
|
||||
},
|
||||
{
|
||||
configKey: APP_CONFIG_KEYS.cloudtentaclesSources,
|
||||
fileName: 'cloudtentacles-sources.json',
|
||||
normalize: normalizeCloudtentaclesSourcesConfig,
|
||||
},
|
||||
{
|
||||
configKey: APP_CONFIG_KEYS.cloudtentaclesSession,
|
||||
fileName: 'cloudtentacles-session.json',
|
||||
normalize: normalizeSessionStatesFile,
|
||||
},
|
||||
{
|
||||
configKey: APP_CONFIG_KEYS.cloudtentaclesOverrideRules,
|
||||
fileName: 'cloudtentacles-override-rules.json',
|
||||
normalize: normalizeCloudtentaclesOverrideRuleConfig,
|
||||
},
|
||||
{
|
||||
configKey: APP_CONFIG_KEYS.kuaishouFeifei,
|
||||
fileName: 'kuaishou-feifei-config.json',
|
||||
normalize: normalizeKuaishouFeifeiSourceConfig,
|
||||
},
|
||||
{
|
||||
configKey: APP_CONFIG_KEYS.kuaishouIndustrySource,
|
||||
fileName: 'kuaishou-industry-source.json',
|
||||
normalize: normalizeKuaishouIndustrySourceConfig,
|
||||
},
|
||||
]
|
||||
|
||||
export async function migrateJsonConfigFilesToDatabase() {
|
||||
const migrated: Array<{ configKey: string, filePath: string }> = []
|
||||
const skipped: Array<{ configKey: string, filePath: string, reason: string }> = []
|
||||
|
||||
for (const item of JSON_CONFIG_MIGRATION_ITEMS) {
|
||||
const filePath = path.join(DATA_DIR, item.fileName)
|
||||
if (!fs.existsSync(filePath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let normalized: unknown
|
||||
try {
|
||||
const rawText = fs.readFileSync(filePath, 'utf8')
|
||||
normalized = item.normalize(JSON.parse(rawText))
|
||||
} catch (error) {
|
||||
skipped.push({
|
||||
configKey: item.configKey,
|
||||
filePath,
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
logWarn('[config/migration]', 'JSON 配置解析失败,已保留原文件', {
|
||||
configKey: item.configKey,
|
||||
filePath,
|
||||
error,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
await upsertAppConfigEntry({
|
||||
configKey: item.configKey,
|
||||
configJson: normalized,
|
||||
sourceFilePath: filePath,
|
||||
migratedFromFile: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
|
||||
try {
|
||||
fs.unlinkSync(filePath)
|
||||
migrated.push({
|
||||
configKey: item.configKey,
|
||||
filePath,
|
||||
})
|
||||
} catch (error) {
|
||||
skipped.push({
|
||||
configKey: item.configKey,
|
||||
filePath,
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
logWarn('[config/migration]', 'JSON 配置已入库,但删除原文件失败', {
|
||||
configKey: item.configKey,
|
||||
filePath,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await loadAppConfigCacheFromDatabase()
|
||||
|
||||
if (migrated.length > 0 || skipped.length > 0) {
|
||||
logInfo('[config/migration]', 'JSON 配置文件迁移完成', {
|
||||
migratedCount: migrated.length,
|
||||
skippedCount: skipped.length,
|
||||
migrated,
|
||||
skipped,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
migrated,
|
||||
skipped,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user