Files
foka-ci/apps/server/libs/route-scanner.ts
hurole 63c1e4df63 feat: 添加路由装饰器系统和全局异常处理
- 新增装饰器支持(@Get, @Post, @Put, @Delete, @Patch, @Controller)
- 实现路由自动注册机制(RouteScanner)
- 添加全局异常处理中间件(Exception)
- 实现统一响应体格式(ApiResponse)
- 新增请求体解析中间件(BodyParser)
- 重构控制器为类模式,支持装饰器路由
- 添加示例用户控制器(UserController)
- 更新TypeScript配置支持装饰器
- 添加reflect-metadata依赖
- 完善项目文档

Breaking Changes:
- 控制器现在返回数据而不是直接设置ctx.body
- 新增统一的API响应格式
2025-09-01 00:14:17 +08:00

167 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type Koa from 'koa';
import KoaRouter from '@koa/router';
import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts';
import { createAutoSuccessResponse } from '../middlewares/exception.ts';
/**
* 控制器类型
*/
export interface ControllerClass {
new (...args: any[]): any;
}
/**
* 路由扫描器,用于自动注册装饰器标注的路由
*/
export class RouteScanner {
private router: KoaRouter;
private controllers: ControllerClass[] = [];
constructor(prefix: string = '/api') {
this.router = new KoaRouter({ prefix });
}
/**
* 注册控制器类
*/
registerController(ControllerClass: ControllerClass): void {
this.controllers.push(ControllerClass);
this.scanController(ControllerClass);
}
/**
* 注册多个控制器类
*/
registerControllers(controllers: ControllerClass[]): void {
controllers.forEach(controller => this.registerController(controller));
}
/**
* 扫描控制器并注册路由
*/
private scanController(ControllerClass: ControllerClass): void {
// 创建控制器实例
const controllerInstance = new ControllerClass();
// 获取控制器的路由前缀
const controllerPrefix = getControllerPrefix(ControllerClass);
// 获取控制器的路由元数据
const routes: RouteMetadata[] = getRouteMetadata(ControllerClass);
// 注册每个路由
routes.forEach(route => {
const fullPath = this.buildFullPath(controllerPrefix, route.path);
const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey);
// 根据HTTP方法注册路由
switch (route.method) {
case 'GET':
this.router.get(fullPath, handler);
break;
case 'POST':
this.router.post(fullPath, handler);
break;
case 'PUT':
this.router.put(fullPath, handler);
break;
case 'DELETE':
this.router.delete(fullPath, handler);
break;
case 'PATCH':
this.router.patch(fullPath, handler);
break;
default:
console.warn(`未支持的HTTP方法: ${route.method}`);
}
console.log(`注册路由: ${route.method} ${fullPath} -> ${ControllerClass.name}.${route.propertyKey}`);
});
}
/**
* 构建完整的路由路径
*/
private buildFullPath(controllerPrefix: string, routePath: string): string {
// 清理和拼接路径
const cleanControllerPrefix = controllerPrefix.replace(/^\/+|\/+$/g, '');
const cleanRoutePath = routePath.replace(/^\/+|\/+$/g, '');
let fullPath = '';
if (cleanControllerPrefix) {
fullPath += '/' + cleanControllerPrefix;
}
if (cleanRoutePath) {
fullPath += '/' + cleanRoutePath;
}
// 如果路径为空,返回根路径
return fullPath || '/';
}
/**
* 包装控制器方法,统一处理响应格式
*/
private wrapControllerMethod(instance: any, methodName: string) {
return async (ctx: Koa.Context, next: Koa.Next) => {
try {
// 调用控制器方法
const method = instance[methodName];
if (typeof method !== 'function') {
throw new Error(`控制器方法 ${methodName} 不存在或不是函数`);
}
// 绑定this并调用方法
const result = await method.call(instance, ctx, next);
// 如果控制器返回了数据,则包装成统一响应格式
if (result !== undefined) {
ctx.body = createAutoSuccessResponse(result);
}
// 如果控制器没有返回数据,说明已经自己处理了响应
} catch (error) {
// 错误由全局异常处理中间件处理
throw error;
}
};
}
/**
* 获取Koa路由器实例用于应用到Koa应用中
*/
getRouter(): KoaRouter {
return this.router;
}
/**
* 应用路由到Koa应用
*/
applyToApp(app: Koa): void {
app.use(this.router.routes());
app.use(this.router.allowedMethods());
}
/**
* 获取已注册的路由信息(用于调试)
*/
getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> {
const routes: Array<{ method: string; path: string; controller: string; action: string }> = [];
this.controllers.forEach(ControllerClass => {
const controllerPrefix = getControllerPrefix(ControllerClass);
const routeMetadata = getRouteMetadata(ControllerClass);
routeMetadata.forEach(route => {
routes.push({
method: route.method,
path: this.buildFullPath(controllerPrefix, route.path),
controller: ControllerClass.name,
action: route.propertyKey
});
});
});
return routes;
}
}