Files
foka-ci/apps/server/middlewares/exception.ts
2025-09-06 12:38:02 +08:00

168 lines
4.1 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 { z } from 'zod';
import type { Middleware } from './types.ts';
import { log } from '../libs/logger.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 = '操作成功',
): ApiResponse<T> {
return {
code: 0,
message,
data,
timestamp: Date.now(),
};
}
/**
* 创建成功响应的辅助函数(自动设置消息)
*/
export function createAutoSuccessResponse<T>(data: T): ApiResponse<T> {
let message = '操作成功';
// 根据数据类型自动生成消息
if (Array.isArray(data)) {
message = `获取列表成功,共${data.length}条记录`;
} else if (data && typeof data === 'object') {
message = '获取数据成功';
} else if (data === null || data === undefined) {
message = '操作完成';
}
return createSuccessResponse(data, message);
}
/**
* 创建失败响应的辅助函数
*/
export function createErrorResponse(
code: number,
message: string,
data?: any,
): ApiResponse {
return {
code,
message,
data,
timestamp: Date.now(),
};
}