Files
order_site/apps/backend/src/services/scheduler/config-service.ts
T

264 lines
8.0 KiB
TypeScript

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 CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health'
const WORK_ORDER_TIMEOUT_JOB_ID = 'work-order-timeout'
const DEPOSIT_UNFREEZE_JOB_ID = 'deposit-unfreeze'
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 getCloudtentaclesHealthJob(config: JsonObject = getScheduledJobsConfig()) {
return (Array.isArray(config.jobs) ? config.jobs : [])
.find((item) => String(item.id || '').trim() === CLOUDTENTACLES_HEALTH_JOB_ID)
|| createDefaultCloudtentaclesHealthJob()
}
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 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<ReturnType<typeof normalizeScheduledJob>> => Boolean(item))
const hasCloudtentaclesHealth = jobs.some((item) => item.id === CLOUDTENTACLES_HEALTH_JOB_ID)
const hasWorkOrderTimeout = jobs.some((item) => item.id === WORK_ORDER_TIMEOUT_JOB_ID)
const hasDepositUnfreeze = jobs.some((item) => item.id === DEPOSIT_UNFREEZE_JOB_ID)
if (!hasCloudtentaclesHealth) {
jobs.push(createDefaultCloudtentaclesHealthJob())
}
if (!hasWorkOrderTimeout) {
jobs.push(createDefaultWorkOrderTimeoutJob())
}
if (!hasDepositUnfreeze) {
jobs.push(createDefaultDepositUnfreezeJob())
}
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()
if (type === 'cloudtentacles_health') {
return normalizeCloudtentaclesHealthJob(rawValue)
}
if (type === 'work_order_timeout') {
return normalizeWorkOrderTimeoutJob(rawValue)
}
if (type === 'deposit_unfreeze') {
return normalizeDepositUnfreezeJob(rawValue)
}
return null
}
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 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 normalizeCloudtentaclesHealthJob(rawValue: JsonObject) {
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
const defaultAssetThreshold = normalizeRangeInteger(config.assetThreshold, 500, 0, 999999)
return {
id: CLOUDTENTACLES_HEALTH_JOB_ID,
type: 'cloudtentacles_health',
enabled: rawValue.enabled === true,
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 300, 60, 86400),
cooldownSeconds: normalizeRangeInteger(rawValue.cooldownSeconds, 1800, 60, 86400),
config: {
assetThreshold: defaultAssetThreshold,
accounts: normalizeCloudtentaclesHealthAccounts(config, defaultAssetThreshold),
},
}
}
function normalizeCloudtentaclesHealthAccounts(config: JsonObject, defaultAssetThreshold: number) {
const rawAccounts = Array.isArray(config.accounts)
? config.accounts
: Array.isArray(config.sourceKeys)
? config.sourceKeys.map((sourceKey) => ({ sourceKey }))
: []
const accounts = rawAccounts
.map((item) => normalizeCloudtentaclesHealthAccount(item, defaultAssetThreshold))
.filter(Boolean) as ReturnType<typeof normalizeCloudtentaclesHealthAccount>[]
if (accounts.length === 0) {
accounts.push(normalizeCloudtentaclesHealthAccount({
sourceKey: config.sourceKey || 'default',
assetThreshold: defaultAssetThreshold,
}, defaultAssetThreshold))
}
const seen = new Set<string>()
return accounts.filter((item) => {
const sourceKey = String(item?.sourceKey || '').trim()
if (!sourceKey || seen.has(sourceKey)) {
return false
}
seen.add(sourceKey)
return true
})
}
function normalizeCloudtentaclesHealthAccount(rawValue: unknown, defaultAssetThreshold: number) {
const source = isPlainObject(rawValue) ? rawValue : {}
const sourceKey = String(source.sourceKey || source.key || '').trim() || 'default'
return {
sourceKey,
label: String(source.label || '').trim(),
enabled: source.enabled !== false,
assetThreshold: normalizeRangeInteger(
source.assetThreshold ?? source.threshold,
defaultAssetThreshold,
0,
999999,
),
}
}
function createDefaultScheduledJobsConfig() {
return {
enabled: true,
jobs: [
createDefaultCloudtentaclesHealthJob(),
createDefaultWorkOrderTimeoutJob(),
createDefaultDepositUnfreezeJob(),
],
}
}
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 createDefaultCloudtentaclesHealthJob() {
return {
id: CLOUDTENTACLES_HEALTH_JOB_ID,
type: 'cloudtentacles_health',
enabled: false,
intervalSeconds: 300,
cooldownSeconds: 1800,
config: {
assetThreshold: 500,
accounts: [
{
sourceKey: 'default',
label: '默认账号',
enabled: true,
assetThreshold: 500,
},
],
},
}
}
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]'
}