From 838dbbd406427a3204d9c19670626b670b794c38 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Wed, 18 Jun 2025 01:37:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=99=A8=E3=80=81=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E8=B0=83=E5=BA=A6=E5=92=8C=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=E6=8F=92=E4=BB=B6=EF=BC=8C=E9=87=8D=E6=9E=84=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 73460 -> 73807 bytes package.json | 1 + src/controllers/StatusController.js | 22 ++++++++ src/controllers/userController.js | 26 ++++++++-- src/controllers/v1/statusController.js | 3 -- src/jobs/exampleJob.js | 8 +++ src/jobs/index.js | 15 ++++++ src/logger.js | 9 ++++ src/main.js | 89 +++++++++++---------------------- src/plugins/errorHandler/index.js | 38 ++++++++++++++ src/plugins/install.js | 6 ++- src/routes/apiRoutes.js | 9 ---- src/routes/v1Routes.js | 14 ------ src/services/userService.js | 39 +++++++++++++++ src/utils/autoRegister.js | 44 ++++++++++++++++ src/utils/scheduler.js | 32 ++++++++++++ 16 files changed, 262 insertions(+), 93 deletions(-) create mode 100644 src/controllers/StatusController.js delete mode 100644 src/controllers/v1/statusController.js create mode 100644 src/jobs/exampleJob.js create mode 100644 src/jobs/index.js create mode 100644 src/plugins/errorHandler/index.js delete mode 100644 src/routes/apiRoutes.js delete mode 100644 src/routes/v1Routes.js create mode 100644 src/services/userService.js create mode 100644 src/utils/autoRegister.js create mode 100644 src/utils/scheduler.js diff --git a/bun.lockb b/bun.lockb index 771d03bed36d95fd712a53975bc909cf3b5f8f5f..27399468c11ad9e9ae05dc61ce59fd25766f9c18 100644 GIT binary patch delta 9966 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 delta 10179 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 s-x)tXPX9!s)cQ#O$%f)Ht!P|*8!E5wP { + 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 diff --git a/src/controllers/userController.js b/src/controllers/userController.js index b97be5f..3f94791 100644 --- a/src/controllers/userController.js +++ b/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}`; -} \ No newline at end of file +export default UserController; \ No newline at end of file diff --git a/src/controllers/v1/statusController.js b/src/controllers/v1/statusController.js deleted file mode 100644 index 5c7a68d..0000000 --- a/src/controllers/v1/statusController.js +++ /dev/null @@ -1,3 +0,0 @@ -export async function status(ctx) { - ctx.body = 'OK'; -} \ No newline at end of file diff --git a/src/jobs/exampleJob.js b/src/jobs/exampleJob.js new file mode 100644 index 0000000..085fb1e --- /dev/null +++ b/src/jobs/exampleJob.js @@ -0,0 +1,8 @@ +export default { + id: 'exampleJob', + cronTime: '*/5 * * * * *', // 每5秒执行一次 + task: () => { + console.log('定时任务执行:', new Date()); + }, + options: { scheduled: false } // 由调度器统一启动 +}; diff --git a/src/jobs/index.js b/src/jobs/index.js new file mode 100644 index 0000000..2a9e445 --- /dev/null +++ b/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(); diff --git a/src/logger.js b/src/logger.js index 06927dc..1f6e4c9 100644 --- a/src/logger.js +++ b/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" }, diff --git a/src/main.js b/src/main.js index d6bbb04..b6f4296 100644 --- a/src/main.js +++ b/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 = ` - - ${status} Error - -

${status} Error

-

${message}

- - - `; - } 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 diff --git a/src/plugins/errorHandler/index.js b/src/plugins/errorHandler/index.js new file mode 100644 index 0000000..d643593 --- /dev/null +++ b/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 = ` + + ${status} Error + +

${status} Error

+

${message}

+ + + `; + } 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'); + } + }; +} diff --git a/src/plugins/install.js b/src/plugins/install.js index 7d3daf0..7ef8498 100644 --- a/src/plugins/install.js +++ b/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 { diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js deleted file mode 100644 index 5365f83..0000000 --- a/src/routes/apiRoutes.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/routes/v1Routes.js b/src/routes/v1Routes.js deleted file mode 100644 index 197e16f..0000000 --- a/src/routes/v1Routes.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/services/userService.js b/src/services/userService.js new file mode 100644 index 0000000..1bd0a77 --- /dev/null +++ b/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; diff --git a/src/utils/autoRegister.js b/src/utils/autoRegister.js new file mode 100644 index 0000000..4390aba --- /dev/null +++ b/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} [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()) + }) + })() +} diff --git a/src/utils/scheduler.js b/src/utils/scheduler.js new file mode 100644 index 0000000..d0a665e --- /dev/null +++ b/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();