Files
order_site/apps/backend/src/services/scheduler/config-service.ts
T
yml2213 915ce017f3 perf(finance): 消除定时同步与后台页面的重查询
- 新增 order_items.manual_sku_key 生成列(迁移 106),统一内部 SKU 口径(skuNick → outSkuSn → sku_code → sku_name,去空格)
- 人工渠道匹配 8.4s → 62ms;人工商品归一写回 7.0s → 307ms
- 销售事实重算改为物化订单集合 + 预聚合成本/退款,6.9s → 0.5s 内
- 成本异常写入去掉逐项 LATERAL 工单探测,收口按最近 7 天窗口,7–8s → 0.5s 内
- 销售毛利订单统计去掉重复 NOT EXISTS,9.4s → 0.4s
- 接单平台店铺下拉改 EXISTS 半连接,3.1s → 0.3s
- cost-finance-sync 默认间隔 5 分钟 → 15 分钟;完整同步 51.2s → 34.9s
2026-10-28 13:41:11 +08:00

284 lines
8.2 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 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<ReturnType<typeof normalizeScheduledJob>> => 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]'
}