Browse Source

feat: 添加作业控制器和服务,重构调度器,优化日志记录和响应时间中间件

alpha
谢亚昕 2 months ago
parent
commit
c073c46410
  1. 3
      jsconfig.json
  2. 3
      package.json
  3. 41
      src/controllers/JobController.js
  4. 2
      src/controllers/userController.js
  5. 11
      src/jobs/exampleJob.js
  6. 41
      src/jobs/index.js
  7. 26
      src/logger.js
  8. 19
      src/plugins/ResponseTime/index.js
  9. 18
      src/services/JobService.js
  10. 58
      src/utils/scheduler.js

3
jsconfig.json

@ -10,6 +10,9 @@
],
"utils/*": [
"src/utils/*"
],
"services/*": [
"src/services/*"
]
},
"module": "commonjs",

3
package.json

@ -26,6 +26,7 @@
"_moduleAliases": {
"@": "./src",
"db": "./src/db",
"utils": "./src/utils"
"utils": "./src/utils",
"services": "./src/services"
}
}

41
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 方法,实现接口调用

2
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 {

11
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
};

41
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);
}
}
};

26
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');

19
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");
}

18
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;

58
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
}));
}
}

Loading…
Cancel
Save