feat: 增加日志打印和异常处理

This commit is contained in:
mazxd
2025-05-11 17:55:59 +08:00
parent e43ffe011f
commit fd8b4ec01b
21 changed files with 939 additions and 119 deletions
+10
View File
@@ -0,0 +1,10 @@
---
description:
globs:
alwaysApply: true
---
# 通用规则
1. 如果需要使用npm安装任何包,请使用pnpm来代替
2. Windows环境下请注意Powershell 不支持 && 连续命令,请自行分开命令
3. 写业务类的时候自动使用logger打印日志,并且日志用中文显示。
+1 -1
View File
@@ -24,7 +24,7 @@
"redis": "^5.0.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.11.0"
"winston": "^3.17.0"
},
"devDependencies": {
"@types/cors": "^2.8.0",
+1 -1
View File
@@ -28,7 +28,7 @@
"redis": "^5.0.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.11.0"
"winston": "^3.17.0"
},
"devDependencies": {
"@types/cors": "^2.8.0",
@@ -9,6 +9,7 @@ export class BaiduDriveAdapter implements DriveAdapter {
account: Account
): Promise<TransferResult> {
try {
logger.info('Baidu drive transferAndShare', { sourceLink, sourceLinkType, accountId: account.id });
// TODO: 实现百度网盘的具体转存和分享逻辑
// 1. 解析账号凭证
const credentials = JSON.parse(account.credentials);
+3 -1
View File
@@ -86,7 +86,7 @@ export async function getSearchResults(req: Request, res: Response) {
// 转换参数类型
const searchParams = {
keyword: keyword as string,
types: Array.isArray(types) ? types : types ? [types as string] : [],
types: Array.isArray(types) ? types.map(t => String(t)) : types ? [String(types)] : [],
page: Number(page),
fileType: fileType as string,
fileSize: fileSize as string,
@@ -94,6 +94,7 @@ export async function getSearchResults(req: Request, res: Response) {
mode: mode as 'standard' | 'fuzzy'
};
const clientIp = req.ip || req.socket.remoteAddress;
const result = await search(searchParams);
res.json(result);
} catch (error) {
@@ -158,6 +159,7 @@ export async function getLinkForResource(req: Request, res: Response) {
});
}
const clientIp = req.ip || req.socket.remoteAddress;
const result = await generateLink(resourceId);
res.json(result);
} catch (error) {
+49
View File
@@ -0,0 +1,49 @@
import { AppError } from '../utils/AppError';
import logger from '../lib/logger';
import { AxiosError } from 'axios';
export function handleError(errorMessage?: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error: unknown) {
// 记录错误日志
logger.error(`${errorMessage || '操作执行失败'}:`, {
method: propertyKey,
args,
error: error instanceof Error ? error.message : '未知错误',
stack: error instanceof Error ? error.stack : undefined
});
// 如果是 AppError,直接抛出
if (error instanceof AppError) {
throw error;
}
// 如果是 Axios 错误,转换为 AppError
if (error instanceof AxiosError) {
const statusCode = error.response?.status || 500;
throw new AppError(
error.response?.data?.message || '请求外部服务失败',
statusCode
);
}
// 其他错误转换为 AppError
throw new AppError(
error instanceof Error ? error.message : '操作执行失败',
500
);
}
};
return descriptor;
};
}
+67
View File
@@ -0,0 +1,67 @@
import winston from 'winston';
import path from 'path';
// 定义日志级别
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
// 根据环境选择日志级别
const level = () => {
const env = process.env.NODE_ENV || 'development';
const isDevelopment = env === 'development';
return isDevelopment ? 'debug' : 'warn';
};
// 定义日志颜色
const colors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'white',
};
// 添加颜色
winston.addColors(colors);
// 定义日志格式
const format = winston.format.combine(
// 添加时间戳
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
// 添加颜色
winston.format.colorize({ all: true }),
// 定义日志打印格式
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
),
);
// 定义日志输出目标
const transports = [
// 控制台输出
new winston.transports.Console(),
// 错误日志文件
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
}),
// 所有日志文件
new winston.transports.File({
filename: path.join('logs', 'all.log')
}),
];
// 创建日志记录器
const logger = winston.createLogger({
level: level(),
levels,
format,
transports,
});
export default logger;
+55
View File
@@ -0,0 +1,55 @@
import { PrismaClient, Prisma } from '@prisma/client';
import logger from './logger';
declare global {
var prisma: PrismaClient | undefined;
}
export const prisma = global.prisma || new PrismaClient({
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'event',
level: 'error',
},
{
emit: 'event',
level: 'info',
},
{
emit: 'event',
level: 'warn',
},
],
});
// 监听 SQL 查询
(prisma as any).$on('query', (e: Prisma.QueryEvent) => {
logger.debug('SQL Query', {
query: e.query,
params: e.params,
duration: e.duration,
});
});
// 监听错误
(prisma as any).$on('error', (e: Prisma.LogEvent) => {
logger.error('Prisma Error', { error: e });
});
// 监听信息
(prisma as any).$on('info', (e: Prisma.LogEvent) => {
logger.info('Prisma Info', { message: e });
});
// 监听警告
(prisma as any).$on('warn', (e: Prisma.LogEvent) => {
logger.warn('Prisma Warning', { message: e });
});
if (process.env.NODE_ENV !== 'production') {
global.prisma = prisma;
}
+48
View File
@@ -0,0 +1,48 @@
import { Request, Response, NextFunction } from 'express';
import { ActivityLogService } from '../services/activityLogService';
export interface LoggedRequest extends Request {
startTime?: number;
}
export function activityLogger(operationType: string) {
return async (req: LoggedRequest, res: Response, next: NextFunction) => {
// 记录开始时间
req.startTime = Date.now();
// 保存原始的 res.json 方法
const originalJson = res.json;
// 重写 res.json 方法
res.json = function (body: any) {
// 计算请求持续时间
const durationMs = req.startTime ? Date.now() - req.startTime : undefined;
// 异步记录日志,不等待完成
ActivityLogService.logActivity({
operationType,
clientIp: req.ip || req.socket.remoteAddress,
keyword: req.query.keyword as string,
itemId: req.params.resourceId,
details: JSON.stringify({
method: req.method,
path: req.path,
query: req.query,
params: req.params,
statusCode: res.statusCode,
responseTime: durationMs
}),
durationMs,
errorCode: res.statusCode >= 400 ? `HTTP_${res.statusCode}` : undefined,
errorMessage: res.statusCode >= 400 ? body.message : undefined
}).catch(error => {
console.error('Failed to log activity:', error);
});
// 调用原始的 json 方法
return originalJson.call(this, body);
};
next();
};
}
+38 -25
View File
@@ -1,40 +1,53 @@
import { Router } from 'express';
import { SearchService } from '../services/searchService';
import { activityLogger } from '../middleware/activityLogger';
const router = Router();
const searchService = new SearchService();
router.get('/search', async (req, res) => {
try {
const { keyword } = req.query;
router.get('/search',
activityLogger('SEARCH'),
async (req, res) => {
try {
const { keyword } = req.query;
if (!keyword || typeof keyword !== 'string') {
return res.status(400).json({
success: false,
code: 400,
message: 'Keyword is required and must be a string',
});
}
if (!keyword || typeof keyword !== 'string') {
return res.status(400).json({
success: false,
code: 400,
message: 'Keyword is required and must be a string',
});
}
const result = await searchService.search(keyword);
if (!result) {
return res.status(500).json({
const result = await searchService.search(keyword);
res.json(result);
} catch (error) {
console.error('Search route error:', error);
res.status(500).json({
success: false,
code: 500,
message: 'Search service error',
message: 'Internal server error',
});
}
return res.json(result);
} catch (error) {
console.error('Search route error:', error);
return res.status(500).json({
success: false,
code: 500,
message: 'Internal server error',
});
}
});
);
router.get('/link/:resourceId',
activityLogger('GENERATE_LINK'),
async (req, res) => {
try {
const { resourceId } = req.params;
const result = await searchService.generateLink(resourceId);
res.json(result);
} catch (error) {
console.error('Generate link route error:', error);
res.status(500).json({
success: false,
code: 500,
message: 'Failed to generate link',
});
}
}
);
export default router;
@@ -0,0 +1,66 @@
import { prisma } from '../lib/prisma';
import { Prisma } from '@prisma/client';
// 监听 SQL 查询
(prisma as any).$on('query', (e: Prisma.QueryEvent) => {
console.log('\n=== SQL Query ===');
console.log('Query:', e.query);
console.log('Params:', e.params);
console.log('Duration:', e.duration, 'ms');
console.log('================\n');
});
// 监听错误
(prisma as any).$on('error', (e: Error) => {
console.error('\n=== Prisma Error ===');
console.error(e);
console.error('===================\n');
});
export class ActivityLogService {
static async logActivity({
operationType,
clientIp,
keyword,
itemId,
driveType,
managedAccountId,
generatedLink,
errorCode,
errorMessage,
durationMs,
details
}: {
operationType: string;
clientIp?: string;
keyword?: string;
itemId?: string;
driveType?: string;
managedAccountId?: number;
generatedLink?: string;
errorCode?: string;
errorMessage?: string;
durationMs?: number;
details?: string;
}) {
try {
await prisma.activityLog.create({
data: {
operationType,
clientIp,
keyword,
itemId,
driveType,
managedAccountId,
generatedLink,
errorCode,
errorMessage,
durationMs,
details
}
});
} catch (error) {
console.error('Failed to log activity:', error);
}
}
}
+99 -88
View File
@@ -1,21 +1,24 @@
import axios from 'axios';
import { config } from '../config/config';
import { createClient } from 'redis';
import logger from '../lib/logger';
import { handleError } from '../decorators/errorHandler';
import { AppError } from '../utils/AppError';
const redis = createClient({
url: `redis://${config.redis.host}:${config.redis.port}`,
password: config.redis.password
});
// Redis error handling and connection monitoring
redis.on('error', err => console.error('Redis Client Error:', err));
redis.on('connect', () => console.log('Redis Client Connected'));
redis.on('ready', () => console.log('Redis Client Ready'));
redis.on('reconnecting', () => console.log('Redis Client Reconnecting'));
// Redis 错误处理和连接监控
redis.on('error', err => logger.error('Redis 客户端错误:', { error: err }));
redis.on('connect', () => logger.info('Redis 客户端已连接'));
redis.on('ready', () => logger.info('Redis 客户端就绪'));
redis.on('reconnecting', () => logger.warn('Redis 客户端正在重连'));
// Initialize Redis connection
// 初始化 Redis 连接
redis.connect().catch(err => {
console.error('Redis Connection Error:', err);
logger.error('Redis 连接错误:', { error: err });
});
const TOKEN_KEY = 'search_service_token';
@@ -89,98 +92,106 @@ export class SearchService {
constructor() {
this.baseUrl = config.searchService.baseUrl;
logger.info('搜索服务已初始化', { baseUrl: this.baseUrl });
}
private async getToken(): Promise<string | null> {
try {
// Try to get token from Redis
const cachedToken = await redis.get(TOKEN_KEY);
if (cachedToken) {
return cachedToken;
}
// Login to get new token
const response = await axios.post<LoginResponse>(
`${this.baseUrl}${config.searchService.endpoints.login}`,
{
username: process.env.SEARCH_SERVICE_USERNAME,
password: process.env.SEARCH_SERVICE_PASSWORD,
}
);
if (response.data.success && response.data.data.token) {
// Cache token in Redis
await redis.set(TOKEN_KEY, response.data.data.token, {
EX: 21600, // 6 hours
});
return response.data.data.token;
}
} catch (error) {
console.error('Error getting token:', error);
@handleError('获取认证 token 失败')
private async getToken(): Promise<string> {
// 尝试从 Redis 获取 token
const cachedToken = await redis.get(TOKEN_KEY);
if (cachedToken) {
logger.debug('从缓存中获取到 token');
return cachedToken;
}
return null;
}
public async search(keyword: string): Promise<SearchResponse | null> {
try {
const token = await this.getToken();
if (!token) {
throw new Error('Failed to get authentication token');
logger.info('缓存中未找到 token,尝试登录获取');
// 登录获取新 token
const response = await axios.post<LoginResponse>(
`${this.baseUrl}${config.searchService.endpoints.login}`,
{
username: process.env.SEARCH_SERVICE_USERNAME,
password: process.env.SEARCH_SERVICE_PASSWORD,
}
);
const response = await axios.get<SearchResponse>(
`${this.baseUrl}${config.searchService.endpoints.search}`,
{
params: { keyword },
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
} catch (error) {
console.error('Error performing search:', error);
if (axios.isAxiosError(error) && error.response?.status === 401) {
// Token expired, clear it from Redis
await redis.del(TOKEN_KEY);
}
return null;
if (!response.data.success || !response.data.data.token) {
throw new AppError('登录失败', 401);
}
logger.info('成功获取新 token');
// 将 token 缓存到 Redis
await redis.set(TOKEN_KEY, response.data.data.token, {
EX: 21600, // 6 小时
});
return response.data.data.token;
}
public async searchByParams(params: SearchParams): Promise<SearchResult> {
try {
// TODO: 实现实际的搜索逻辑
return {
success: true,
code: 0,
data: [],
message: '搜索成功'
};
} catch (error) {
console.error('Search service error:', error);
throw error;
}
}
@handleError('搜索执行失败')
public async search(keyword: string): Promise<SearchResponse> {
logger.info('开始执行搜索', { keyword });
const token = await this.getToken();
public async generateLink(resourceId: string): Promise<LinkResult> {
try {
// TODO: 实现实际的链接生成逻辑
return {
success: true,
code: 0,
data: {
url: `https://example.com/download/${resourceId}`,
expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30分钟后过期
const response = await axios.get<SearchResponse>(
`${this.baseUrl}${config.searchService.endpoints.search}`,
{
params: { keyword },
headers: {
Authorization: `Bearer ${token}`,
},
message: '链接生成成功'
};
} catch (error) {
console.error('Generate link service error:', error);
throw error;
}
}
);
logger.info('搜索完成', {
keyword,
resultCount: response.data.data?.length || 0
});
return response.data;
}
@handleError('参数化搜索失败')
public async searchByParams(params: SearchParams): Promise<SearchResult> {
logger.info('开始执行参数化搜索', {
keyword: params.keyword,
types: params.types,
page: params.page,
mode: params.mode
});
// TODO: 实现实际的搜索逻辑
const result = {
success: true,
code: 0,
data: [],
message: '搜索成功'
};
logger.info('参数化搜索完成', {
keyword: params.keyword,
resultCount: result.data.length
});
return result;
}
@handleError('生成资源链接失败')
public async generateLink(resourceId: string): Promise<LinkResult> {
logger.info('开始生成资源链接', { resourceId });
// TODO: 实现实际的链接生成逻辑
const result = {
success: true,
code: 0,
data: {
url: `https://example.com/download/${resourceId}`,
expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30分钟后过期
},
message: '链接生成成功'
};
logger.info('资源链接生成成功', {
resourceId,
expireTime: result.data.expireTime
});
return result;
}
}
+7
View File
@@ -0,0 +1,7 @@
export class AppError extends Error {
statusCode: number;
status: string;
isOperational: boolean;
constructor(message: string, statusCode: number);
}
+14
View File
@@ -0,0 +1,14 @@
export class AppError extends Error {
statusCode: number;
status: string;
isOperational: boolean;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
+3 -1
View File
@@ -6,7 +6,9 @@
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
+286 -1
View File
@@ -9,13 +9,23 @@
"@types/redis": "^4.0.11",
"axios": "^1.9.0",
"dotenv": "^16.5.0",
"redis": "^5.0.1"
"redis": "^5.0.1",
"winston": "^3.17.0"
},
"devDependencies": {
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
},
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
"license": "MIT",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -29,6 +39,17 @@
"node": ">=12"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"license": "MIT",
"dependencies": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -169,6 +190,12 @@
"redis": "*"
}
},
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
@@ -202,6 +229,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -237,6 +270,51 @@
"node": ">=0.10.0"
}
},
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"license": "MIT",
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"license": "MIT",
"dependencies": {
"color": "^3.1.3",
"text-hex": "1.0.x"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -297,6 +375,12 @@
"node": ">= 0.4"
}
},
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
"license": "MIT"
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -338,6 +422,18 @@
"node": ">= 0.4"
}
},
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
"license": "MIT"
},
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
"license": "MIT"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -461,6 +557,53 @@
"node": ">= 0.4"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
"license": "MIT"
},
"node_modules/logform": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
"license": "MIT",
"dependencies": {
"@colors/colors": "1.6.0",
"@types/triple-beam": "^1.3.2",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -495,11 +638,40 @@
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"license": "MIT",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/redis": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redis/-/redis-5.0.1.tgz",
@@ -515,6 +687,77 @@
"node": ">= 18"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"license": "MIT"
},
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
"license": "MIT",
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -581,6 +824,12 @@
"license": "MIT",
"peer": true
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@@ -588,6 +837,42 @@
"dev": true,
"license": "MIT"
},
"node_modules/winston": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
"license": "MIT",
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.7.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.9.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
"license": "MIT",
"dependencies": {
"logform": "^2.7.0",
"readable-stream": "^3.6.2",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+2 -1
View File
@@ -4,7 +4,8 @@
"@types/redis": "^4.0.11",
"axios": "^1.9.0",
"dotenv": "^16.5.0",
"redis": "^5.0.1"
"redis": "^5.0.1",
"winston": "^3.17.0"
},
"devDependencies": {
"ts-node": "^10.9.2",
+21
View File
@@ -0,0 +1,21 @@
import express from 'express';
import { errorHandler } from './middleware/errorHandler';
import { AppError } from './utils/AppError';
const app = express();
// 中间件
app.use(express.json());
// 路由
// ... 你的路由配置 ...
// 处理未找到的路由
app.all('*', (req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
});
// 错误处理中间件 (必须放在所有路由之后)
app.use(errorHandler);
export default app;
+48
View File
@@ -0,0 +1,48 @@
import { AppError } from '../utils/AppError';
import logger from '../lib/logger';
export function handleError(errorMessage?: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
// 记录错误日志
logger.error(`${errorMessage || '操作执行失败'}:`, {
method: propertyKey,
args,
error: error instanceof Error ? error.message : '未知错误',
stack: error instanceof Error ? error.stack : undefined
});
// 如果是 AppError,直接抛出
if (error instanceof AppError) {
throw error;
}
// 如果是 Axios 错误,转换为 AppError
if (error.isAxiosError) {
const statusCode = error.response?.status || 500;
throw new AppError(
error.response?.data?.message || '请求外部服务失败',
statusCode
);
}
// 其他错误转换为 AppError
throw new AppError(
error instanceof Error ? error.message : '操作执行失败',
500
);
}
};
return descriptor;
};
}
+106
View File
@@ -0,0 +1,106 @@
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../utils/AppError';
import winston from 'winston';
// 创建日志记录器
const logger = winston.createLogger({
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log' }),
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
// 开发环境错误处理
const sendErrorDev = (err: any, res: Response) => {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
};
// 生产环境错误处理
const sendErrorProd = (err: any, res: Response) => {
// 可操作的错误:发送详细信息给客户端
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
}
// 编程或其他未知错误:不泄露错误详情
else {
// 记录错误
logger.error('ERROR 💥', err);
// 发送通用错误消息
res.status(500).json({
status: 'error',
message: 'Something went wrong!'
});
}
};
// 处理特定类型的错误
const handleCastErrorDB = (err: any) => {
const message = `Invalid ${err.path}: ${err.value}.`;
return new AppError(message, 400);
};
const handleDuplicateFieldsDB = (err: any) => {
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
const message = `Duplicate field value: ${value}. Please use another value!`;
return new AppError(message, 400);
};
const handleValidationErrorDB = (err: any) => {
const errors = Object.values(err.errors).map((el: any) => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
};
// 全局错误处理中间件
export const errorHandler = (
err: any,
req: Request,
res: Response,
next: NextFunction
) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// 记录错误日志
logger.error({
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
body: req.body,
params: req.params,
query: req.query
});
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, res);
} else {
let error = { ...err };
error.message = err.message;
if (error.name === 'CastError') error = handleCastErrorDB(error);
if (error.code === 11000) error = handleDuplicateFieldsDB(error);
if (error.name === 'ValidationError') error = handleValidationErrorDB(error);
sendErrorProd(error, res);
}
};
+14
View File
@@ -0,0 +1,14 @@
export class AppError extends Error {
statusCode: number;
status: string;
isOperational: boolean;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}