接单工单支持任务时限,超时自动判定失败并处置
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
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'
|
||||
|
||||
export function getScheduledJobsFilePath() {
|
||||
return SCHEDULED_JOBS_FILE_PATH
|
||||
@@ -39,17 +40,32 @@ export function getCloudtentaclesHealthJob(config: JsonObject = getScheduledJobs
|
||||
|| 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 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 ReturnType<typeof normalizeCloudtentaclesHealthJob> => Boolean(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)
|
||||
|
||||
if (!hasCloudtentaclesHealth) {
|
||||
jobs.push(createDefaultCloudtentaclesHealthJob())
|
||||
}
|
||||
if (!hasWorkOrderTimeout) {
|
||||
jobs.push(createDefaultWorkOrderTimeoutJob())
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
|
||||
@@ -63,11 +79,28 @@ function normalizeScheduledJob(rawValue: unknown) {
|
||||
}
|
||||
|
||||
const type = String(rawValue.type || '').trim()
|
||||
if (type !== 'cloudtentacles_health') {
|
||||
return null
|
||||
if (type === 'cloudtentacles_health') {
|
||||
return normalizeCloudtentaclesHealthJob(rawValue)
|
||||
}
|
||||
if (type === 'work_order_timeout') {
|
||||
return normalizeWorkOrderTimeoutJob(rawValue)
|
||||
}
|
||||
|
||||
return normalizeCloudtentaclesHealthJob(rawValue)
|
||||
return null
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -137,10 +170,23 @@ function createDefaultScheduledJobsConfig() {
|
||||
enabled: true,
|
||||
jobs: [
|
||||
createDefaultCloudtentaclesHealthJob(),
|
||||
createDefaultWorkOrderTimeoutJob(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -2,10 +2,11 @@ import { logError, logInfo, logWarn } from '../../utils/logger.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import {
|
||||
getCloudtentaclesHealthJob,
|
||||
getScheduledJobById,
|
||||
getScheduledJobsConfig,
|
||||
} from './config-service.js'
|
||||
import { runCloudtentaclesHealthJob } from './cloudtentacles-health-job.js'
|
||||
import { runWorkOrderTimeoutJob } from './work-order-timeout-job.js'
|
||||
|
||||
const timers = new Map<string, NodeJS.Timeout>()
|
||||
const jobStates = new Map<string, JsonObject>()
|
||||
@@ -67,14 +68,20 @@ export async function runScheduledJobNow(jobId: unknown) {
|
||||
|
||||
const result = await runJob(job, { manual: true })
|
||||
if (getScheduledJobsConfig().enabled !== false) {
|
||||
const latestJob = getCloudtentaclesHealthJob(getScheduledJobsConfig())
|
||||
if (latestJob.enabled === true) {
|
||||
scheduleJob(latestJob, Math.max(60, Number(latestJob.intervalSeconds || 300)) * 1000)
|
||||
}
|
||||
rescheduleJob(job)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function rescheduleJob(job: JsonObject) {
|
||||
const jobId = String(job.id || '').trim()
|
||||
if (!jobId) return
|
||||
const latestJob = getScheduledJobById(jobId)
|
||||
if (!latestJob || latestJob.enabled !== true) return
|
||||
const intervalMs = Math.max(60, Number(latestJob.intervalSeconds || 60)) * 1000
|
||||
scheduleJob(latestJob, intervalMs)
|
||||
}
|
||||
|
||||
function scheduleJob(job: JsonObject, delayMs: number) {
|
||||
const jobId = String(job.id || '').trim()
|
||||
if (!jobId) {
|
||||
@@ -90,10 +97,7 @@ function scheduleJob(job: JsonObject, delayMs: number) {
|
||||
const timer = setTimeout(() => {
|
||||
timers.delete(jobId)
|
||||
void runJob(job).finally(() => {
|
||||
const latestJob = getCloudtentaclesHealthJob(getScheduledJobsConfig())
|
||||
if (latestJob.enabled === true) {
|
||||
scheduleJob(latestJob, Math.max(60, Number(latestJob.intervalSeconds || 300)) * 1000)
|
||||
}
|
||||
rescheduleJob(job)
|
||||
})
|
||||
}, delayMs)
|
||||
|
||||
@@ -126,25 +130,27 @@ async function runJob(job: JsonObject, { manual = false }: { manual?: boolean }
|
||||
|
||||
try {
|
||||
const result = await dispatchJob(job)
|
||||
const summary =
|
||||
result && typeof result === 'object' ? (result as JsonObject) : {}
|
||||
updateJobState(jobId, {
|
||||
running: false,
|
||||
lastFinishedAt: new Date().toISOString(),
|
||||
lastStatus: result?.status || 'ok',
|
||||
lastMessage: result?.message || '执行完成',
|
||||
lastAsset: typeof result?.asset === 'number' ? result.asset : null,
|
||||
lastThreshold: typeof result?.threshold === 'number' ? result.threshold : null,
|
||||
lastAccounts: Array.isArray(result?.accounts) ? result.accounts : [],
|
||||
lastAccountCount: Number(result?.accountCount || 0),
|
||||
lastCheckedCount: Number(result?.checkedCount || 0),
|
||||
lastOkCount: Number(result?.okCount || 0),
|
||||
lastLowAssetCount: Number(result?.lowAssetCount || 0),
|
||||
lastFailedCount: Number(result?.failedCount || 0),
|
||||
lastStatus: String(summary.status || 'ok'),
|
||||
lastMessage: String(summary.message || '执行完成'),
|
||||
lastAsset: typeof summary.asset === 'number' ? summary.asset : null,
|
||||
lastThreshold: typeof summary.threshold === 'number' ? summary.threshold : null,
|
||||
lastAccounts: Array.isArray(summary.accounts) ? summary.accounts : [],
|
||||
lastAccountCount: Number(summary.accountCount || 0),
|
||||
lastCheckedCount: Number(summary.checkedCount || 0),
|
||||
lastOkCount: Number(summary.okCount || 0),
|
||||
lastLowAssetCount: Number(summary.lowAssetCount || 0),
|
||||
lastFailedCount: Number(summary.failedCount || 0),
|
||||
lastManual: manual,
|
||||
})
|
||||
logInfo('[scheduler]', '定时任务执行完成', {
|
||||
jobId,
|
||||
type: job.type,
|
||||
status: result?.status || 'ok',
|
||||
status: String(summary.status || 'ok'),
|
||||
})
|
||||
return result
|
||||
} catch (error) {
|
||||
@@ -173,6 +179,9 @@ function dispatchJob(job: JsonObject) {
|
||||
if (job.type === 'cloudtentacles_health') {
|
||||
return runCloudtentaclesHealthJob(job)
|
||||
}
|
||||
if (job.type === 'work_order_timeout') {
|
||||
return runWorkOrderTimeoutJob(job)
|
||||
}
|
||||
|
||||
throw createHttpError(`不支持的定时任务类型:${job.type}`, {
|
||||
statusCode: 400,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { logInfo } from '../../utils/logger.js'
|
||||
import { settleOverdueWorkOrders } from '../worker-platform/worker-service.js'
|
||||
|
||||
export async function runWorkOrderTimeoutJob(job: JsonObject) {
|
||||
const config =
|
||||
job.config && typeof job.config === 'object' && !Array.isArray(job.config)
|
||||
? (job.config as JsonObject)
|
||||
: {}
|
||||
const scanLimit = Math.max(1, Number(config.scanLimit || 50))
|
||||
|
||||
const result = await settleOverdueWorkOrders({ limit: scanLimit })
|
||||
|
||||
logInfo('[work-order-timeout]', '超时工单扫描完成', result)
|
||||
|
||||
return {
|
||||
ok: result.processedCount === result.checkedCount,
|
||||
status: 'ok',
|
||||
message: `扫描 ${result.checkedCount} 个,处置 ${result.processedCount} 个,跳过 ${result.skippedCount} 个`,
|
||||
checkedCount: result.checkedCount,
|
||||
failedCount: result.skippedCount,
|
||||
asset: result.processedCount,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user