feat: 增加日志打印和异常处理
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# 通用规则
|
||||||
|
1. 如果需要使用npm安装任何包,请使用pnpm来代替
|
||||||
|
2. Windows环境下请注意Powershell 不支持 && 连续命令,请自行分开命令
|
||||||
|
|
||||||
|
3. 写业务类的时候自动使用logger打印日志,并且日志用中文显示。
|
||||||
Generated
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
"redis": "^5.0.1",
|
"redis": "^5.0.1",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.0",
|
"@types/cors": "^2.8.0",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"redis": "^5.0.1",
|
"redis": "^5.0.1",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.0",
|
"@types/cors": "^2.8.0",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export class BaiduDriveAdapter implements DriveAdapter {
|
|||||||
account: Account
|
account: Account
|
||||||
): Promise<TransferResult> {
|
): Promise<TransferResult> {
|
||||||
try {
|
try {
|
||||||
|
logger.info('Baidu drive transferAndShare', { sourceLink, sourceLinkType, accountId: account.id });
|
||||||
// TODO: 实现百度网盘的具体转存和分享逻辑
|
// TODO: 实现百度网盘的具体转存和分享逻辑
|
||||||
// 1. 解析账号凭证
|
// 1. 解析账号凭证
|
||||||
const credentials = JSON.parse(account.credentials);
|
const credentials = JSON.parse(account.credentials);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export async function getSearchResults(req: Request, res: Response) {
|
|||||||
// 转换参数类型
|
// 转换参数类型
|
||||||
const searchParams = {
|
const searchParams = {
|
||||||
keyword: keyword as string,
|
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),
|
page: Number(page),
|
||||||
fileType: fileType as string,
|
fileType: fileType as string,
|
||||||
fileSize: fileSize as string,
|
fileSize: fileSize as string,
|
||||||
@@ -94,6 +94,7 @@ export async function getSearchResults(req: Request, res: Response) {
|
|||||||
mode: mode as 'standard' | 'fuzzy'
|
mode: mode as 'standard' | 'fuzzy'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clientIp = req.ip || req.socket.remoteAddress;
|
||||||
const result = await search(searchParams);
|
const result = await search(searchParams);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} 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);
|
const result = await generateLink(resourceId);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} 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 { Router } from 'express';
|
||||||
import { SearchService } from '../services/searchService';
|
import { SearchService } from '../services/searchService';
|
||||||
|
import { activityLogger } from '../middleware/activityLogger';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const searchService = new SearchService();
|
const searchService = new SearchService();
|
||||||
|
|
||||||
router.get('/search', async (req, res) => {
|
router.get('/search',
|
||||||
try {
|
activityLogger('SEARCH'),
|
||||||
const { keyword } = req.query;
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { keyword } = req.query;
|
||||||
|
|
||||||
if (!keyword || typeof keyword !== 'string') {
|
if (!keyword || typeof keyword !== 'string') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
code: 400,
|
code: 400,
|
||||||
message: 'Keyword is required and must be a string',
|
message: 'Keyword is required and must be a string',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await searchService.search(keyword);
|
const result = await searchService.search(keyword);
|
||||||
|
res.json(result);
|
||||||
if (!result) {
|
} catch (error) {
|
||||||
return res.status(500).json({
|
console.error('Search route error:', error);
|
||||||
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
code: 500,
|
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;
|
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 axios from 'axios';
|
||||||
import { config } from '../config/config';
|
import { config } from '../config/config';
|
||||||
import { createClient } from 'redis';
|
import { createClient } from 'redis';
|
||||||
|
import logger from '../lib/logger';
|
||||||
|
import { handleError } from '../decorators/errorHandler';
|
||||||
|
import { AppError } from '../utils/AppError';
|
||||||
|
|
||||||
const redis = createClient({
|
const redis = createClient({
|
||||||
url: `redis://${config.redis.host}:${config.redis.port}`,
|
url: `redis://${config.redis.host}:${config.redis.port}`,
|
||||||
password: config.redis.password
|
password: config.redis.password
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redis error handling and connection monitoring
|
// Redis 错误处理和连接监控
|
||||||
redis.on('error', err => console.error('Redis Client Error:', err));
|
redis.on('error', err => logger.error('Redis 客户端错误:', { error: err }));
|
||||||
redis.on('connect', () => console.log('Redis Client Connected'));
|
redis.on('connect', () => logger.info('Redis 客户端已连接'));
|
||||||
redis.on('ready', () => console.log('Redis Client Ready'));
|
redis.on('ready', () => logger.info('Redis 客户端就绪'));
|
||||||
redis.on('reconnecting', () => console.log('Redis Client Reconnecting'));
|
redis.on('reconnecting', () => logger.warn('Redis 客户端正在重连'));
|
||||||
|
|
||||||
// Initialize Redis connection
|
// 初始化 Redis 连接
|
||||||
redis.connect().catch(err => {
|
redis.connect().catch(err => {
|
||||||
console.error('Redis Connection Error:', err);
|
logger.error('Redis 连接错误:', { error: err });
|
||||||
});
|
});
|
||||||
|
|
||||||
const TOKEN_KEY = 'search_service_token';
|
const TOKEN_KEY = 'search_service_token';
|
||||||
@@ -89,98 +92,106 @@ export class SearchService {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = config.searchService.baseUrl;
|
this.baseUrl = config.searchService.baseUrl;
|
||||||
|
logger.info('搜索服务已初始化', { baseUrl: this.baseUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getToken(): Promise<string | null> {
|
@handleError('获取认证 token 失败')
|
||||||
try {
|
private async getToken(): Promise<string> {
|
||||||
// Try to get token from Redis
|
// 尝试从 Redis 获取 token
|
||||||
const cachedToken = await redis.get(TOKEN_KEY);
|
const cachedToken = await redis.get(TOKEN_KEY);
|
||||||
if (cachedToken) {
|
if (cachedToken) {
|
||||||
return cachedToken;
|
logger.debug('从缓存中获取到 token');
|
||||||
}
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
logger.info('缓存中未找到 token,尝试登录获取');
|
||||||
}
|
// 登录获取新 token
|
||||||
|
const response = await axios.post<LoginResponse>(
|
||||||
public async search(keyword: string): Promise<SearchResponse | null> {
|
`${this.baseUrl}${config.searchService.endpoints.login}`,
|
||||||
try {
|
{
|
||||||
const token = await this.getToken();
|
username: process.env.SEARCH_SERVICE_USERNAME,
|
||||||
if (!token) {
|
password: process.env.SEARCH_SERVICE_PASSWORD,
|
||||||
throw new Error('Failed to get authentication token');
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const response = await axios.get<SearchResponse>(
|
if (!response.data.success || !response.data.data.token) {
|
||||||
`${this.baseUrl}${config.searchService.endpoints.search}`,
|
throw new AppError('登录失败', 401);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
@handleError('搜索执行失败')
|
||||||
try {
|
public async search(keyword: string): Promise<SearchResponse> {
|
||||||
// TODO: 实现实际的搜索逻辑
|
logger.info('开始执行搜索', { keyword });
|
||||||
return {
|
const token = await this.getToken();
|
||||||
success: true,
|
|
||||||
code: 0,
|
|
||||||
data: [],
|
|
||||||
message: '搜索成功'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Search service error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async generateLink(resourceId: string): Promise<LinkResult> {
|
const response = await axios.get<SearchResponse>(
|
||||||
try {
|
`${this.baseUrl}${config.searchService.endpoints.search}`,
|
||||||
// TODO: 实现实际的链接生成逻辑
|
{
|
||||||
return {
|
params: { keyword },
|
||||||
success: true,
|
headers: {
|
||||||
code: 0,
|
Authorization: `Bearer ${token}`,
|
||||||
data: {
|
|
||||||
url: `https://example.com/download/${resourceId}`,
|
|
||||||
expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30分钟后过期
|
|
||||||
},
|
},
|
||||||
message: '链接生成成功'
|
}
|
||||||
};
|
);
|
||||||
} catch (error) {
|
|
||||||
console.error('Generate link service error:', error);
|
logger.info('搜索完成', {
|
||||||
throw error;
|
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",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|||||||
Generated
+286
-1
@@ -9,13 +9,23 @@
|
|||||||
"@types/redis": "^4.0.11",
|
"@types/redis": "^4.0.11",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"redis": "^5.0.1"
|
"redis": "^5.0.1",
|
||||||
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3"
|
"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": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -29,6 +39,17 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
@@ -169,6 +190,12 @@
|
|||||||
"redis": "*"
|
"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": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
@@ -202,6 +229,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -237,6 +270,51 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -297,6 +375,12 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -338,6 +422,18 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.9",
|
"version": "1.15.9",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
@@ -461,6 +557,53 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
@@ -495,11 +638,40 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
"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": {
|
"node_modules/redis": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redis/-/redis-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redis/-/redis-5.0.1.tgz",
|
||||||
@@ -515,6 +687,77 @@
|
|||||||
"node": ">= 18"
|
"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": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
@@ -581,6 +824,12 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"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": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
@@ -588,6 +837,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
"@types/redis": "^4.0.11",
|
"@types/redis": "^4.0.11",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"redis": "^5.0.1"
|
"redis": "^5.0.1",
|
||||||
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
+21
@@ -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;
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user