Files
foka-ci/apps/server/libs/route-scanner.ts
hurole d22fdc9618 feat: 实现环境变量预设功能 & 移除稀疏检出
## 后端改动
- 添加 Project.envPresets 字段(JSON 格式)
- 移除 Deployment.env 字段,统一使用 envVars
- 更新部署 DTO,支持 envVars (Record<string, string>)
- pipeline-runner 支持解析并注入 envVars 到环境
- 移除稀疏检出模板和相关环境变量
- 优化代码格式(Biome lint & format)

## 前端改动
- 新增 EnvPresetsEditor 组件(支持单选/多选/输入框类型)
- 项目创建/编辑界面集成环境预设编辑器
- 部署界面基于预设动态生成环境变量表单
- 移除稀疏检出表单项
- 项目详情页添加环境变量预设配置 tab
- 优化部署界面布局(基本参数 & 环境变量分区)

## 文档
- 添加完整文档目录结构(docs/)
- 创建设计文档 design-0005(部署流程重构)
- 添加 API 文档、架构设计文档等

## 数据库
- 执行 prisma db push 同步 schema 变更
2026-01-03 22:59:20 +08:00

173 lines
4.3 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 KoaRouter from '@koa/router';
import type Koa from 'koa';
import {
getControllerPrefix,
getRouteMetadata,
type RouteMetadata,
} from '../decorators/route.ts';
import { createSuccessResponse } 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}`);
}
});
}
/**
* 构建完整的路由路径
*/
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) => {
// 调用控制器方法
const method = instance[methodName];
if (typeof method !== 'function') {
ctx.throw(401, 'Not Found');
}
// 绑定this并调用方法
const result = (await method.call(instance, ctx, next)) ?? null;
ctx.body = createSuccessResponse(result);
};
}
/**
* 获取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;
}
}