16 changed files with 262 additions and 93 deletions
Binary file not shown.
@ -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 |
@ -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; |
@ -1,3 +0,0 @@ |
|||
export async function status(ctx) { |
|||
ctx.body = 'OK'; |
|||
} |
@ -0,0 +1,8 @@ |
|||
export default { |
|||
id: 'exampleJob', |
|||
cronTime: '*/5 * * * * *', // 每5秒执行一次
|
|||
task: () => { |
|||
console.log('定时任务执行:', new Date()); |
|||
}, |
|||
options: { scheduled: false } // 由调度器统一启动
|
|||
}; |
@ -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(); |
@ -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 |
|||
|
@ -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'); |
|||
} |
|||
}; |
|||
} |
@ -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; |
@ -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; |
@ -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; |
@ -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()) |
|||
}) |
|||
})() |
|||
} |
@ -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…
Reference in new issue