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) { |
import UserService from '@/services/userService.js'; |
||||
ctx.body = 'Hello World'; |
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) { |
export default UserController; |
||||
ctx.body = `User ID: ${ctx.params.id}`; |
|
||||
} |
|
@ -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 "./logger.js" |
||||
import os from 'os'; |
import "./jobs/index.js" |
||||
|
|
||||
|
// 第三方依赖
|
||||
|
import Koa from "koa" |
||||
|
import os from "os" |
||||
import log4js from "log4js" |
import log4js from "log4js" |
||||
|
|
||||
|
// 应用插件与自动路由
|
||||
import LoadPlugins from "./plugins/install.js" |
import LoadPlugins from "./plugins/install.js" |
||||
import apiRoutes from './routes/apiRoutes.js'; |
import { autoRegisterControllers } from "utils/autoRegister.js" |
||||
import v1Routes from './routes/v1Routes.js'; |
|
||||
|
|
||||
const logger = log4js.getLogger() |
const logger = log4js.getLogger() |
||||
|
const app = new Koa() |
||||
|
|
||||
const app = new Koa(); |
// 注册插件
|
||||
|
|
||||
LoadPlugins(app) |
LoadPlugins(app) |
||||
|
// 自动注册所有 controller
|
||||
|
autoRegisterControllers(app) |
||||
|
|
||||
// 错误响应格式化工具
|
const PORT = process.env.PORT || 3000 |
||||
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 server = app.listen(PORT, () => { |
const server = app.listen(PORT, () => { |
||||
const port = server.address().port |
const port = server.address().port |
||||
const getLocalIP = () => { |
// 获取本地 IP
|
||||
const interfaces = os.networkInterfaces() |
const getLocalIP = () => { |
||||
for (const name of Object.keys(interfaces)) { |
const interfaces = os.networkInterfaces() |
||||
for (const iface of interfaces[name]) { |
for (const name of Object.keys(interfaces)) { |
||||
if (iface.family === "IPv4" && !iface.internal) { |
for (const iface of interfaces[name]) { |
||||
return iface.address |
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