feat: 增加日志打印和异常处理
This commit is contained in:
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export class AppError extends Error {
|
||||
statusCode: number;
|
||||
status: string;
|
||||
isOperational: boolean;
|
||||
|
||||
constructor(message: string, statusCode: number);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user