diff --git a/bun.lockb b/bun.lockb index 771d03b..2739946 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 05a95a7..881491d 100644 --- a/package.json +++ b/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" }, diff --git a/src/controllers/StatusController.js b/src/controllers/StatusController.js new file mode 100644 index 0000000..f6775ed --- /dev/null +++ b/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 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 = ` - -
${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 = ` + +${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