Browse Source

feat: 添加用户控制器、定时任务调度和错误处理插件,重构路由系统

alpha
npmrun 2 months ago
parent
commit
838dbbd406
  1. BIN
      bun.lockb
  2. 1
      package.json
  3. 22
      src/controllers/StatusController.js
  4. 26
      src/controllers/userController.js
  5. 3
      src/controllers/v1/statusController.js
  6. 8
      src/jobs/exampleJob.js
  7. 15
      src/jobs/index.js
  8. 9
      src/logger.js
  9. 89
      src/main.js
  10. 38
      src/plugins/errorHandler/index.js
  11. 6
      src/plugins/install.js
  12. 9
      src/routes/apiRoutes.js
  13. 14
      src/routes/v1Routes.js
  14. 39
      src/services/userService.js
  15. 44
      src/utils/autoRegister.js
  16. 32
      src/utils/scheduler.js

BIN
bun.lockb

Binary file not shown.

1
package.json

@ -19,6 +19,7 @@
"koa": "^3.0.0",
"log4js": "^6.9.1",
"module-alias": "^2.2.3",
"node-cron": "^4.1.0",
"path-to-regexp": "^8.2.0",
"sqlite3": "^5.1.7"
},

22
src/controllers/StatusController.js

@ -0,0 +1,22 @@
import Router from 'utils/router.js';
class StatusController {
static routes() {
const v1 = new Router({ prefix: "/api/v1" })
// 组内中间件
v1.use((ctx, next) => {
ctx.set("X-API-Version", "v1")
return next()
})
v1.get("/status", StatusController.status)
return v1
}
static async status(ctx) {
ctx.body = "OK"
}
}
export default StatusController

26
src/controllers/userController.js

@ -1,7 +1,23 @@
export async function hello(ctx) {
ctx.body = 'Hello World';
import UserService from '@/services/userService.js';
import Router from 'utils/router.js';
class UserController {
static routes() {
let router = new Router({ prefix: '/api' });
router.get('/hello', UserController.hello);
router.get('/user/:id', UserController.getUser);
return router;
}
static async hello(ctx) {
ctx.body = 'Hello World';
}
static async getUser(ctx) {
// 调用 service 层获取用户
const user = await UserService.getUserById(ctx.params.id);
ctx.body = user;
}
}
export async function getUser(ctx) {
ctx.body = `User ID: ${ctx.params.id}`;
}
export default UserController;

3
src/controllers/v1/statusController.js

@ -1,3 +0,0 @@
export async function status(ctx) {
ctx.body = 'OK';
}

8
src/jobs/exampleJob.js

@ -0,0 +1,8 @@
export default {
id: 'exampleJob',
cronTime: '*/5 * * * * *', // 每5秒执行一次
task: () => {
console.log('定时任务执行:', new Date());
},
options: { scheduled: false } // 由调度器统一启动
};

15
src/jobs/index.js

@ -0,0 +1,15 @@
import fs from 'fs';
import path from 'path';
import scheduler from 'utils/scheduler.js';
const jobsDir = __dirname;
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);
}
});
scheduler.startAll();

9
src/logger.js

@ -26,12 +26,21 @@ log4js.configure({
alwaysIncludePattern: true,
backups: 3,
},
jobs: {
type: "file",
filename: "logs/jobs.log",
maxLogSize: 102400,
pattern: "-yyyy-MM-dd.log",
alwaysIncludePattern: true,
backups: 3,
},
console: {
type: "console",
layout: { type: "colored" },
},
},
categories: {
jobs: { appenders: ["console", "jobs"], level: "ALL" },
error: { appenders: ["console", "error"], level: "error" },
default: { appenders: ["console", "all"], level: "ALL" },
debug: { appenders: ["debug"], level: "debug" },

89
src/main.js

@ -1,73 +1,42 @@
import './logger.js';
import Koa from 'koa';
import os from 'os';
// 日志、全局插件、定时任务等基础设施
import "./logger.js"
import "./jobs/index.js"
// 第三方依赖
import Koa from "koa"
import os from "os"
import log4js from "log4js"
// 应用插件与自动路由
import LoadPlugins from "./plugins/install.js"
import apiRoutes from './routes/apiRoutes.js';
import v1Routes from './routes/v1Routes.js';
import { autoRegisterControllers } from "utils/autoRegister.js"
const logger = log4js.getLogger()
const app = new Koa()
const app = new Koa();
// 注册插件
LoadPlugins(app)
// 自动注册所有 controller
autoRegisterControllers(app)
// 错误响应格式化工具
function formatError(ctx, status, message) {
const accept = ctx.accepts('json', 'html', 'text');
if (accept === 'json') {
ctx.type = 'application/json';
ctx.body = { success: false, error: message };
} else if (accept === 'html') {
ctx.type = 'html';
ctx.body = `
<html>
<head><title>${status} Error</title></head>
<body>
<h1>${status} Error</h1>
<p>${message}</p>
</body>
</html>
`;
} else {
ctx.type = 'text';
ctx.body = `${status} - ${message}`;
}
ctx.status = status;
}
// 错误处理中间
app.use(async (ctx, next) => {
try {
await next();
if (ctx.status === 404) {
formatError(ctx, 404, 'Resource not found');
}
} catch (err) {
formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error');
}
});
app.use(apiRoutes.middleware());
app.use(v1Routes.middleware());
const PORT = process.env.PORT || 3000;
const PORT = process.env.PORT || 3000
const server = app.listen(PORT, () => {
const port = server.address().port
const getLocalIP = () => {
const interfaces = os.networkInterfaces()
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address
const port = server.address().port
// 获取本地 IP
const getLocalIP = () => {
const interfaces = os.networkInterfaces()
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address
}
}
}
}
return "localhost"
}
return "localhost"
}
const localIP = getLocalIP()
logger.trace(`服务器运行在: http://${localIP}:${port}`)
const localIP = getLocalIP()
logger.trace(`服务器运行在: http://${localIP}:${port}`)
})
export default app;
export default app

38
src/plugins/errorHandler/index.js

@ -0,0 +1,38 @@
// src/plugins/errorHandler.js
// 错误处理中间件插件
function formatError(ctx, status, message) {
const accept = ctx.accepts('json', 'html', 'text');
if (accept === 'json') {
ctx.type = 'application/json';
ctx.body = { success: false, error: message };
} else if (accept === 'html') {
ctx.type = 'html';
ctx.body = `
<html>
<head><title>${status} Error</title></head>
<body>
<h1>${status} Error</h1>
<p>${message}</p>
</body>
</html>
`;
} else {
ctx.type = 'text';
ctx.body = `${status} - ${message}`;
}
ctx.status = status;
}
export default function errorHandler() {
return async (ctx, next) => {
try {
await next();
if (ctx.status === 404) {
formatError(ctx, 404, 'Resource not found');
}
} catch (err) {
formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error');
}
};
}

6
src/plugins/install.js

@ -1,14 +1,16 @@
import ResponseTime from "./ResponseTime";
import Send from "./Send/index.js";
import Send from "./Send";
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import path from "path"
import path from "path";
import errorHandler from './errorHandler';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const publicPath = resolve(__dirname, '../../public');
export default (app)=>{
app.use(errorHandler());
app.use(ResponseTime)
app.use(async (ctx, next) => {
try {

9
src/routes/apiRoutes.js

@ -1,9 +0,0 @@
import Router from '../utils/router.js';
import { hello, getUser } from '../controllers/userController.js';
const router = new Router({ prefix: '/api' });
router.get('/hello', hello);
router.get('/user/:id', getUser);
export default router;

14
src/routes/v1Routes.js

@ -1,14 +0,0 @@
import Router from '../utils/router.js';
import { status } from '../controllers/v1/statusController.js';
const v1 = new Router({ prefix: '/api/v1' });
// 组内中间件
v1.use((ctx, next) => {
ctx.set('X-API-Version', 'v1');
return next();
});
v1.get('/status', status);
export default v1;

39
src/services/userService.js

@ -0,0 +1,39 @@
// src/services/userService.js
// 用户相关业务逻辑
import UserModel from 'db/models/UserModel.js';
class UserService {
static async getUserById(id) {
// 这里可以调用数据库模型
// 示例返回
return { id, name: `User_${id}` };
}
// 获取所有用户
static async getAllUsers() {
return await UserModel.findAll();
}
// 创建新用户
static async createUser(data) {
if (!data.name) throw new Error('用户名不能为空');
return await UserModel.create(data);
}
// 更新用户
static async updateUser(id, data) {
const user = await UserModel.findById(id);
if (!user) throw new Error('用户不存在');
return await UserModel.update(id, data);
}
// 删除用户
static async deleteUser(id) {
const user = await UserModel.findById(id);
if (!user) throw new Error('用户不存在');
return await UserModel.delete(id);
}
}
export default UserService;

44
src/utils/autoRegister.js

@ -0,0 +1,44 @@
// 自动扫描 controllers 目录并注册路由
// 兼容传统 routes 方式和自动注册 controller 方式
import fs from "fs"
import path from "path"
/**
* 自动扫描 controllers 目录注册所有导出的路由
* 自动检测 routes 目录下已手动注册的 controller避免重复注册
* @param {Koa} app - Koa 实例
* @param {string} controllersDir - controllers 目录路径
* @param {string} prefix - 路由前缀
* @param {Set<string>} [manualControllers] - 可选手动传入已注册 controller 文件名集合优先于自动扫描
*/
export function autoRegisterControllers(app, controllersDir = path.resolve(__dirname, "../controllers")) {
let allRouter = []
async function scan(dir, routePrefix = "") {
for (const file of fs.readdirSync(dir)) {
const fullPath = path.join(dir, file)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
await scan(fullPath, routePrefix + "/" + file)
} else if (file.endsWith("Controller.js")) {
let controller
try {
controller = require(fullPath)
} catch (e) {
controller = (await import(fullPath)).default
}
const routes = controller.routes || controller.default?.routes || controller.default || controller
// 判断 routes 方法参数个数,支持自动适配
if (typeof routes === "function") {
allRouter.push(routes())
}
}
}
}
;(async () => {
await scan(controllersDir)
allRouter.forEach(router => {
app.use(router.middleware())
})
})()
}

32
src/utils/scheduler.js

@ -0,0 +1,32 @@
import cron from 'node-cron';
class Scheduler {
constructor() {
this.jobs = new Map(); // 用 Map 存储,key 为 id
}
add(cronTime, task, options = {}, id) {
if (!id) throw new Error('定时任务必须有唯一 id');
if (this.jobs.has(id)) {
console.warn(`定时任务 [${id}] 已存在,禁止重复注册。`);
return this.jobs.get(id);
}
const job = cron.schedule(cronTime, task, options);
this.jobs.set(id, job);
return job;
}
startAll() {
this.jobs.forEach(job => job.start());
}
stopAll() {
this.jobs.forEach(job => job.stop());
}
getJob(id) {
return this.jobs.get(id);
}
}
export default new Scheduler();
Loading…
Cancel
Save