diff --git a/jsconfig.json b/jsconfig.json index d9e3812..06440b0 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -10,6 +10,9 @@ ], "utils/*": [ "src/utils/*" + ], + "services/*": [ + "src/services/*" ] }, "module": "commonjs", diff --git a/package.json b/package.json index 881491d..20f4fe0 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "_moduleAliases": { "@": "./src", "db": "./src/db", - "utils": "./src/utils" + "utils": "./src/utils", + "services": "./src/services" } } \ No newline at end of file diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js new file mode 100644 index 0000000..be3cb2b --- /dev/null +++ b/src/controllers/JobController.js @@ -0,0 +1,41 @@ +// Job Controller 示例:如何调用 service 层动态控制和查询定时任务 +import JobService from "services/JobService.js" +import Router from "utils/router.js" + +class JobController { + static routes() { + const router = new Router({ prefix: "/api/jobs" }) + router.get("/", JobController.list) + router.post("/start/:id", JobController.start) + router.post("/stop/:id", JobController.stop) + router.post("/update/:id", JobController.updateCron) + return router + } + + static async list(ctx) { + ctx.body = JobService.listJobs() + } + + static async start(ctx) { + const { id } = ctx.params + JobService.startJob(id) + ctx.body = { success: true, message: `${id} 任务已启动` } + } + + static async stop(ctx) { + const { id } = ctx.params + JobService.stopJob(id) + ctx.body = { success: true, message: `${id} 任务已停止` } + } + + static async updateCron(ctx) { + const { id } = ctx.params + const { cronTime } = ctx.request.body + JobService.updateJobCron(id, cronTime) + ctx.body = { success: true, message: `${id} 任务频率已修改` } + } +} + +export default JobController + +// 你可以在路由中引入这些 controller 方法,实现接口调用 diff --git a/src/controllers/userController.js b/src/controllers/userController.js index 3f94791..91a9527 100644 --- a/src/controllers/userController.js +++ b/src/controllers/userController.js @@ -1,4 +1,4 @@ -import UserService from '@/services/userService.js'; +import UserService from 'services/UserService.js'; import Router from 'utils/router.js'; class UserController { diff --git a/src/jobs/exampleJob.js b/src/jobs/exampleJob.js index 085fb1e..c79fc8b 100644 --- a/src/jobs/exampleJob.js +++ b/src/jobs/exampleJob.js @@ -1,8 +1,11 @@ +import { jobLogger } from "@/logger"; + export default { - id: 'exampleJob', - cronTime: '*/5 * * * * *', // 每5秒执行一次 + id: 'example', + cronTime: '*/10 * * * * *', // 每10秒执行一次 task: () => { - console.log('定时任务执行:', new Date()); + jobLogger.info('Example Job 执行了'); }, - options: { scheduled: false } // 由调度器统一启动 + options: {}, + autoStart: false }; diff --git a/src/jobs/index.js b/src/jobs/index.js index 2a9e445..bf8006c 100644 --- a/src/jobs/index.js +++ b/src/jobs/index.js @@ -3,13 +3,46 @@ import path from 'path'; import scheduler from 'utils/scheduler.js'; const jobsDir = __dirname; +const jobModules = {}; fs.readdirSync(jobsDir).forEach(file => { if (file === 'index.js' || !file.endsWith('Job.js')) return; - const jobModule = require(path.join(jobsDir, file)).default; - if (jobModule && jobModule.id && jobModule.cronTime && typeof jobModule.task === 'function') { - scheduler.add(jobModule.cronTime, jobModule.task, jobModule.options, jobModule.id); + const jobModule = require(path.join(jobsDir, file)); + const job = jobModule.default || jobModule; + if (job && job.id && job.cronTime && typeof job.task === 'function') { + jobModules[job.id] = job; + scheduler.add(job.id, job.cronTime, job.task, job.options); + if (job.autoStart) scheduler.start(job.id); } }); -scheduler.startAll(); +function callHook(id, hookName) { + const job = jobModules[id]; + if (job && typeof job[hookName] === 'function') { + try { + job[hookName](); + } catch (e) { + console.error(`[Job:${id}] ${hookName} 执行异常:`, e); + } + } +} + +export default { + start: id => { + callHook(id, 'beforeStart'); + scheduler.start(id); + }, + stop: id => { + scheduler.stop(id); + callHook(id, 'afterStop'); + }, + updateCronTime: (id, cronTime) => scheduler.updateCronTime(id, cronTime), + list: () => scheduler.list(), + reload: id => { + const job = jobModules[id]; + if (job) { + scheduler.remove(id); + scheduler.add(job.id, job.cronTime, job.task, job.options); + } + } +}; diff --git a/src/logger.js b/src/logger.js index 1f6e4c9..00891ac 100644 --- a/src/logger.js +++ b/src/logger.js @@ -9,6 +9,10 @@ log4js.configure({ pattern: "-yyyy-MM-dd.log", alwaysIncludePattern: true, backups: 3, + layout: { + type: 'pattern', + pattern: '[%d{yyyy-MM-dd hh:mm:ss}] [%p] %m', + }, }, all: { type: "file", @@ -17,6 +21,10 @@ log4js.configure({ pattern: "-yyyy-MM-dd.log", alwaysIncludePattern: true, backups: 3, + layout: { + type: 'pattern', + pattern: '[%d{yyyy-MM-dd hh:mm:ss}] [%p] %m', + }, }, error: { type: "file", @@ -25,6 +33,10 @@ log4js.configure({ pattern: "-yyyy-MM-dd.log", alwaysIncludePattern: true, backups: 3, + layout: { + type: 'pattern', + pattern: '[%d{yyyy-MM-dd hh:mm:ss}] [%p] %m', + }, }, jobs: { type: "file", @@ -33,10 +45,17 @@ log4js.configure({ pattern: "-yyyy-MM-dd.log", alwaysIncludePattern: true, backups: 3, + layout: { + type: 'pattern', + pattern: '[%d{yyyy-MM-dd hh:mm:ss}] [%p] %m', + }, }, console: { type: "console", - layout: { type: "colored" }, + layout: { + type: "pattern", + pattern: '\x1b[36m[%d{yyyy-MM-dd hh:mm:ss}]\x1b[0m \x1b[1m[%p]\x1b[0m %m', + }, }, }, categories: { @@ -46,3 +65,8 @@ log4js.configure({ debug: { appenders: ["debug"], level: "debug" }, }, }) + +// 导出常用logger实例,便于直接引用 +export const logger = log4js.getLogger(); +export const jobLogger = log4js.getLogger('jobs'); +export const errorLogger = log4js.getLogger('error'); diff --git a/src/plugins/ResponseTime/index.js b/src/plugins/ResponseTime/index.js index a8e7443..2f01a63 100644 --- a/src/plugins/ResponseTime/index.js +++ b/src/plugins/ResponseTime/index.js @@ -1,30 +1,19 @@ import log4js from "log4js"; -// ANSI颜色代码常量定义 -const COLORS = { - REQ: '\x1b[36m', // 青色 - 请求标记 - RES: '\x1b[32m', // 绿色 - 响应标记 - TIME: '\x1b[33m', // 黄色 - 响应时间 - METHOD: '\x1b[1m', // 加粗 - HTTP方法 - RESET: '\x1b[0m' // 重置颜色 -}; - const logger = log4js.getLogger(); /** * 响应时间记录中间件 - * 为请求和响应添加彩色日志标记,并记录处理时间 * @param {Object} ctx - Koa上下文对象 * @param {Function} next - Koa中间件链函数 */ export default async (ctx, next) => { - // 彩色请求日志:青色标记 + 加粗方法名 - logger.debug(`${COLORS.REQ}::req::${COLORS.RESET} ${COLORS.METHOD}%s${COLORS.RESET} %s`, ctx.method, ctx.path); + logger.info("====================[REQ]===================="); + logger.info(`➡️ ${ctx.method} ${ctx.path}`); const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set("X-Response-Time", `${ms}ms`); - // 彩色响应日志:绿色标记 + 黄色响应时间 + 加粗方法名 - logger.debug(`${COLORS.RES}::res::${COLORS.RESET} takes ${COLORS.TIME}%s${COLORS.RESET} for ${COLORS.METHOD}%s${COLORS.RESET} %s`, - ctx.response.get("X-Response-Time"), ctx.method, ctx.url); + logger.info(`⬅️ ${ctx.method} ${ctx.url} | ⏱️ ${ms}ms`); + logger.info("====================[END]====================\n"); } diff --git a/src/services/JobService.js b/src/services/JobService.js new file mode 100644 index 0000000..1649aeb --- /dev/null +++ b/src/services/JobService.js @@ -0,0 +1,18 @@ +import jobs from '../jobs'; + +class JobService { + static startJob(id) { + return jobs.start(id); + } + static stopJob(id) { + return jobs.stop(id); + } + static updateJobCron(id, cronTime) { + return jobs.updateCronTime(id, cronTime); + } + static listJobs() { + return jobs.list(); + } +} + +export default JobService; \ No newline at end of file diff --git a/src/utils/scheduler.js b/src/utils/scheduler.js index d0a665e..27ea36f 100644 --- a/src/utils/scheduler.js +++ b/src/utils/scheduler.js @@ -2,30 +2,58 @@ import cron from 'node-cron'; class Scheduler { constructor() { - this.jobs = new Map(); // 用 Map 存储,key 为 id + this.jobs = new Map(); } - add(cronTime, task, options = {}, id) { - if (!id) throw new Error('定时任务必须有唯一 id'); - if (this.jobs.has(id)) { - console.warn(`定时任务 [${id}] 已存在,禁止重复注册。`); - return this.jobs.get(id); + add(id, cronTime, task, options = {}) { + if (this.jobs.has(id)) this.remove(id); + const job = cron.createTask(cronTime, task, { ...options, noOverlap: true }); + this.jobs.set(id, { job, cronTime, task, options, status: 'stopped' }); + } + + execute(id) { + const entry = this.jobs.get(id); + if (entry && entry.status === 'running') { + entry.job.execute(); + } + } + + start(id) { + const entry = this.jobs.get(id); + if (entry && entry.status !== 'running') { + entry.job.start(); + entry.status = 'running'; } - const job = cron.schedule(cronTime, task, options); - this.jobs.set(id, job); - return job; } - startAll() { - this.jobs.forEach(job => job.start()); + stop(id) { + const entry = this.jobs.get(id); + if (entry && entry.status === 'running') { + entry.job.stop(); + entry.status = 'stopped'; + } } - stopAll() { - this.jobs.forEach(job => job.stop()); + remove(id) { + const entry = this.jobs.get(id); + if (entry) { + entry.job.destroy(); + this.jobs.delete(id); + } + } + + updateCronTime(id, newCronTime) { + const entry = this.jobs.get(id); + if (entry) { + this.remove(id); + this.add(id, newCronTime, entry.task, entry.options); + } } - getJob(id) { - return this.jobs.get(id); + list() { + return Array.from(this.jobs.entries()).map(([id, { cronTime, status }]) => ({ + id, cronTime, status + })); } }