## 后端改动 - 添加 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 变更
161 lines
3.6 KiB
TypeScript
161 lines
3.6 KiB
TypeScript
import type Koa from 'koa';
|
||
import { z } from 'zod';
|
||
import { log } from '../libs/logger.ts';
|
||
import type { Middleware } from './types.ts';
|
||
|
||
/**
|
||
* 统一响应体结构
|
||
*/
|
||
export interface ApiResponse<T = any> {
|
||
code: number; // 状态码:0表示成功,其他表示失败
|
||
message: string; // 响应消息
|
||
data?: T; // 响应数据
|
||
timestamp: number; // 时间戳
|
||
}
|
||
|
||
/**
|
||
* 自定义业务异常类
|
||
*/
|
||
export class BusinessError extends Error {
|
||
public code: number;
|
||
public httpStatus: number;
|
||
|
||
constructor(message: string, code = 1000, httpStatus = 400) {
|
||
super(message);
|
||
this.name = 'BusinessError';
|
||
this.code = code;
|
||
this.httpStatus = httpStatus;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 全局异常处理中间件
|
||
*/
|
||
export class Exception implements Middleware {
|
||
apply(app: Koa): void {
|
||
app.use(async (ctx, next) => {
|
||
try {
|
||
await next();
|
||
|
||
// 如果没有设置响应体,则返回404
|
||
if (ctx.status === 404) {
|
||
this.sendResponse(ctx, 404, 'Not Found', null, 404);
|
||
}
|
||
} catch (error) {
|
||
log.error('Exception', 'catch error: %o', error);
|
||
this.handleError(ctx, error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理错误
|
||
*/
|
||
private handleError(ctx: Koa.Context, error: any): void {
|
||
if (error instanceof z.ZodError) {
|
||
// Zod 参数验证错误
|
||
const firstError = error.issues[0];
|
||
const errorMessage = firstError?.message || '参数验证失败';
|
||
const fieldPath = firstError?.path?.join('.') || 'unknown';
|
||
|
||
log.info(
|
||
'Exception',
|
||
'Zod validation failed: %s at %s',
|
||
errorMessage,
|
||
fieldPath,
|
||
);
|
||
this.sendResponse(
|
||
ctx,
|
||
1003,
|
||
errorMessage,
|
||
{
|
||
field: fieldPath,
|
||
validationErrors: error.issues.map((issue) => ({
|
||
field: issue.path.join('.'),
|
||
message: issue.message,
|
||
code: issue.code,
|
||
})),
|
||
},
|
||
400,
|
||
);
|
||
} else if (error instanceof BusinessError) {
|
||
// 业务异常
|
||
this.sendResponse(ctx, error.code, error.message, null, error.httpStatus);
|
||
} else if (error.status) {
|
||
// Koa HTTP 错误
|
||
const message =
|
||
error.status === 401
|
||
? '未授权访问'
|
||
: error.status === 403
|
||
? '禁止访问'
|
||
: error.status === 404
|
||
? '资源不存在'
|
||
: error.status === 422
|
||
? '请求参数错误'
|
||
: error.message || '请求失败';
|
||
|
||
this.sendResponse(ctx, error.status, message, null, error.status);
|
||
} else {
|
||
// 系统异常
|
||
const isDev = process.env.NODE_ENV === 'development';
|
||
const message = isDev ? error.message : '服务器内部错误';
|
||
const data = isDev ? { stack: error.stack } : null;
|
||
|
||
this.sendResponse(ctx, 500, message, data, 500);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送统一响应
|
||
*/
|
||
private sendResponse(
|
||
ctx: Koa.Context,
|
||
code: number,
|
||
message: string,
|
||
data: any = null,
|
||
httpStatus = 200,
|
||
): void {
|
||
const response: ApiResponse = {
|
||
code,
|
||
message,
|
||
data,
|
||
timestamp: Date.now(),
|
||
};
|
||
|
||
ctx.status = httpStatus;
|
||
ctx.body = response;
|
||
ctx.type = 'application/json';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建成功响应的辅助函数
|
||
*/
|
||
export function createSuccessResponse<T>(
|
||
data: T,
|
||
message = 'success',
|
||
): ApiResponse<T> {
|
||
return {
|
||
code: 0,
|
||
message,
|
||
data,
|
||
timestamp: Date.now(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建失败响应的辅助函数
|
||
*/
|
||
export function createErrorResponse(
|
||
code: number,
|
||
message: string,
|
||
data?: any,
|
||
): ApiResponse {
|
||
return {
|
||
code,
|
||
message,
|
||
data,
|
||
timestamp: Date.now(),
|
||
};
|
||
}
|