import path from 'node:path' import type { JsonObject } from '../../types/json.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../config/runtime.js' import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js' const SCHEDULED_JOBS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'scheduled-jobs.json') const WORK_ORDER_TIMEOUT_JOB_ID = 'work-order-timeout' const DEPOSIT_UNFREEZE_JOB_ID = 'deposit-unfreeze' const DATA_RETENTION_JOB_ID = 'data-retention' const PDD_ORDER_SYNC_JOB_ID = 'pdd_order_sync' const COST_FINANCE_SYNC_JOB_ID = 'cost-finance-sync' export function getScheduledJobsFilePath() { return SCHEDULED_JOBS_FILE_PATH } export function getScheduledJobsConfig() { return readAppConfigEntry({ configKey: APP_CONFIG_KEYS.scheduledJobs, legacyFilePath: SCHEDULED_JOBS_FILE_PATH, fallback: createDefaultScheduledJobsConfig, normalize: normalizeScheduledJobsConfig, }) } export function saveScheduledJobsConfig(rawValue: unknown) { return saveAppConfigEntry({ configKey: APP_CONFIG_KEYS.scheduledJobs, legacyFilePath: SCHEDULED_JOBS_FILE_PATH, value: rawValue, normalize: normalizeScheduledJobsConfig, }) } export function getWorkOrderTimeoutJob(config: JsonObject = getScheduledJobsConfig()) { return ( (Array.isArray(config.jobs) ? config.jobs : []).find( (item) => String(item.id || '').trim() === WORK_ORDER_TIMEOUT_JOB_ID, ) || createDefaultWorkOrderTimeoutJob() ) } export function getDepositUnfreezeJob(config: JsonObject = getScheduledJobsConfig()) { return ( (Array.isArray(config.jobs) ? config.jobs : []).find( (item) => String(item.id || '').trim() === DEPOSIT_UNFREEZE_JOB_ID, ) || createDefaultDepositUnfreezeJob() ) } export function getPddOrderSyncJob(config: JsonObject = getScheduledJobsConfig()) { return ( (Array.isArray(config.jobs) ? config.jobs : []).find( (item) => String(item.id || '').trim() === PDD_ORDER_SYNC_JOB_ID, ) || createDefaultPddOrderSyncJob() ) } export function getScheduledJobById(jobId: unknown, config: JsonObject = getScheduledJobsConfig()) { return ( (Array.isArray(config.jobs) ? config.jobs : []).find( (item) => String(item.id || '').trim() === String(jobId || '').trim(), ) || null ) } export function normalizeScheduledJobsConfig(rawValue: unknown) { const source = isPlainObject(rawValue) ? rawValue : {} const rawJobs = Array.isArray(source.jobs) ? source.jobs : [] const jobs = rawJobs .map((item) => normalizeScheduledJob(item)) .filter((item): item is NonNullable> => Boolean(item)) const hasWorkOrderTimeout = jobs.some((item) => item.id === WORK_ORDER_TIMEOUT_JOB_ID) const hasDepositUnfreeze = jobs.some((item) => item.id === DEPOSIT_UNFREEZE_JOB_ID) const hasDataRetention = jobs.some((item) => item.id === DATA_RETENTION_JOB_ID) const hasPddOrderSync = jobs.some((item) => item.id === PDD_ORDER_SYNC_JOB_ID) const hasCostFinanceSync = jobs.some((item) => item.id === COST_FINANCE_SYNC_JOB_ID) if (!hasWorkOrderTimeout) { jobs.push(createDefaultWorkOrderTimeoutJob()) } if (!hasDepositUnfreeze) { jobs.push(createDefaultDepositUnfreezeJob()) } if (!hasDataRetention) { jobs.push(createDefaultDataRetentionJob()) } if (!hasPddOrderSync) { jobs.push(createDefaultPddOrderSyncJob()) } if (!hasCostFinanceSync) { jobs.push(createDefaultCostFinanceSyncJob()) } return { enabled: typeof source.enabled === 'boolean' ? source.enabled : true, jobs, } } function normalizeScheduledJob(rawValue: unknown) { if (!isPlainObject(rawValue)) { return null } const type = String(rawValue.type || '').trim() // 已下线的 cloudtentacles_health 配置直接丢弃 if (type === 'cloudtentacles_health') { return null } if (type === 'work_order_timeout') { return normalizeWorkOrderTimeoutJob(rawValue) } if (type === 'deposit_unfreeze') { return normalizeDepositUnfreezeJob(rawValue) } if (type === 'data_retention') { return normalizeDataRetentionJob(rawValue) } if (type === 'pdd_order_sync') { return normalizePddOrderSyncJob(rawValue) } if (type === 'cost_finance_sync') { return normalizeCostFinanceSyncJob(rawValue) } return null } function normalizePddOrderSyncJob(rawValue: JsonObject) { const config = isPlainObject(rawValue.config) ? rawValue.config : {} return { id: PDD_ORDER_SYNC_JOB_ID, type: 'pdd_order_sync', enabled: rawValue.enabled !== false, intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 20, 10, 3600), config: { pageSize: normalizeRangeInteger(config.pageSize, 10, 1, 50), }, } } function normalizeCostFinanceSyncJob(rawValue: JsonObject) { const config = isPlainObject(rawValue.config) ? rawValue.config : {} return { id: COST_FINANCE_SYNC_JOB_ID, type: 'cost_finance_sync', enabled: rawValue.enabled !== false, intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 300, 60, 3600), config: { days: normalizeRangeInteger(config.days, 7, 1, 90), }, } } function normalizeDepositUnfreezeJob(rawValue: JsonObject) { const config = isPlainObject(rawValue.config) ? rawValue.config : {} return { id: DEPOSIT_UNFREEZE_JOB_ID, type: 'deposit_unfreeze', enabled: rawValue.enabled !== false, intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 60, 30, 3600), config: { scanLimit: normalizeRangeInteger(config.scanLimit, 100, 1, 2000), }, } } function normalizeDataRetentionJob(rawValue: JsonObject) { return { id: DATA_RETENTION_JOB_ID, type: 'data_retention', enabled: rawValue.enabled !== false, intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 86400, 3600, 604800), config: {}, } } function normalizeWorkOrderTimeoutJob(rawValue: JsonObject) { const config = isPlainObject(rawValue.config) ? rawValue.config : {} return { id: WORK_ORDER_TIMEOUT_JOB_ID, type: 'work_order_timeout', enabled: rawValue.enabled !== false, intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 60, 30, 3600), config: { scanLimit: normalizeRangeInteger(config.scanLimit, 50, 1, 1000), }, } } function createDefaultScheduledJobsConfig() { return { enabled: true, jobs: [ createDefaultWorkOrderTimeoutJob(), createDefaultDepositUnfreezeJob(), createDefaultDataRetentionJob(), createDefaultPddOrderSyncJob(), createDefaultCostFinanceSyncJob(), ], } } function createDefaultPddOrderSyncJob() { return { id: PDD_ORDER_SYNC_JOB_ID, type: 'pdd_order_sync', enabled: true, intervalSeconds: 20, config: { pageSize: 10, }, } } function createDefaultCostFinanceSyncJob() { return { id: COST_FINANCE_SYNC_JOB_ID, type: 'cost_finance_sync', enabled: true, // 一次同步要跑 6 条全窗口聚合(41 万单实测 30–60 秒)。5 分钟一次会让 // 数据库长期处在 10–20% 的同步负载下,后台页面首屏被拖慢;财务事实 // 15 分钟刷新已足够,需要更快可在后台定时任务里改回 300。 intervalSeconds: 900, config: { days: 7, }, } } function createDefaultDataRetentionJob() { return { id: DATA_RETENTION_JOB_ID, type: 'data_retention', enabled: true, intervalSeconds: 86400, config: {}, } } function createDefaultDepositUnfreezeJob() { return { id: DEPOSIT_UNFREEZE_JOB_ID, type: 'deposit_unfreeze', enabled: true, intervalSeconds: 60, config: { scanLimit: 100, }, } } function createDefaultWorkOrderTimeoutJob() { return { id: WORK_ORDER_TIMEOUT_JOB_ID, type: 'work_order_timeout', enabled: true, intervalSeconds: 60, config: { scanLimit: 50, }, } } function normalizeRangeInteger(value: unknown, fallback: number, min: number, max: number) { const parsed = Number(value) if (!Number.isInteger(parsed)) { return fallback } return Math.min(max, Math.max(min, parsed)) } function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' }