配置改为数据库存储
将运行时 JSON 配置迁入 app_config_entries,启动时从文件导入并删除源文件。
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
export const APP_CONFIG_KEYS = {
|
||||||
|
notification: 'notification',
|
||||||
|
scheduledJobs: 'scheduled_jobs',
|
||||||
|
fulfillmentRouting: 'fulfillment_routing',
|
||||||
|
cloudtentaclesSources: 'cloudtentacles_sources',
|
||||||
|
cloudtentaclesSession: 'cloudtentacles_session',
|
||||||
|
cloudtentaclesOverrideRules: 'cloudtentacles_override_rules',
|
||||||
|
kuaishouFeifei: 'kuaishou_feifei',
|
||||||
|
kuaishouIndustrySource: 'kuaishou_industry_source',
|
||||||
|
} as const
|
||||||
@@ -36,6 +36,25 @@ COMMENT ON TABLE admin_users IS '后台管理用户';
|
|||||||
COMMENT ON TABLE admin_audit_logs IS '后台操作审计日志';
|
COMMENT ON TABLE admin_audit_logs IS '后台操作审计日志';
|
||||||
COMMENT ON COLUMN admin_audit_logs.payload_json IS '审计上下文快照,保留请求关键字段和变更内容';
|
COMMENT ON COLUMN admin_audit_logs.payload_json IS '审计上下文快照,保留请求关键字段和变更内容';
|
||||||
|
|
||||||
|
-- 运行时配置中心。
|
||||||
|
CREATE TABLE IF NOT EXISTS app_config_entries (
|
||||||
|
config_key TEXT PRIMARY KEY,
|
||||||
|
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
source_file_path TEXT NOT NULL DEFAULT '',
|
||||||
|
migrated_from_file BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_app_config_entries_updated_at
|
||||||
|
ON app_config_entries(updated_at DESC);
|
||||||
|
|
||||||
|
COMMENT ON TABLE app_config_entries IS '运行时配置中心,保存从 JSON 文件迁移来的平台和系统配置';
|
||||||
|
COMMENT ON COLUMN app_config_entries.config_key IS '配置唯一键';
|
||||||
|
COMMENT ON COLUMN app_config_entries.config_json IS '配置内容 JSON';
|
||||||
|
COMMENT ON COLUMN app_config_entries.source_file_path IS '首次迁移来源文件路径,便于排查历史来源';
|
||||||
|
COMMENT ON COLUMN app_config_entries.migrated_from_file IS '是否由旧 JSON 文件迁移产生';
|
||||||
|
|
||||||
-- 履约档案与履约资源要求。
|
-- 履约档案与履约资源要求。
|
||||||
CREATE TABLE IF NOT EXISTS fulfillment_profiles (
|
CREATE TABLE IF NOT EXISTS fulfillment_profiles (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { query } from '../db/client.js'
|
||||||
|
|
||||||
|
export type AppConfigEntryRow = {
|
||||||
|
config_key: string
|
||||||
|
config_json: unknown
|
||||||
|
source_file_path: string
|
||||||
|
migrated_from_file: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpsertAppConfigEntryInput = {
|
||||||
|
configKey: string
|
||||||
|
configJson: unknown
|
||||||
|
sourceFilePath?: string | undefined
|
||||||
|
migratedFromFile?: boolean
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAppConfigEntries(): Promise<AppConfigEntryRow[]> {
|
||||||
|
const result = await query<AppConfigEntryRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM app_config_entries
|
||||||
|
ORDER BY config_key ASC
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertAppConfigEntry(
|
||||||
|
input: UpsertAppConfigEntryInput,
|
||||||
|
): Promise<AppConfigEntryRow | null> {
|
||||||
|
const result = await query<AppConfigEntryRow>(
|
||||||
|
`
|
||||||
|
INSERT INTO app_config_entries (
|
||||||
|
config_key,
|
||||||
|
config_json,
|
||||||
|
source_file_path,
|
||||||
|
migrated_from_file,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES ($1, $2::jsonb, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT (config_key) DO UPDATE
|
||||||
|
SET
|
||||||
|
config_json = EXCLUDED.config_json,
|
||||||
|
source_file_path = CASE
|
||||||
|
WHEN EXCLUDED.source_file_path <> '' THEN EXCLUDED.source_file_path
|
||||||
|
ELSE app_config_entries.source_file_path
|
||||||
|
END,
|
||||||
|
migrated_from_file = app_config_entries.migrated_from_file OR EXCLUDED.migrated_from_file,
|
||||||
|
updated_at = EXCLUDED.updated_at
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
String(input.configKey || '').trim(),
|
||||||
|
JSON.stringify(input.configJson ?? {}),
|
||||||
|
String(input.sourceFilePath || '').trim(),
|
||||||
|
input.migratedFromFile === true,
|
||||||
|
input.timestamp,
|
||||||
|
input.timestamp,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
@@ -178,28 +178,28 @@ export function getAdminCloudtentaclesOverrideRules() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAdminCloudtentaclesOverrideRules(
|
export async function updateAdminCloudtentaclesOverrideRules(
|
||||||
payload: AdminCloudtentaclesOverrideRuleConfigInput = {}
|
payload: AdminCloudtentaclesOverrideRuleConfigInput = {}
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
filePath: getCloudtentaclesOverrideRuleFilePath(),
|
filePath: getCloudtentaclesOverrideRuleFilePath(),
|
||||||
...saveCloudtentaclesOverrideRuleConfig(payload),
|
...(await saveCloudtentaclesOverrideRuleConfig(payload)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAdminCloudtentaclesSourceConfig(
|
export async function updateAdminCloudtentaclesSourceConfig(
|
||||||
payload: AdminCloudtentaclesSourceConfigInput = {}
|
payload: AdminCloudtentaclesSourceConfigInput = {}
|
||||||
) {
|
) {
|
||||||
// Batch save: if payload.sources is an array, save the whole list
|
// 批量保存:payload.sources 是数组时替换整份账号列表。
|
||||||
if (Array.isArray(payload.sources)) {
|
if (Array.isArray(payload.sources)) {
|
||||||
const normalizedList = payload.sources.map((item) =>
|
const normalizedList = payload.sources.map((item) =>
|
||||||
normalizeAdminCloudtentaclesSourceConfigPayload(item)
|
normalizeAdminCloudtentaclesSourceConfigPayload(item)
|
||||||
);
|
);
|
||||||
const savedList = saveCloudtentaclesSourcesList({
|
const savedList = await saveCloudtentaclesSourcesList({
|
||||||
enabled: payload.enabled !== false,
|
enabled: payload.enabled !== false,
|
||||||
sources: normalizedList,
|
sources: normalizedList,
|
||||||
});
|
});
|
||||||
const sessions = pruneCloudtentaclesSessionStates(
|
const sessions = await pruneCloudtentaclesSessionStates(
|
||||||
normalizedList.map((item) => item.key)
|
normalizedList.map((item) => item.key)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -220,20 +220,20 @@ export function updateAdminCloudtentaclesSourceConfig(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single source save
|
// 单账号保存。
|
||||||
const sourceKey = String(payload.sourceKey || "").trim() || "default";
|
const sourceKey = String(payload.sourceKey || "").trim() || "default";
|
||||||
const current = getCloudtentaclesSourceByKey(sourceKey);
|
const current = getCloudtentaclesSourceByKey(sourceKey);
|
||||||
const normalized = normalizeAdminCloudtentaclesSourceConfigPayload({
|
const normalized = normalizeAdminCloudtentaclesSourceConfigPayload({
|
||||||
...payload,
|
...payload,
|
||||||
sourceKey,
|
sourceKey,
|
||||||
});
|
});
|
||||||
const saved = saveCloudtentaclesSourceByKey(sourceKey, normalized);
|
const saved = await saveCloudtentaclesSourceByKey(sourceKey, normalized);
|
||||||
const shouldClearSession = hasCloudtentaclesCredentialContextChanged(
|
const shouldClearSession = hasCloudtentaclesCredentialContextChanged(
|
||||||
current || undefined,
|
current || undefined,
|
||||||
saved
|
saved
|
||||||
);
|
);
|
||||||
const session = shouldClearSession
|
const session = shouldClearSession
|
||||||
? (clearCloudtentaclesSessionStateByKey(sourceKey),
|
? (await clearCloudtentaclesSessionStateByKey(sourceKey),
|
||||||
getCloudtentaclesSessionStateByKey(sourceKey))
|
getCloudtentaclesSessionStateByKey(sourceKey))
|
||||||
: getCloudtentaclesSessionStateByKey(sourceKey);
|
: getCloudtentaclesSessionStateByKey(sourceKey);
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ export function updateAdminCloudtentaclesSourceConfig(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteAdminCloudtentaclesSource(sourceKey: string) {
|
export async function deleteAdminCloudtentaclesSource(sourceKey: string) {
|
||||||
const key = String(sourceKey || "").trim();
|
const key = String(sourceKey || "").trim();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw createHttpError("cloudtentacles sourceKey 不能为空", {
|
throw createHttpError("cloudtentacles sourceKey 不能为空", {
|
||||||
@@ -253,8 +253,8 @@ export function deleteAdminCloudtentaclesSource(sourceKey: string) {
|
|||||||
errorCode: "cloudtentacles_source_key_required",
|
errorCode: "cloudtentacles_source_key_required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
deleteCloudtentaclesSessionStateByKey(key);
|
await deleteCloudtentaclesSessionStateByKey(key);
|
||||||
deleteCloudtentaclesSourceByKey(key);
|
await deleteCloudtentaclesSourceByKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendAdminCloudtentaclesSmsCode(
|
export async function sendAdminCloudtentaclesSmsCode(
|
||||||
@@ -283,7 +283,7 @@ export async function testAdminCloudtentaclesLogin(
|
|||||||
...credentialContext,
|
...credentialContext,
|
||||||
code: String(payload.code || "").trim(),
|
code: String(payload.code || "").trim(),
|
||||||
});
|
});
|
||||||
const savedSession = saveCloudtentaclesSessionStateByKey(
|
const savedSession = await saveCloudtentaclesSessionStateByKey(
|
||||||
sourceKey,
|
sourceKey,
|
||||||
buildAdminCloudtentaclesPersistedSessionPayload(session, {
|
buildAdminCloudtentaclesPersistedSessionPayload(session, {
|
||||||
deviceId: credentialContext.deviceId,
|
deviceId: credentialContext.deviceId,
|
||||||
@@ -305,7 +305,7 @@ export async function validateAdminCloudtentaclesSession(
|
|||||||
persistedSession,
|
persistedSession,
|
||||||
});
|
});
|
||||||
const session = await validateCloudtentaclesSession(sessionContext);
|
const session = await validateCloudtentaclesSession(sessionContext);
|
||||||
const savedSession = saveCloudtentaclesSessionStateByKey(
|
const savedSession = await saveCloudtentaclesSessionStateByKey(
|
||||||
sourceKey,
|
sourceKey,
|
||||||
buildAdminCloudtentaclesPersistedSessionPayload(session, {
|
buildAdminCloudtentaclesPersistedSessionPayload(session, {
|
||||||
username: pickFirstNonEmpty([
|
username: pickFirstNonEmpty([
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ export function getAdminFulfillmentRoutingConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAdminFulfillmentRoutingConfig(payload: JsonRecord = {}) {
|
export async function updateAdminFulfillmentRoutingConfig(payload: JsonRecord = {}) {
|
||||||
return {
|
return {
|
||||||
filePath: getFulfillmentRoutingConfigFilePath(),
|
filePath: getFulfillmentRoutingConfigFilePath(),
|
||||||
...saveFulfillmentRoutingConfig(payload),
|
...(await saveFulfillmentRoutingConfig(payload)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export function getAdminKuaishouFeifeiConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAdminKuaishouFeifeiConfig(payload: JsonObject = {}) {
|
export async function updateAdminKuaishouFeifeiConfig(payload: JsonObject = {}) {
|
||||||
const saved = saveKuaishouFeifeiSourceConfig(payload)
|
const saved = await saveKuaishouFeifeiSourceConfig(payload)
|
||||||
const effective = getKuaishouFeifeiConfig()
|
const effective = getKuaishouFeifeiConfig()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -92,7 +92,7 @@ export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject =
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((rule): rule is NonNullable<typeof rule> => Boolean(rule))
|
.filter((rule): rule is NonNullable<typeof rule> => Boolean(rule))
|
||||||
const saved = saveKuaishouFeifeiSourceConfig({
|
const saved = await saveKuaishouFeifeiSourceConfig({
|
||||||
...source,
|
...source,
|
||||||
productRules,
|
productRules,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export function getAdminKuaishouIndustrySourceConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAdminKuaishouIndustrySourceConfig(payload: JsonObject = {}) {
|
export async function updateAdminKuaishouIndustrySourceConfig(payload: JsonObject = {}) {
|
||||||
const current = getKuaishouIndustrySourceConfig()
|
const current = getKuaishouIndustrySourceConfig()
|
||||||
const saved = saveKuaishouIndustrySourceConfig({
|
const saved = await saveKuaishouIndustrySourceConfig({
|
||||||
...current,
|
...current,
|
||||||
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current.enabled !== false,
|
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current.enabled !== false,
|
||||||
baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl),
|
baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl),
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ export function getAdminNotificationConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAdminNotificationConfig(payload: AdminNotificationConfigInput = {}) {
|
export async function updateAdminNotificationConfig(payload: AdminNotificationConfigInput = {}) {
|
||||||
const saved = saveNotificationConfig(payload)
|
const saved = await saveNotificationConfig(payload)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filePath: getNotificationConfigFilePath(),
|
filePath: getNotificationConfigFilePath(),
|
||||||
@@ -64,8 +64,8 @@ export function getAdminScheduledJobsConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAdminScheduledJobsConfig(payload: AdminScheduledJobsConfigInput = {}) {
|
export async function updateAdminScheduledJobsConfig(payload: AdminScheduledJobsConfigInput = {}) {
|
||||||
const saved = saveScheduledJobsConfig(payload)
|
const saved = await saveScheduledJobsConfig(payload)
|
||||||
reloadScheduledJobs()
|
reloadScheduledJobs()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||||
import { readJsonFile, writeJsonFile } from '../../utils/json-file-store.js'
|
import {
|
||||||
|
readAppConfigEntry,
|
||||||
|
saveAppConfigEntry,
|
||||||
|
} from '../config/app-config-store.js'
|
||||||
import { normalizeCloudtentaclesMatchName } from '../order/cloudtentacles-match-utils.js'
|
import { normalizeCloudtentaclesMatchName } from '../order/cloudtentacles-match-utils.js'
|
||||||
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
|
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
|
||||||
|
|
||||||
@@ -77,19 +81,21 @@ export function getFulfillmentRoutingConfigFilePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getFulfillmentRoutingConfig(): FulfillmentRoutingConfig {
|
export function getFulfillmentRoutingConfig(): FulfillmentRoutingConfig {
|
||||||
return readJsonFile(
|
return readAppConfigEntry({
|
||||||
FULFILLMENT_ROUTING_CONFIG_FILE_PATH,
|
configKey: APP_CONFIG_KEYS.fulfillmentRouting,
|
||||||
createDefaultFulfillmentRoutingConfig,
|
legacyFilePath: FULFILLMENT_ROUTING_CONFIG_FILE_PATH,
|
||||||
normalizeFulfillmentRoutingConfig,
|
fallback: createDefaultFulfillmentRoutingConfig,
|
||||||
)
|
normalize: normalizeFulfillmentRoutingConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveFulfillmentRoutingConfig(rawValue: unknown): FulfillmentRoutingConfig {
|
export function saveFulfillmentRoutingConfig(rawValue: unknown): Promise<FulfillmentRoutingConfig> {
|
||||||
return writeJsonFile(
|
return saveAppConfigEntry({
|
||||||
FULFILLMENT_ROUTING_CONFIG_FILE_PATH,
|
configKey: APP_CONFIG_KEYS.fulfillmentRouting,
|
||||||
rawValue,
|
legacyFilePath: FULFILLMENT_ROUTING_CONFIG_FILE_PATH,
|
||||||
normalizeFulfillmentRoutingConfig,
|
value: rawValue,
|
||||||
)
|
normalize: normalizeFulfillmentRoutingConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeFulfillmentRoutingConfig(rawValue: unknown): FulfillmentRoutingConfig {
|
export function normalizeFulfillmentRoutingConfig(rawValue: unknown): FulfillmentRoutingConfig {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||||
import { readJsonFile, writeJsonFile } from '../../utils/json-file-store.js'
|
import {
|
||||||
|
readAppConfigEntry,
|
||||||
|
saveAppConfigEntry,
|
||||||
|
} from '../config/app-config-store.js'
|
||||||
|
|
||||||
const NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notification-config.json')
|
const NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notification-config.json')
|
||||||
const DEFAULT_BARK_SERVER_URL = 'https://api.day.app'
|
const DEFAULT_BARK_SERVER_URL = 'https://api.day.app'
|
||||||
@@ -20,11 +24,21 @@ export function getNotificationConfigFilePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getNotificationConfig() {
|
export function getNotificationConfig() {
|
||||||
return loadNotificationConfigFromFile()
|
return readAppConfigEntry({
|
||||||
|
configKey: APP_CONFIG_KEYS.notification,
|
||||||
|
legacyFilePath: NOTIFICATION_CONFIG_FILE_PATH,
|
||||||
|
fallback: createDefaultNotificationConfig,
|
||||||
|
normalize: normalizeNotificationConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveNotificationConfig(rawValue: unknown) {
|
export function saveNotificationConfig(rawValue: unknown) {
|
||||||
return writeJsonFile(NOTIFICATION_CONFIG_FILE_PATH, rawValue, normalizeNotificationConfig)
|
return saveAppConfigEntry({
|
||||||
|
configKey: APP_CONFIG_KEYS.notification,
|
||||||
|
legacyFilePath: NOTIFICATION_CONFIG_FILE_PATH,
|
||||||
|
value: rawValue,
|
||||||
|
normalize: normalizeNotificationConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listEnabledBarkRecipients(config: JsonObject = getNotificationConfig()) {
|
export function listEnabledBarkRecipients(config: JsonObject = getNotificationConfig()) {
|
||||||
@@ -45,14 +59,6 @@ export function listEnabledWpushRecipients(config: JsonObject = getNotificationC
|
|||||||
.filter((item: JsonObject) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim())
|
.filter((item: JsonObject) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadNotificationConfigFromFile() {
|
|
||||||
return readJsonFile(
|
|
||||||
NOTIFICATION_CONFIG_FILE_PATH,
|
|
||||||
createDefaultNotificationConfig,
|
|
||||||
normalizeNotificationConfig,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeNotificationConfig(rawValue: unknown) {
|
export function normalizeNotificationConfig(rawValue: unknown) {
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
const bark = isPlainObject(source.channels?.bark) ? source.channels.bark : {}
|
const bark = isPlainObject(source.channels?.bark) ? source.channels.bark : {}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||||
import { readJsonFile, writeJsonFile } from '../../utils/json-file-store.js'
|
import {
|
||||||
|
readAppConfigEntry,
|
||||||
|
saveAppConfigEntry,
|
||||||
|
} from '../config/app-config-store.js'
|
||||||
import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js'
|
import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js'
|
||||||
|
|
||||||
const CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH = path.join(
|
const CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH = path.join(
|
||||||
@@ -38,19 +42,21 @@ export function getCloudtentaclesOverrideRuleFilePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCloudtentaclesOverrideRuleConfig(): CloudtentaclesOverrideRuleConfig {
|
export function getCloudtentaclesOverrideRuleConfig(): CloudtentaclesOverrideRuleConfig {
|
||||||
return readJsonFile(
|
return readAppConfigEntry({
|
||||||
CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
|
configKey: APP_CONFIG_KEYS.cloudtentaclesOverrideRules,
|
||||||
createDefaultCloudtentaclesOverrideRuleConfig,
|
legacyFilePath: CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
|
||||||
normalizeCloudtentaclesOverrideRuleConfig,
|
fallback: createDefaultCloudtentaclesOverrideRuleConfig,
|
||||||
)
|
normalize: normalizeCloudtentaclesOverrideRuleConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveCloudtentaclesOverrideRuleConfig(rawValue: unknown): CloudtentaclesOverrideRuleConfig {
|
export function saveCloudtentaclesOverrideRuleConfig(rawValue: unknown): Promise<CloudtentaclesOverrideRuleConfig> {
|
||||||
return writeJsonFile(
|
return saveAppConfigEntry({
|
||||||
CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
|
configKey: APP_CONFIG_KEYS.cloudtentaclesOverrideRules,
|
||||||
rawValue,
|
legacyFilePath: CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
|
||||||
normalizeCloudtentaclesOverrideRuleConfig,
|
value: rawValue,
|
||||||
)
|
normalize: normalizeCloudtentaclesOverrideRuleConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveCloudtentaclesOverrideRule(productName: unknown) {
|
export function resolveCloudtentaclesOverrideRule(productName: unknown) {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
|
||||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
|
import {
|
||||||
|
readAppConfigEntry,
|
||||||
|
saveAppConfigEntry,
|
||||||
|
} from '../../config/app-config-store.js'
|
||||||
import {
|
import {
|
||||||
DEFAULT_CLOUDTENTACLES_DEVICE_ID,
|
DEFAULT_CLOUDTENTACLES_DEVICE_ID,
|
||||||
DEFAULT_CLOUDTENTACLES_DEVICE_TYPE,
|
DEFAULT_CLOUDTENTACLES_DEVICE_TYPE,
|
||||||
@@ -22,47 +26,38 @@ export function getCloudtentaclesSessionFilePath() {
|
|||||||
return CLOUDTENTACLES_SESSION_FILE_PATH
|
return CLOUDTENTACLES_SESSION_FILE_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 兼容旧调用:返回 key='default' 的会话。
|
||||||
* Backward-compatible: returns the session for key='default'.
|
|
||||||
* Old callers that expect a single session object still work.
|
|
||||||
*/
|
|
||||||
export function getCloudtentaclesSessionState() {
|
export function getCloudtentaclesSessionState() {
|
||||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
const states = loadCloudtentaclesSessionStates()
|
||||||
return states.sessions['default'] || createDefaultCloudtentaclesSessionState()
|
return states.sessions['default'] || createDefaultCloudtentaclesSessionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 按 sourceKey 获取会话,未命中时返回 null。
|
||||||
* Get a session by its sourceKey. Returns null if not found.
|
|
||||||
*/
|
|
||||||
export function getCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
export function getCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
const states = loadCloudtentaclesSessionStates()
|
||||||
const key = String(sourceKey || '').trim()
|
const key = String(sourceKey || '').trim()
|
||||||
if (!key) return null
|
if (!key) return null
|
||||||
return states.sessions[key] || null
|
return states.sessions[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 返回所有账号的会话状态。
|
||||||
* Return the entire session states map: { sessions: { 'default': {...}, ... } }.
|
|
||||||
*/
|
|
||||||
export function getAllCloudtentaclesSessionStates() {
|
export function getAllCloudtentaclesSessionStates() {
|
||||||
return loadCloudtentaclesSessionStatesFromFile()
|
return loadCloudtentaclesSessionStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 兼容旧保存格式:支持单会话对象和 sessions 映射。
|
||||||
* Backward-compatible save: accepts both old single-object format
|
|
||||||
* and new sessions-map format, normalizes, and persists.
|
|
||||||
*/
|
|
||||||
export function saveCloudtentaclesSessionState(rawValue: unknown) {
|
export function saveCloudtentaclesSessionState(rawValue: unknown) {
|
||||||
const normalized = normalizeSessionStatesFile(rawValue)
|
const normalized = normalizeSessionStatesFile(rawValue)
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
return saveAppConfigEntry({
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
configKey: APP_CONFIG_KEYS.cloudtentaclesSession,
|
||||||
return normalized.sessions['default'] || createDefaultCloudtentaclesSessionState()
|
legacyFilePath: CLOUDTENTACLES_SESSION_FILE_PATH,
|
||||||
|
value: normalized,
|
||||||
|
normalize: normalizeSessionStatesFile,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 保存指定 sourceKey 的会话。
|
||||||
* Save a session for a specific sourceKey.
|
export async function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue: unknown) {
|
||||||
*/
|
|
||||||
export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue: unknown) {
|
|
||||||
const key = String(sourceKey || '').trim()
|
const key = String(sourceKey || '').trim()
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||||
@@ -71,18 +66,15 @@ export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
const states = loadCloudtentaclesSessionStates()
|
||||||
states.sessions[key] = normalizeCloudtentaclesSessionState(rawValue)
|
states.sessions[key] = normalizeCloudtentaclesSessionState(rawValue)
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
await saveCloudtentaclesSessionState(states)
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
|
|
||||||
return states.sessions[key]
|
return states.sessions[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 删除指定 sourceKey 的会话。
|
||||||
* Delete a session by sourceKey.
|
export async function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||||
*/
|
|
||||||
export function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
|
||||||
const key = String(sourceKey || '').trim()
|
const key = String(sourceKey || '').trim()
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||||
@@ -91,25 +83,20 @@ export function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
const states = loadCloudtentaclesSessionStates()
|
||||||
delete states.sessions[key]
|
delete states.sessions[key]
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
await saveCloudtentaclesSessionState(states)
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
|
|
||||||
return states
|
return states
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 兼容旧调用:只清理 default 会话。
|
||||||
* Backward-compatible clear: clears the 'default' session only.
|
|
||||||
*/
|
|
||||||
export function clearCloudtentaclesSessionState() {
|
export function clearCloudtentaclesSessionState() {
|
||||||
return clearCloudtentaclesSessionStateByKey('default')
|
return clearCloudtentaclesSessionStateByKey('default')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 清理指定 sourceKey 的会话,重置为空状态。
|
||||||
* Clear a session by sourceKey (sets it to default empty state).
|
export async function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||||
*/
|
|
||||||
export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
|
||||||
const key = String(sourceKey || '').trim()
|
const key = String(sourceKey || '').trim()
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||||
@@ -119,50 +106,37 @@ export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleared = createDefaultCloudtentaclesSessionState()
|
const cleared = createDefaultCloudtentaclesSessionState()
|
||||||
saveCloudtentaclesSessionStateByKey(key, cleared)
|
await saveCloudtentaclesSessionStateByKey(key, cleared)
|
||||||
return cleared
|
return cleared
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 只保留仍存在 source 配置的会话。
|
||||||
* Keep only sessions whose sourceKey still exists in source config.
|
export async function pruneCloudtentaclesSessionStates(sourceKeys: unknown[] = []) {
|
||||||
*/
|
|
||||||
export function pruneCloudtentaclesSessionStates(sourceKeys: unknown[] = []) {
|
|
||||||
const allowedKeys = new Set(
|
const allowedKeys = new Set(
|
||||||
Array.isArray(sourceKeys)
|
Array.isArray(sourceKeys)
|
||||||
? sourceKeys.map((value) => String(value || '').trim()).filter(Boolean)
|
? sourceKeys.map((value) => String(value || '').trim()).filter(Boolean)
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
|
|
||||||
const states = loadCloudtentaclesSessionStatesFromFile()
|
const states = loadCloudtentaclesSessionStates()
|
||||||
states.sessions = Object.fromEntries(
|
states.sessions = Object.fromEntries(
|
||||||
Object.entries(states.sessions || {}).filter(([key]) => allowedKeys.has(String(key || '').trim()))
|
Object.entries(states.sessions || {}).filter(([key]) => allowedKeys.has(String(key || '').trim()))
|
||||||
)
|
)
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
await saveCloudtentaclesSessionState(states)
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
|
|
||||||
return states
|
return states
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
function loadCloudtentaclesSessionStates(): CloudtentaclesSessionStatesFile {
|
||||||
// Internal helpers
|
return readAppConfigEntry({
|
||||||
// ---------------------------------------------------------------------------
|
configKey: APP_CONFIG_KEYS.cloudtentaclesSession,
|
||||||
|
legacyFilePath: CLOUDTENTACLES_SESSION_FILE_PATH,
|
||||||
function loadCloudtentaclesSessionStatesFromFile(): CloudtentaclesSessionStatesFile {
|
fallback: createDefaultCloudtentaclesSessionStates,
|
||||||
if (!fs.existsSync(CLOUDTENTACLES_SESSION_FILE_PATH)) {
|
normalize: normalizeSessionStatesFile,
|
||||||
return createDefaultCloudtentaclesSessionStates()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 标准化单个会话状态。
|
||||||
const rawText = fs.readFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, 'utf8')
|
|
||||||
return normalizeSessionStatesFile(JSON.parse(rawText))
|
|
||||||
} catch {
|
|
||||||
return createDefaultCloudtentaclesSessionStates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a single session state item.
|
|
||||||
*/
|
|
||||||
function normalizeCloudtentaclesSessionState(rawValue: unknown) {
|
function normalizeCloudtentaclesSessionState(rawValue: unknown) {
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
|
|
||||||
@@ -177,13 +151,8 @@ function normalizeCloudtentaclesSessionState(rawValue: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 标准化整体会话状态,同时兼容旧版单会话对象。
|
||||||
* Normalize the overall session states file format.
|
export function normalizeSessionStatesFile(rawValue: unknown): CloudtentaclesSessionStatesFile {
|
||||||
* Handles old format (single object without sessions key) by auto-wrapping
|
|
||||||
* into { sessions: { 'default': ... } }.
|
|
||||||
*/
|
|
||||||
function normalizeSessionStatesFile(rawValue: unknown): CloudtentaclesSessionStatesFile {
|
|
||||||
// Old format: { token: 'xxx', ... } (single object, no sessions key)
|
|
||||||
if (isPlainObject(rawValue) && !rawValue.sessions) {
|
if (isPlainObject(rawValue) && !rawValue.sessions) {
|
||||||
return {
|
return {
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -192,7 +161,6 @@ function normalizeSessionStatesFile(rawValue: unknown): CloudtentaclesSessionSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New format: { sessions: { 'default': {...}, ... } }
|
|
||||||
return {
|
return {
|
||||||
sessions: isPlainObject(rawValue) && isPlainObject(rawValue.sessions)
|
sessions: isPlainObject(rawValue) && isPlainObject(rawValue.sessions)
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
|
||||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
|
import {
|
||||||
|
readAppConfigEntry,
|
||||||
|
saveAppConfigEntry,
|
||||||
|
} from '../../config/app-config-store.js'
|
||||||
import {
|
import {
|
||||||
normalizeCloudtentaclesDeviceId,
|
normalizeCloudtentaclesDeviceId,
|
||||||
normalizeCloudtentaclesDeviceType,
|
normalizeCloudtentaclesDeviceType,
|
||||||
@@ -16,56 +20,44 @@ export function getCloudtentaclesSourcesFilePath() {
|
|||||||
return CLOUDTENTACLES_SOURCES_FILE_PATH
|
return CLOUDTENTACLES_SOURCES_FILE_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 兼容旧调用:返回 key='default' 的单账号配置。
|
||||||
* Backward-compatible: returns the source with key='default'.
|
|
||||||
* Old callers that expect a single source object still work.
|
|
||||||
*/
|
|
||||||
export function getCloudtentaclesSourceConfig() {
|
export function getCloudtentaclesSourceConfig() {
|
||||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
const config = loadCloudtentaclesSourcesConfig()
|
||||||
const defaultSource = config.sources.find(s => s.key === 'default')
|
const defaultSource = config.sources.find(s => s.key === 'default')
|
||||||
return defaultSource || normalizeCloudtentaclesSourceItem({ key: 'default' })
|
return defaultSource || normalizeCloudtentaclesSourceItem({ key: 'default' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 按 sourceKey 获取账号配置,未命中时返回 null。
|
||||||
* Get a source item by its key. Returns null if not found.
|
|
||||||
*/
|
|
||||||
export function getCloudtentaclesSourceByKey(sourceKey: unknown) {
|
export function getCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
const config = loadCloudtentaclesSourcesConfig()
|
||||||
const key = String(sourceKey || '').trim()
|
const key = String(sourceKey || '').trim()
|
||||||
if (!key) return null
|
if (!key) return null
|
||||||
return config.sources.find(s => s.key === key) || null
|
return config.sources.find(s => s.key === key) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 返回完整的多账号配置。
|
||||||
* Return the entire normalized config: { enabled, sources }.
|
|
||||||
*/
|
|
||||||
export function listCloudtentaclesSources() {
|
export function listCloudtentaclesSources() {
|
||||||
return loadCloudtentaclesSourcesConfigFromFile()
|
return loadCloudtentaclesSourcesConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 兼容旧保存格式:支持单账号对象和 { enabled, sources } 多账号格式。
|
||||||
* Backward-compatible save: accepts both old single-object format
|
|
||||||
* and new list format, normalizes, and persists.
|
|
||||||
*/
|
|
||||||
export function saveCloudtentaclesSourceConfig(rawValue: unknown) {
|
export function saveCloudtentaclesSourceConfig(rawValue: unknown) {
|
||||||
const normalized = normalizeCloudtentaclesSourcesConfig(rawValue)
|
const normalized = normalizeCloudtentaclesSourcesConfig(rawValue)
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
return saveAppConfigEntry({
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
configKey: APP_CONFIG_KEYS.cloudtentaclesSources,
|
||||||
return normalized
|
legacyFilePath: CLOUDTENTACLES_SOURCES_FILE_PATH,
|
||||||
|
value: normalized,
|
||||||
|
normalize: normalizeCloudtentaclesSourcesConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 保存完整多账号配置。
|
||||||
* Save the entire list-format config object: { enabled, sources: [...] }.
|
|
||||||
*/
|
|
||||||
export function saveCloudtentaclesSourcesList(rawValue: unknown) {
|
export function saveCloudtentaclesSourcesList(rawValue: unknown) {
|
||||||
return saveCloudtentaclesSourceConfig(rawValue)
|
return saveCloudtentaclesSourceConfig(rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 按 sourceKey 新增或替换单个账号配置。
|
||||||
* Save or update a single source item identified by sourceKey.
|
export async function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObject = {}) {
|
||||||
* If a source with the same key exists, it is replaced; otherwise it is appended.
|
|
||||||
*/
|
|
||||||
export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObject = {}) {
|
|
||||||
const key = String(sourceKey || '').trim()
|
const key = String(sourceKey || '').trim()
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||||
@@ -74,7 +66,7 @@ export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObje
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
const config = loadCloudtentaclesSourcesConfig()
|
||||||
const normalizedItem = normalizeCloudtentaclesSourceItem({ ...data, key })
|
const normalizedItem = normalizeCloudtentaclesSourceItem({ ...data, key })
|
||||||
const existingIndex = config.sources.findIndex(s => s.key === key)
|
const existingIndex = config.sources.findIndex(s => s.key === key)
|
||||||
|
|
||||||
@@ -84,15 +76,12 @@ export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObje
|
|||||||
config.sources.push(normalizedItem)
|
config.sources.push(normalizedItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
await saveCloudtentaclesSourceConfig(config)
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
return normalizedItem
|
||||||
return config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 删除单个账号配置。
|
||||||
* Delete a single source by key.
|
export async function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||||
*/
|
|
||||||
export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
|
||||||
const key = String(sourceKey || '').trim()
|
const key = String(sourceKey || '').trim()
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
throw createHttpError('cloudtentacles sourceKey 不能为空', {
|
||||||
@@ -101,7 +90,7 @@ export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
const config = loadCloudtentaclesSourcesConfig()
|
||||||
const existingIndex = config.sources.findIndex(s => s.key === key)
|
const existingIndex = config.sources.findIndex(s => s.key === key)
|
||||||
|
|
||||||
if (existingIndex < 0) {
|
if (existingIndex < 0) {
|
||||||
@@ -110,31 +99,20 @@ export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
|||||||
|
|
||||||
config.sources.splice(existingIndex, 1)
|
config.sources.splice(existingIndex, 1)
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
await saveCloudtentaclesSourceConfig(config)
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
function loadCloudtentaclesSourcesConfig() {
|
||||||
// Internal helpers
|
return readAppConfigEntry({
|
||||||
// ---------------------------------------------------------------------------
|
configKey: APP_CONFIG_KEYS.cloudtentaclesSources,
|
||||||
|
legacyFilePath: CLOUDTENTACLES_SOURCES_FILE_PATH,
|
||||||
function loadCloudtentaclesSourcesConfigFromFile() {
|
fallback: createDefaultCloudtentaclesSourcesConfig,
|
||||||
if (!fs.existsSync(CLOUDTENTACLES_SOURCES_FILE_PATH)) {
|
normalize: normalizeCloudtentaclesSourcesConfig,
|
||||||
return createDefaultCloudtentaclesSourcesConfig()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 标准化单个账号配置,补齐 key 和 label。
|
||||||
const rawText = fs.readFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, 'utf8')
|
|
||||||
return normalizeCloudtentaclesSourcesConfig(JSON.parse(rawText))
|
|
||||||
} catch {
|
|
||||||
return createDefaultCloudtentaclesSourcesConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a single source item. Adds key (required) and label (optional).
|
|
||||||
*/
|
|
||||||
function normalizeCloudtentaclesSourceItem(rawValue: unknown) {
|
function normalizeCloudtentaclesSourceItem(rawValue: unknown) {
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
|
|
||||||
@@ -151,12 +129,8 @@ function normalizeCloudtentaclesSourceItem(rawValue: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 标准化整体配置,同时兼容旧版单账号对象。
|
||||||
* Normalize the overall config. Handles both old single-object format
|
export function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) {
|
||||||
* (auto-migrates to new list format) and new { enabled, sources } format.
|
|
||||||
*/
|
|
||||||
function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) {
|
|
||||||
// Old format: { enabled: true, username: 'xxx', ... } (single object, no sources array)
|
|
||||||
if (isPlainObject(rawValue) && !Array.isArray(rawValue.sources)) {
|
if (isPlainObject(rawValue) && !Array.isArray(rawValue.sources)) {
|
||||||
return {
|
return {
|
||||||
enabled: rawValue.enabled !== false,
|
enabled: rawValue.enabled !== false,
|
||||||
@@ -164,13 +138,12 @@ function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) {
|
|||||||
normalizeCloudtentaclesSourceItem({
|
normalizeCloudtentaclesSourceItem({
|
||||||
key: 'default',
|
key: 'default',
|
||||||
label: '默认账号',
|
label: '默认账号',
|
||||||
...rawValue, // old fields auto-map to default source
|
...rawValue, // 旧字段自动映射为 default 账号。
|
||||||
}),
|
}),
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New format: { enabled, sources: [...] }
|
|
||||||
return {
|
return {
|
||||||
enabled: isPlainObject(rawValue) ? rawValue.enabled !== false : true,
|
enabled: isPlainObject(rawValue) ? rawValue.enabled !== false : true,
|
||||||
sources: isPlainObject(rawValue) && Array.isArray(rawValue.sources)
|
sources: isPlainObject(rawValue) && Array.isArray(rawValue.sources)
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
|
||||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||||
import { readJsonFile, writeJsonFile } from '../../../utils/json-file-store.js'
|
import {
|
||||||
|
hasAppConfigEntry,
|
||||||
|
readAppConfigEntry,
|
||||||
|
saveAppConfigEntry,
|
||||||
|
} from '../../config/app-config-store.js'
|
||||||
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
||||||
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
||||||
|
|
||||||
@@ -29,23 +33,28 @@ export function getKuaishouFeifeiConfigFilePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasKuaishouFeifeiConfigFile() {
|
export function hasKuaishouFeifeiConfigFile() {
|
||||||
return fs.existsSync(KUAISHOU_FEIFEI_CONFIG_FILE_PATH)
|
return hasAppConfigEntry(
|
||||||
|
APP_CONFIG_KEYS.kuaishouFeifei,
|
||||||
|
KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getKuaishouFeifeiSourceConfig(): KuaishouFeifeiSourceConfig {
|
export function getKuaishouFeifeiSourceConfig(): KuaishouFeifeiSourceConfig {
|
||||||
return readJsonFile(
|
return readAppConfigEntry({
|
||||||
KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
configKey: APP_CONFIG_KEYS.kuaishouFeifei,
|
||||||
createDefaultKuaishouFeifeiSourceConfig,
|
legacyFilePath: KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||||
normalizeKuaishouFeifeiSourceConfig,
|
fallback: createDefaultKuaishouFeifeiSourceConfig,
|
||||||
)
|
normalize: normalizeKuaishouFeifeiSourceConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveKuaishouFeifeiSourceConfig(rawValue: unknown): KuaishouFeifeiSourceConfig {
|
export function saveKuaishouFeifeiSourceConfig(rawValue: unknown): Promise<KuaishouFeifeiSourceConfig> {
|
||||||
return writeJsonFile(
|
return saveAppConfigEntry({
|
||||||
KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
configKey: APP_CONFIG_KEYS.kuaishouFeifei,
|
||||||
rawValue,
|
legacyFilePath: KUAISHOU_FEIFEI_CONFIG_FILE_PATH,
|
||||||
normalizeKuaishouFeifeiSourceConfig,
|
value: rawValue,
|
||||||
)
|
normalize: normalizeKuaishouFeifeiSourceConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeKuaishouFeifeiSourceConfig(rawValue: unknown): KuaishouFeifeiSourceConfig {
|
export function normalizeKuaishouFeifeiSourceConfig(rawValue: unknown): KuaishouFeifeiSourceConfig {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
|
||||||
import { PROJECT_ROOT, runtimeConfig } from '../../../config/runtime.js'
|
import { PROJECT_ROOT, runtimeConfig } from '../../../config/runtime.js'
|
||||||
import { readJsonFile, writeJsonFile } from '../../../utils/json-file-store.js'
|
import {
|
||||||
|
readAppConfigEntry,
|
||||||
|
saveAppConfigEntry,
|
||||||
|
} from '../../config/app-config-store.js'
|
||||||
|
|
||||||
const KUAISHOU_INDUSTRY_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-industry-source.json')
|
const KUAISHOU_INDUSTRY_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-industry-source.json')
|
||||||
const DEFAULT_CALLBACK_BASE_URL = 'https://openapi.kwaixiaodian.com'
|
const DEFAULT_CALLBACK_BASE_URL = 'https://openapi.kwaixiaodian.com'
|
||||||
@@ -62,24 +66,26 @@ export function getKuaishouIndustrySourceFilePath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConfig {
|
export function getKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConfig {
|
||||||
return readJsonFile(
|
return readAppConfigEntry({
|
||||||
KUAISHOU_INDUSTRY_SOURCE_FILE_PATH,
|
configKey: APP_CONFIG_KEYS.kuaishouIndustrySource,
|
||||||
createDefaultKuaishouIndustrySourceConfig,
|
legacyFilePath: KUAISHOU_INDUSTRY_SOURCE_FILE_PATH,
|
||||||
normalizeKuaishouIndustrySourceConfig,
|
fallback: createDefaultKuaishouIndustrySourceConfig,
|
||||||
)
|
normalize: normalizeKuaishouIndustrySourceConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig {
|
export function saveKuaishouIndustrySourceConfig(rawValue: unknown): Promise<KuaishouIndustrySourceConfig> {
|
||||||
return writeJsonFile(
|
return saveAppConfigEntry({
|
||||||
KUAISHOU_INDUSTRY_SOURCE_FILE_PATH,
|
configKey: APP_CONFIG_KEYS.kuaishouIndustrySource,
|
||||||
rawValue,
|
legacyFilePath: KUAISHOU_INDUSTRY_SOURCE_FILE_PATH,
|
||||||
normalizeKuaishouIndustrySourceConfig,
|
value: rawValue,
|
||||||
)
|
normalize: normalizeKuaishouIndustrySourceConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patchKuaishouIndustrySourceConfig(
|
export function patchKuaishouIndustrySourceConfig(
|
||||||
patch: Partial<KuaishouIndustrySourceConfig>,
|
patch: Partial<KuaishouIndustrySourceConfig>,
|
||||||
): KuaishouIndustrySourceConfig {
|
): Promise<KuaishouIndustrySourceConfig> {
|
||||||
return saveKuaishouIndustrySourceConfig({
|
return saveKuaishouIndustrySourceConfig({
|
||||||
...getKuaishouIndustrySourceConfig(),
|
...getKuaishouIndustrySourceConfig(),
|
||||||
...patch,
|
...patch,
|
||||||
@@ -108,7 +114,7 @@ export function findKuaishouIndustryShopConfig(
|
|||||||
export function patchKuaishouIndustryShopConfig(
|
export function patchKuaishouIndustryShopConfig(
|
||||||
sellerId: unknown,
|
sellerId: unknown,
|
||||||
patch: Partial<KuaishouIndustryShopConfig>,
|
patch: Partial<KuaishouIndustryShopConfig>,
|
||||||
): KuaishouIndustrySourceConfig {
|
): Promise<KuaishouIndustrySourceConfig> {
|
||||||
const source = getKuaishouIndustrySourceConfig()
|
const source = getKuaishouIndustrySourceConfig()
|
||||||
const normalizedSellerId = String(patch.sellerId || sellerId || '').trim()
|
const normalizedSellerId = String(patch.sellerId || sellerId || '').trim()
|
||||||
const existing = findKuaishouIndustryShopConfig(normalizedSellerId, source)
|
const existing = findKuaishouIndustryShopConfig(normalizedSellerId, source)
|
||||||
@@ -119,7 +125,7 @@ export function patchKuaishouIndustryShopConfig(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!nextShop) {
|
if (!nextShop) {
|
||||||
return source
|
return Promise.resolve(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
const shops = listKuaishouIndustryShopConfigs(source)
|
const shops = listKuaishouIndustryShopConfigs(source)
|
||||||
@@ -131,7 +137,7 @@ export function patchKuaishouIndustryShopConfig(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig {
|
export function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig {
|
||||||
const fallback = createDefaultKuaishouIndustrySourceConfig()
|
const fallback = createDefaultKuaishouIndustrySourceConfig()
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
const shops = normalizeKuaishouIndustryShopList(source)
|
const shops = normalizeKuaishouIndustryShopList(source)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export async function refreshKuaishouIndustryAccessToken(
|
|||||||
failureMessage: '快手 accessToken 刷新失败',
|
failureMessage: '快手 accessToken 刷新失败',
|
||||||
errorCode: 'kuaishou_industry_access_token_refresh_failed',
|
errorCode: 'kuaishou_industry_access_token_refresh_failed',
|
||||||
})
|
})
|
||||||
const saved = saveTokenPayload(tokenPayload, config, shop)
|
const saved = await saveTokenPayload(tokenPayload, config, shop)
|
||||||
|
|
||||||
logInfo('[kuaishou-industry/token]', 'accessToken 刷新成功', {
|
logInfo('[kuaishou-industry/token]', 'accessToken 刷新成功', {
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
@@ -94,10 +94,10 @@ export async function refreshKuaishouIndustryAccessToken(
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
patchKuaishouIndustryShopConfig(shop.sellerId, {
|
await patchKuaishouIndustryShopConfig(shop.sellerId, {
|
||||||
lastRefreshError: message,
|
lastRefreshError: message,
|
||||||
})
|
})
|
||||||
patchKuaishouIndustrySourceConfig({
|
await patchKuaishouIndustrySourceConfig({
|
||||||
lastRefreshError: message,
|
lastRefreshError: message,
|
||||||
})
|
})
|
||||||
logWarn('[kuaishou-industry/token]', 'accessToken 刷新失败', resolveRefreshErrorDetail(error))
|
logWarn('[kuaishou-industry/token]', 'accessToken 刷新失败', resolveRefreshErrorDetail(error))
|
||||||
@@ -133,7 +133,7 @@ export async function exchangeKuaishouIndustryAuthorizationCode(
|
|||||||
failureMessage: '快手授权码换取 accessToken 失败',
|
failureMessage: '快手授权码换取 accessToken 失败',
|
||||||
errorCode: 'kuaishou_industry_authorization_code_exchange_failed',
|
errorCode: 'kuaishou_industry_authorization_code_exchange_failed',
|
||||||
})
|
})
|
||||||
const saved = saveTokenPayload(tokenPayload, config, {
|
const saved = await saveTokenPayload(tokenPayload, config, {
|
||||||
...createEmptyShopConfig(),
|
...createEmptyShopConfig(),
|
||||||
sellerId: String(options.sellerId || '').trim(),
|
sellerId: String(options.sellerId || '').trim(),
|
||||||
shopId: String(options.sellerId || '').trim(),
|
shopId: String(options.sellerId || '').trim(),
|
||||||
@@ -157,7 +157,7 @@ export async function exchangeKuaishouIndustryAuthorizationCode(
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
patchKuaishouIndustrySourceConfig({
|
await patchKuaishouIndustrySourceConfig({
|
||||||
lastRefreshError: message,
|
lastRefreshError: message,
|
||||||
})
|
})
|
||||||
logWarn('[kuaishou-industry/token]', '授权码换取 accessToken 失败', resolveRefreshErrorDetail(error))
|
logWarn('[kuaishou-industry/token]', '授权码换取 accessToken 失败', resolveRefreshErrorDetail(error))
|
||||||
@@ -314,7 +314,7 @@ async function requestKuaishouIndustryToken({
|
|||||||
|
|
||||||
if (!response.ok || !isTokenResponseSuccess(json) || !tokenPayload.accessToken) {
|
if (!response.ok || !isTokenResponseSuccess(json) || !tokenPayload.accessToken) {
|
||||||
const message = resolveTokenErrorMessage(json, response.status, failureMessage)
|
const message = resolveTokenErrorMessage(json, response.status, failureMessage)
|
||||||
patchKuaishouIndustrySourceConfig({
|
await patchKuaishouIndustrySourceConfig({
|
||||||
lastRefreshError: message,
|
lastRefreshError: message,
|
||||||
})
|
})
|
||||||
throw createHttpError(message, {
|
throw createHttpError(message, {
|
||||||
@@ -326,7 +326,7 @@ async function requestKuaishouIndustryToken({
|
|||||||
return tokenPayload
|
return tokenPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTokenPayload(
|
async function saveTokenPayload(
|
||||||
tokenPayload: ReturnType<typeof normalizeTokenResponse>,
|
tokenPayload: ReturnType<typeof normalizeTokenResponse>,
|
||||||
config: KuaishouIndustrySourceConfig,
|
config: KuaishouIndustrySourceConfig,
|
||||||
shop: KuaishouIndustryShopConfig,
|
shop: KuaishouIndustryShopConfig,
|
||||||
@@ -340,7 +340,7 @@ function saveTokenPayload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextRefreshToken = tokenPayload.refreshToken || shop.refreshToken
|
const nextRefreshToken = tokenPayload.refreshToken || shop.refreshToken
|
||||||
const savedConfig = patchKuaishouIndustryShopConfig(nextSellerId, {
|
const savedConfig = await patchKuaishouIndustryShopConfig(nextSellerId, {
|
||||||
...shop,
|
...shop,
|
||||||
enabled: shop.enabled !== false,
|
enabled: shop.enabled !== false,
|
||||||
sellerId: nextSellerId,
|
sellerId: nextSellerId,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||||
import { readJsonFile, writeJsonFile } from '../../utils/json-file-store.js'
|
import {
|
||||||
|
readAppConfigEntry,
|
||||||
|
saveAppConfigEntry,
|
||||||
|
} from '../config/app-config-store.js'
|
||||||
|
|
||||||
const SCHEDULED_JOBS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'scheduled-jobs.json')
|
const SCHEDULED_JOBS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'scheduled-jobs.json')
|
||||||
const CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health'
|
const CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health'
|
||||||
@@ -13,11 +17,21 @@ export function getScheduledJobsFilePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getScheduledJobsConfig() {
|
export function getScheduledJobsConfig() {
|
||||||
return loadScheduledJobsConfigFromFile()
|
return readAppConfigEntry({
|
||||||
|
configKey: APP_CONFIG_KEYS.scheduledJobs,
|
||||||
|
legacyFilePath: SCHEDULED_JOBS_FILE_PATH,
|
||||||
|
fallback: createDefaultScheduledJobsConfig,
|
||||||
|
normalize: normalizeScheduledJobsConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveScheduledJobsConfig(rawValue: unknown) {
|
export function saveScheduledJobsConfig(rawValue: unknown) {
|
||||||
return writeJsonFile(SCHEDULED_JOBS_FILE_PATH, rawValue, normalizeScheduledJobsConfig)
|
return saveAppConfigEntry({
|
||||||
|
configKey: APP_CONFIG_KEYS.scheduledJobs,
|
||||||
|
legacyFilePath: SCHEDULED_JOBS_FILE_PATH,
|
||||||
|
value: rawValue,
|
||||||
|
normalize: normalizeScheduledJobsConfig,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCloudtentaclesHealthJob(config: JsonObject = getScheduledJobsConfig()) {
|
export function getCloudtentaclesHealthJob(config: JsonObject = getScheduledJobsConfig()) {
|
||||||
@@ -26,14 +40,6 @@ export function getCloudtentaclesHealthJob(config: JsonObject = getScheduledJobs
|
|||||||
|| createDefaultCloudtentaclesHealthJob()
|
|| createDefaultCloudtentaclesHealthJob()
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadScheduledJobsConfigFromFile() {
|
|
||||||
return readJsonFile(
|
|
||||||
SCHEDULED_JOBS_FILE_PATH,
|
|
||||||
createDefaultScheduledJobsConfig,
|
|
||||||
normalizeScheduledJobsConfig,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeScheduledJobsConfig(rawValue: unknown) {
|
export function normalizeScheduledJobsConfig(rawValue: unknown) {
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
const rawJobs = Array.isArray(source.jobs) ? source.jobs : []
|
const rawJobs = Array.isArray(source.jobs) ? source.jobs : []
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { runDatabaseMigrations } from "../db/migrate.js";
|
import { runDatabaseMigrations } from "../db/migrate.js";
|
||||||
import { ensureAdminUsersBootstrapped } from "../services/admin/admin-auth-service.js";
|
import { ensureAdminUsersBootstrapped } from "../services/admin/admin-auth-service.js";
|
||||||
import { ensureFulfillmentCatalogBootstrapped } from "../services/bootstrap/fulfillment-bootstrap-service.js";
|
import { ensureFulfillmentCatalogBootstrapped } from "../services/bootstrap/fulfillment-bootstrap-service.js";
|
||||||
|
import { migrateJsonConfigFilesToDatabase } from "../services/config/json-config-migration-service.js";
|
||||||
import { startKuaishouIndustrySendCallbackRetryWorker } from "../services/platforms/kuaishou-industry/send-code-service.js";
|
import { startKuaishouIndustrySendCallbackRetryWorker } from "../services/platforms/kuaishou-industry/send-code-service.js";
|
||||||
import { startScheduledJobs } from "../services/scheduler/scheduler-service.js";
|
import { startScheduledJobs } from "../services/scheduler/scheduler-service.js";
|
||||||
import { logError, logInfo } from "../utils/logger.js";
|
import { logError, logInfo } from "../utils/logger.js";
|
||||||
@@ -30,6 +31,7 @@ export async function bootstrapCoreServices(
|
|||||||
});
|
});
|
||||||
|
|
||||||
await runDatabaseMigrations();
|
await runDatabaseMigrations();
|
||||||
|
await migrateJsonConfigFilesToDatabase();
|
||||||
await ensureFulfillmentCatalogBootstrapped();
|
await ensureFulfillmentCatalogBootstrapped();
|
||||||
await ensureAdminUsersBootstrapped();
|
await ensureAdminUsersBootstrapped();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user