- 删除 fulfillment_mode 相关的旧同步/回填链路与定时任务 - 删除 cost_finance_sync、成本快照同步、人工商品归一等遗留服务 - 迁移 113 直接删除 order_items.fulfillment_mode 及其索引约束 - 断代脚本自带归档步骤,不再依赖独立回填脚本 - 退款/事实/总账统一走渠道版销售事实
434 lines
13 KiB
TypeScript
434 lines
13 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'
|
|
|
|
/** v2 管道作业:各自只负责一层,可单独启停。 */
|
|
const CHANNEL_ASSIGN_JOB_ID = 'channel-assign'
|
|
const CARRIER_DISPATCH_JOB_ID = 'carrier-dispatch'
|
|
const PRODUCT_NORMALIZE_JOB_ID = 'product-normalize'
|
|
const COST_SNAPSHOT_JOB_ID = 'cost-snapshot'
|
|
const SALES_FACTS_JOB_ID = 'sales-facts'
|
|
const LEDGER_POST_JOB_ID = 'ledger-post'
|
|
|
|
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 hasChannelAssign = jobs.some((item) => item.id === CHANNEL_ASSIGN_JOB_ID)
|
|
const hasCarrierDispatch = jobs.some((item) => item.id === CARRIER_DISPATCH_JOB_ID)
|
|
const hasProductNormalize = jobs.some((item) => item.id === PRODUCT_NORMALIZE_JOB_ID)
|
|
const hasCostSnapshot = jobs.some((item) => item.id === COST_SNAPSHOT_JOB_ID)
|
|
const hasSalesFacts = jobs.some((item) => item.id === SALES_FACTS_JOB_ID)
|
|
const hasLedgerPost = jobs.some((item) => item.id === LEDGER_POST_JOB_ID)
|
|
|
|
if (!hasWorkOrderTimeout) {
|
|
jobs.push(createDefaultWorkOrderTimeoutJob())
|
|
}
|
|
if (!hasDepositUnfreeze) {
|
|
jobs.push(createDefaultDepositUnfreezeJob())
|
|
}
|
|
if (!hasDataRetention) {
|
|
jobs.push(createDefaultDataRetentionJob())
|
|
}
|
|
if (!hasPddOrderSync) {
|
|
jobs.push(createDefaultPddOrderSyncJob())
|
|
}
|
|
// 旧作业 cost_finance_sync 已下线:v2 管道接管,且它写旧字段会覆盖新口径。
|
|
// 保留配置项仅用于兼容读取,运行期强制停用。
|
|
// v2 管道:老配置里没有这些任务时自动补齐,保证升级后无需手工添加。
|
|
if (!hasChannelAssign) jobs.push(createDefaultChannelAssignJob())
|
|
if (!hasCarrierDispatch) jobs.push(createDefaultCarrierDispatchJob())
|
|
if (!hasProductNormalize) jobs.push(createDefaultProductNormalizeJob())
|
|
if (!hasCostSnapshot) jobs.push(createDefaultCostSnapshotJob())
|
|
if (!hasSalesFacts) jobs.push(createDefaultSalesFactsJob())
|
|
if (!hasLedgerPost) jobs.push(createDefaultLedgerPostJob())
|
|
|
|
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 === 'channel_assign') {
|
|
return normalizeChannelAssignJob(rawValue)
|
|
}
|
|
if (type === 'carrier_dispatch') {
|
|
return normalizeCarrierDispatchJob(rawValue)
|
|
}
|
|
if (type === 'product_normalize') {
|
|
return normalizeProductNormalizeJob(rawValue)
|
|
}
|
|
if (type === 'cost_snapshot') {
|
|
return normalizeCostSnapshotJob(rawValue)
|
|
}
|
|
if (type === 'sales_facts') {
|
|
return normalizeSalesFactsJob(rawValue)
|
|
}
|
|
if (type === 'ledger_post') {
|
|
return normalizeLedgerPostJob(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 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(),
|
|
createDefaultChannelAssignJob(),
|
|
createDefaultCarrierDispatchJob(),
|
|
createDefaultProductNormalizeJob(),
|
|
createDefaultCostSnapshotJob(),
|
|
createDefaultSalesFactsJob(),
|
|
createDefaultLedgerPostJob(),
|
|
],
|
|
}
|
|
}
|
|
|
|
function normalizeChannelAssignJob(rawValue: JsonObject) {
|
|
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
|
|
return {
|
|
id: CHANNEL_ASSIGN_JOB_ID,
|
|
type: 'channel_assign',
|
|
enabled: rawValue.enabled !== false,
|
|
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 60, 30, 3600),
|
|
config: {
|
|
batchSize: normalizeRangeInteger(config.batchSize, 500, 50, 5000),
|
|
maxBatches: normalizeRangeInteger(config.maxBatches, 20, 1, 200),
|
|
},
|
|
}
|
|
}
|
|
|
|
function normalizeCarrierDispatchJob(rawValue: JsonObject) {
|
|
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
|
|
return {
|
|
id: CARRIER_DISPATCH_JOB_ID,
|
|
type: 'carrier_dispatch',
|
|
enabled: rawValue.enabled !== false,
|
|
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 60, 30, 3600),
|
|
config: {
|
|
limit: normalizeRangeInteger(config.limit, 200, 10, 2000),
|
|
},
|
|
}
|
|
}
|
|
|
|
function normalizeProductNormalizeJob(rawValue: JsonObject) {
|
|
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
|
|
return {
|
|
id: PRODUCT_NORMALIZE_JOB_ID,
|
|
type: 'product_normalize',
|
|
enabled: rawValue.enabled !== false,
|
|
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 300, 60, 3600),
|
|
config: {
|
|
batchSize: normalizeRangeInteger(config.batchSize, 500, 50, 5000),
|
|
maxBatches: normalizeRangeInteger(config.maxBatches, 20, 1, 200),
|
|
},
|
|
}
|
|
}
|
|
|
|
function normalizeCostSnapshotJob(rawValue: JsonObject) {
|
|
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
|
|
return {
|
|
id: COST_SNAPSHOT_JOB_ID,
|
|
type: 'cost_snapshot',
|
|
enabled: rawValue.enabled !== false,
|
|
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 300, 60, 3600),
|
|
config: {
|
|
batchSize: normalizeRangeInteger(config.batchSize, 500, 50, 5000),
|
|
maxBatches: normalizeRangeInteger(config.maxBatches, 20, 1, 200),
|
|
},
|
|
}
|
|
}
|
|
|
|
function normalizeSalesFactsJob(rawValue: JsonObject) {
|
|
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
|
|
return {
|
|
id: SALES_FACTS_JOB_ID,
|
|
type: 'sales_facts',
|
|
enabled: rawValue.enabled !== false,
|
|
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 600, 60, 3600),
|
|
config: {
|
|
days: normalizeRangeInteger(config.days, 3, 1, 60),
|
|
},
|
|
}
|
|
}
|
|
|
|
function normalizeLedgerPostJob(rawValue: JsonObject) {
|
|
const config = isPlainObject(rawValue.config) ? rawValue.config : {}
|
|
return {
|
|
id: LEDGER_POST_JOB_ID,
|
|
type: 'ledger_post',
|
|
enabled: rawValue.enabled !== false,
|
|
intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 600, 60, 3600),
|
|
config: {
|
|
days: normalizeRangeInteger(config.days, 3, 1, 60),
|
|
},
|
|
}
|
|
}
|
|
|
|
function createDefaultChannelAssignJob() {
|
|
return {
|
|
id: CHANNEL_ASSIGN_JOB_ID,
|
|
type: 'channel_assign',
|
|
enabled: true,
|
|
intervalSeconds: 60,
|
|
config: { batchSize: 500, maxBatches: 20 },
|
|
}
|
|
}
|
|
|
|
function createDefaultCarrierDispatchJob() {
|
|
return {
|
|
id: CARRIER_DISPATCH_JOB_ID,
|
|
type: 'carrier_dispatch',
|
|
enabled: true,
|
|
intervalSeconds: 60,
|
|
config: { limit: 200 },
|
|
}
|
|
}
|
|
|
|
function createDefaultProductNormalizeJob() {
|
|
return {
|
|
id: PRODUCT_NORMALIZE_JOB_ID,
|
|
type: 'product_normalize',
|
|
enabled: true,
|
|
intervalSeconds: 300,
|
|
config: { batchSize: 500, maxBatches: 20 },
|
|
}
|
|
}
|
|
|
|
function createDefaultCostSnapshotJob() {
|
|
return {
|
|
id: COST_SNAPSHOT_JOB_ID,
|
|
type: 'cost_snapshot',
|
|
enabled: true,
|
|
intervalSeconds: 300,
|
|
config: { batchSize: 500, maxBatches: 20 },
|
|
}
|
|
}
|
|
|
|
function createDefaultSalesFactsJob() {
|
|
return {
|
|
id: SALES_FACTS_JOB_ID,
|
|
type: 'sales_facts',
|
|
enabled: true,
|
|
intervalSeconds: 600,
|
|
config: { days: 3 },
|
|
}
|
|
}
|
|
|
|
function createDefaultLedgerPostJob() {
|
|
return {
|
|
id: LEDGER_POST_JOB_ID,
|
|
type: 'ledger_post',
|
|
enabled: true,
|
|
intervalSeconds: 600,
|
|
config: { days: 3 },
|
|
}
|
|
}
|
|
|
|
function createDefaultPddOrderSyncJob() {
|
|
return {
|
|
id: PDD_ORDER_SYNC_JOB_ID,
|
|
type: 'pdd_order_sync',
|
|
enabled: true,
|
|
intervalSeconds: 20,
|
|
config: {
|
|
pageSize: 10,
|
|
},
|
|
}
|
|
}
|
|
|
|
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]'
|
|
}
|