feat: 百度网盘相关功能开发&代码整理
This commit is contained in:
@@ -21,6 +21,7 @@ output/
|
|||||||
# Prisma
|
# Prisma
|
||||||
backend/prisma/dev.db
|
backend/prisma/dev.db
|
||||||
backend/prisma/dev.db-journal
|
backend/prisma/dev.db-journal
|
||||||
|
backend/prisma/generated/
|
||||||
|
|
||||||
# 其他
|
# 其他
|
||||||
frontend/.output/
|
frontend/.output/
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
|
output = "./generated/client"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
@@ -10,55 +11,107 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 网盘类型枚举
|
||||||
|
enum DriveType {
|
||||||
|
/// 百度网盘
|
||||||
|
BAIDU
|
||||||
|
/// 阿里云盘
|
||||||
|
ALIYUN
|
||||||
|
/// 其他网盘类型可以在这里添加
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 网盘账号信息表
|
||||||
model Account {
|
model Account {
|
||||||
|
/// 账号ID
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
driveType String @unique
|
/// 网盘类型
|
||||||
|
driveType DriveType
|
||||||
|
/// 账号标识符(如:用户名、邮箱等)
|
||||||
accountIdentifier String
|
accountIdentifier String
|
||||||
|
/// 账号凭证(密码)
|
||||||
credentials String @db.Text
|
credentials String @db.Text
|
||||||
|
/// 账号状态(active: 活跃, inactive: 未激活, blocked: 被封禁)
|
||||||
status String @default("active")
|
status String @default("active")
|
||||||
|
/// 最后检查时间
|
||||||
lastChecked DateTime?
|
lastChecked DateTime?
|
||||||
|
/// 创建时间
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
/// 更新时间
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
/// 转码记录关联
|
||||||
transcodeRecords TranscodeRecord[]
|
transcodeRecords TranscodeRecord[]
|
||||||
|
/// 活动日志关联
|
||||||
activityLogs ActivityLog[]
|
activityLogs ActivityLog[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 转码记录表
|
||||||
model TranscodeRecord {
|
model TranscodeRecord {
|
||||||
|
/// 记录ID
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
/// 项目ID
|
||||||
itemId String
|
itemId String
|
||||||
|
/// 源文件链接
|
||||||
sourceLink String @db.Text
|
sourceLink String @db.Text
|
||||||
driveType String
|
/// 网盘类型
|
||||||
|
driveType DriveType
|
||||||
|
/// 管理的账号ID
|
||||||
managedAccountId Int
|
managedAccountId Int
|
||||||
|
/// 转码状态(pending: 等待中, processing: 处理中, completed: 完成, failed: 失败)
|
||||||
status String @default("pending")
|
status String @default("pending")
|
||||||
|
/// 临时分享链接
|
||||||
tempLink String? @db.Text
|
tempLink String? @db.Text
|
||||||
|
/// 转存后的文件ID
|
||||||
transferredFileId String?
|
transferredFileId String?
|
||||||
|
/// 转存开始时间
|
||||||
transferStartTime DateTime @default(now())
|
transferStartTime DateTime @default(now())
|
||||||
|
/// 转存结束时间
|
||||||
transferEndTime DateTime?
|
transferEndTime DateTime?
|
||||||
|
/// 错误代码
|
||||||
errorCode String?
|
errorCode String?
|
||||||
|
/// 错误信息
|
||||||
errorMessage String? @db.Text
|
errorMessage String? @db.Text
|
||||||
|
/// 清理状态(pending: 待清理, completed: 已清理, failed: 清理失败)
|
||||||
cleanupStatus String @default("pending")
|
cleanupStatus String @default("pending")
|
||||||
|
/// 创建时间
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
/// 更新时间
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
/// 关联的账号
|
||||||
account Account @relation(fields: [managedAccountId], references: [id])
|
account Account @relation(fields: [managedAccountId], references: [id])
|
||||||
|
|
||||||
@@index([itemId])
|
@@index([itemId])
|
||||||
@@index([cleanupStatus])
|
@@index([cleanupStatus])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 活动日志表
|
||||||
model ActivityLog {
|
model ActivityLog {
|
||||||
|
/// 日志ID
|
||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
|
/// 时间戳
|
||||||
timestamp DateTime @default(now())
|
timestamp DateTime @default(now())
|
||||||
|
/// 操作类型
|
||||||
operationType String
|
operationType String
|
||||||
|
/// 客户端IP
|
||||||
clientIp String?
|
clientIp String?
|
||||||
|
/// 关键词
|
||||||
keyword String?
|
keyword String?
|
||||||
|
/// 项目ID
|
||||||
itemId String?
|
itemId String?
|
||||||
driveType String?
|
/// 网盘类型
|
||||||
|
driveType DriveType?
|
||||||
|
/// 管理的账号ID
|
||||||
managedAccountId Int?
|
managedAccountId Int?
|
||||||
|
/// 生成的链接
|
||||||
generatedLink String? @db.Text
|
generatedLink String? @db.Text
|
||||||
|
/// 错误代码
|
||||||
errorCode String?
|
errorCode String?
|
||||||
|
/// 错误信息
|
||||||
errorMessage String? @db.Text
|
errorMessage String? @db.Text
|
||||||
|
/// 操作持续时间(毫秒)
|
||||||
durationMs Int?
|
durationMs Int?
|
||||||
|
/// 详细信息
|
||||||
details String? @db.Text
|
details String? @db.Text
|
||||||
|
/// 关联的账号
|
||||||
account Account? @relation(fields: [managedAccountId], references: [id])
|
account Account? @relation(fields: [managedAccountId], references: [id])
|
||||||
|
|
||||||
@@index([itemId])
|
@@index([itemId])
|
||||||
|
|||||||
@@ -1,8 +1,45 @@
|
|||||||
import { Account } from '@prisma/client';
|
import { Account, PrismaClient } from '@prisma/client';
|
||||||
import { DriveAdapter, TransferResult } from './DriveAdapter';
|
import { DriveAdapter } from './DriveAdapter';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { execFile } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { DRIVE_TYPE, DriveType, TransferResult } from '../entity/drive.types';
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export class BaiduDriveAdapter implements DriveAdapter {
|
export class BaiduDriveAdapter implements DriveAdapter {
|
||||||
|
private getBaiduPCSExecutable(): string {
|
||||||
|
const isWindows = os.platform() === 'win32';
|
||||||
|
const binDir = path.join(__dirname, '../../bin');
|
||||||
|
const platformDir = isWindows ? 'windows' : 'linux';
|
||||||
|
const executable = isWindows ? 'BaiduPCS-Go.exe' : 'BaiduPCS-Go';
|
||||||
|
return path.join(binDir, platformDir, executable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeCommand(args: string[]): Promise<string> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFileAsync(this.getBaiduPCSExecutable(), args);
|
||||||
|
return stdout.trim();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('BaiduPCS command execution failed', { error, args });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateAccountStatus(account: Account, status: string, error?: string): Promise<void> {
|
||||||
|
await prisma.account.update({
|
||||||
|
where: { id: account.id },
|
||||||
|
data: {
|
||||||
|
status,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
...(error && { credentials: error })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async transferAndShare(
|
async transferAndShare(
|
||||||
sourceLink: string,
|
sourceLink: string,
|
||||||
sourceLinkType: string,
|
sourceLinkType: string,
|
||||||
@@ -10,22 +47,42 @@ export class BaiduDriveAdapter implements DriveAdapter {
|
|||||||
): Promise<TransferResult> {
|
): Promise<TransferResult> {
|
||||||
try {
|
try {
|
||||||
logger.info('Baidu drive transferAndShare', { sourceLink, sourceLinkType, accountId: account.id });
|
logger.info('Baidu drive transferAndShare', { sourceLink, sourceLinkType, accountId: account.id });
|
||||||
// TODO: 实现百度网盘的具体转存和分享逻辑
|
|
||||||
// 1. 解析账号凭证
|
|
||||||
const credentials = JSON.parse(account.credentials);
|
|
||||||
|
|
||||||
// 2. 转存文件
|
// 1. 确保账号状态正常
|
||||||
// const transferredFileId = await this.transferFile(sourceLink, credentials);
|
if (account.status !== 'active') {
|
||||||
|
throw new Error(`Account is not active, current status: ${account.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 确保已登录
|
||||||
|
await this.ensureLoggedIn(account);
|
||||||
|
|
||||||
// 3. 生成分享链接
|
// 3. 转存文件
|
||||||
// const tempLink = await this.generateShareLink(transferredFileId, credentials);
|
const [link, pwd] = sourceLink.split('?pwd=');
|
||||||
|
const transferArgs = ['transfer', link];
|
||||||
|
if (pwd) {
|
||||||
|
transferArgs.push(pwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transferResult = await this.executeCommand(transferArgs);
|
||||||
|
logger.info('File transfer completed', { transferResult });
|
||||||
|
|
||||||
|
// 4. 获取转存后的文件ID
|
||||||
|
const transferredFileId = this.parseFileIdFromTransferResult(transferResult);
|
||||||
|
|
||||||
|
// 5. 生成分享链接
|
||||||
|
const shareResult = await this.executeCommand(['share', transferredFileId]);
|
||||||
|
const tempLink = this.parseShareLinkFromShareResult(shareResult);
|
||||||
|
|
||||||
|
// 6. 更新账号状态
|
||||||
|
await this.updateAccountStatus(account, 'active');
|
||||||
|
|
||||||
// 临时返回模拟数据
|
|
||||||
return {
|
return {
|
||||||
tempLink: 'https://pan.baidu.com/s/example',
|
tempLink,
|
||||||
transferredFileId: 'example_file_id'
|
transferredFileId
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
// 更新账号状态为错误
|
||||||
|
await this.updateAccountStatus(account, 'error', error?.message || 'Unknown error');
|
||||||
logger.error('Baidu drive transfer failed', { error, accountId: account.id });
|
logger.error('Baidu drive transfer failed', { error, accountId: account.id });
|
||||||
throw new Error('Failed to transfer and share file');
|
throw new Error('Failed to transfer and share file');
|
||||||
}
|
}
|
||||||
@@ -33,10 +90,23 @@ export class BaiduDriveAdapter implements DriveAdapter {
|
|||||||
|
|
||||||
async delete(fileId: string, account: Account): Promise<void> {
|
async delete(fileId: string, account: Account): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// TODO: 实现百度网盘的文件删除逻辑
|
// 1. 确保账号状态正常
|
||||||
const credentials = JSON.parse(account.credentials);
|
if (account.status !== 'active') {
|
||||||
// await this.deleteFile(fileId, credentials);
|
throw new Error(`Account is not active, current status: ${account.status}`);
|
||||||
} catch (error) {
|
}
|
||||||
|
|
||||||
|
// 2. 确保已登录
|
||||||
|
await this.ensureLoggedIn(account);
|
||||||
|
|
||||||
|
// 3. 删除文件
|
||||||
|
await this.executeCommand(['rm', fileId]);
|
||||||
|
logger.info('File deleted successfully', { fileId, accountId: account.id });
|
||||||
|
|
||||||
|
// 4. 更新账号状态
|
||||||
|
await this.updateAccountStatus(account, 'active');
|
||||||
|
} catch (error: any) {
|
||||||
|
// 更新账号状态为错误
|
||||||
|
await this.updateAccountStatus(account, 'error', error?.message || 'Unknown error');
|
||||||
logger.error('Baidu drive delete failed', { error, accountId: account.id });
|
logger.error('Baidu drive delete failed', { error, accountId: account.id });
|
||||||
throw new Error('Failed to delete file');
|
throw new Error('Failed to delete file');
|
||||||
}
|
}
|
||||||
@@ -44,14 +114,46 @@ export class BaiduDriveAdapter implements DriveAdapter {
|
|||||||
|
|
||||||
async checkAccountStatus(account: Account): Promise<boolean> {
|
async checkAccountStatus(account: Account): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// TODO: 实现百度网盘的账号状态检查逻辑
|
// 1. 尝试登录
|
||||||
const credentials = JSON.parse(account.credentials);
|
await this.ensureLoggedIn(account);
|
||||||
// const isValid = await this.checkAccount(credentials);
|
|
||||||
// return isValid;
|
// 2. 验证登录状态
|
||||||
return true; // 临时返回
|
await this.executeCommand(['pwd']);
|
||||||
} catch (error) {
|
|
||||||
|
// 3. 更新账号状态为活跃
|
||||||
|
await this.updateAccountStatus(account, 'active');
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
// 更新账号状态为错误
|
||||||
|
await this.updateAccountStatus(account, 'error', error?.message || 'Unknown error');
|
||||||
logger.error('Baidu drive account check failed', { error, accountId: account.id });
|
logger.error('Baidu drive account check failed', { error, accountId: account.id });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureLoggedIn(account: Account): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 先尝试执行一个命令来检查是否已登录
|
||||||
|
await this.executeCommand(['pwd']);
|
||||||
|
} catch (error) {
|
||||||
|
// 如果未登录,则执行登录
|
||||||
|
await this.executeCommand([
|
||||||
|
'login',
|
||||||
|
account.accountIdentifier,
|
||||||
|
account.credentials
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFileIdFromTransferResult(result: string): string {
|
||||||
|
// 从转存结果中解析文件ID
|
||||||
|
const match = result.match(/文件ID: (\d+)/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseShareLinkFromShareResult(result: string): string {
|
||||||
|
// 从分享结果中解析分享链接
|
||||||
|
const match = result.match(/分享链接: (https:\/\/pan\.baidu\.com\/s\/\w+)/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,17 @@ import rateLimit from 'express-rate-limit'
|
|||||||
import swaggerUi from 'swagger-ui-express'
|
import swaggerUi from 'swagger-ui-express'
|
||||||
import { swaggerSpec } from './config/swagger'
|
import { swaggerSpec } from './config/swagger'
|
||||||
import { getSearchResults, getLinkForResource } from './controllers/searchController'
|
import { getSearchResults, getLinkForResource } from './controllers/searchController'
|
||||||
|
import { initRedis } from './lib/redis'
|
||||||
|
import logger from './lib/logger'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
// 初始化 Redis 连接
|
||||||
|
initRedis().catch(err => {
|
||||||
|
logger.error('Redis 初始化失败,应用启动终止', { error: err })
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(rateLimit({ windowMs: 1000, max: 5 }))
|
app.use(rateLimit({ windowMs: 1000, max: 5 }))
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Redis key 常量定义
|
||||||
|
* 命名规范:
|
||||||
|
* 1. 使用大写字母和下划线
|
||||||
|
* 2. 按功能模块分组
|
||||||
|
* 3. 添加注释说明用途
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 搜索服务相关
|
||||||
|
export const SEARCH_SERVICE = {
|
||||||
|
/** 搜索服务认证 token */
|
||||||
|
TOKEN: 'search_service_token',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 用户相关
|
||||||
|
export const USER = {
|
||||||
|
/** 用户会话信息 */
|
||||||
|
SESSION: 'user:session:',
|
||||||
|
/** 用户令牌 */
|
||||||
|
TOKEN: 'user:token:',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 系统相关
|
||||||
|
export const SYSTEM = {
|
||||||
|
/** 系统配置缓存 */
|
||||||
|
CONFIG: 'system:config',
|
||||||
|
/** 系统状态 */
|
||||||
|
STATUS: 'system:status',
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const DRIVE_TYPE = {
|
||||||
|
BAIDU: 'BAIDU',
|
||||||
|
ALIYUN: 'ALIYUN'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DriveType = typeof DRIVE_TYPE[keyof typeof DRIVE_TYPE];
|
||||||
|
|
||||||
|
export interface TransferResult {
|
||||||
|
tempLink: string;
|
||||||
|
transferredFileId: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
data: {
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResponse {
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
data: Array<{
|
||||||
|
list: Array<{
|
||||||
|
messageId: string;
|
||||||
|
title: string;
|
||||||
|
pubDate: string;
|
||||||
|
content: string;
|
||||||
|
image?: string;
|
||||||
|
cloudLinks?: Array<{
|
||||||
|
link: string;
|
||||||
|
cloudType: string;
|
||||||
|
}>;
|
||||||
|
tags?: string[];
|
||||||
|
channel: string;
|
||||||
|
channelId: string;
|
||||||
|
}>;
|
||||||
|
channelInfo: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
index: number;
|
||||||
|
channelLogo?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchParams {
|
||||||
|
keyword: string;
|
||||||
|
types: string[];
|
||||||
|
page: number;
|
||||||
|
fileType?: string;
|
||||||
|
fileSize?: string;
|
||||||
|
fileTime?: string;
|
||||||
|
mode?: 'standard' | 'fuzzy';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
data: any[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkResult {
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
data: {
|
||||||
|
url: string;
|
||||||
|
expireTime: string;
|
||||||
|
};
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { createClient } from 'redis';
|
||||||
|
import { config } from '../config/config';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
|
// 创建 Redis 客户端实例
|
||||||
|
const redis = createClient({
|
||||||
|
url: `redis://${config.redis.host}:${config.redis.port}`,
|
||||||
|
password: config.redis.password
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 客户端正在重连'));
|
||||||
|
|
||||||
|
// 初始化 Redis 连接
|
||||||
|
export async function initRedis(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await redis.connect();
|
||||||
|
logger.info('Redis 连接初始化成功');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Redis 连接初始化失败:', { error: err });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出 Redis 客户端实例
|
||||||
|
export { redis };
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { prisma } from '../lib/prisma';
|
import { prisma } from '../lib/prisma';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { DriveType } from '../entity/drive.types';
|
||||||
|
|
||||||
// 监听 SQL 查询
|
// 监听 SQL 查询
|
||||||
(prisma as any).$on('query', (e: Prisma.QueryEvent) => {
|
(prisma as any).$on('query', (e: Prisma.QueryEvent) => {
|
||||||
@@ -35,7 +36,7 @@ export class ActivityLogService {
|
|||||||
clientIp?: string;
|
clientIp?: string;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
itemId?: string;
|
itemId?: string;
|
||||||
driveType?: string;
|
driveType?: DriveType;
|
||||||
managedAccountId?: number;
|
managedAccountId?: number;
|
||||||
generatedLink?: string;
|
generatedLink?: string;
|
||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
|
|||||||
@@ -1,91 +1,11 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { config } from '../config/config';
|
import { config } from '../config/config';
|
||||||
import { createClient } from 'redis';
|
|
||||||
import logger from '../lib/logger';
|
import logger from '../lib/logger';
|
||||||
import { handleError } from '../decorators/errorHandler';
|
import { handleError } from '../decorators/errorHandler';
|
||||||
import { AppError } from '../utils/AppError';
|
import { AppError } from '../utils/AppError';
|
||||||
|
import { LoginResponse, SearchResponse, SearchParams, SearchResult, LinkResult } from '../entity/search.types';
|
||||||
const redis = createClient({
|
import { SEARCH_SERVICE } from '../constants/redis.keys';
|
||||||
url: `redis://${config.redis.host}:${config.redis.port}`,
|
import { redis } from '../lib/redis';
|
||||||
password: config.redis.password
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 客户端正在重连'));
|
|
||||||
|
|
||||||
// 初始化 Redis 连接
|
|
||||||
redis.connect().catch(err => {
|
|
||||||
logger.error('Redis 连接错误:', { error: err });
|
|
||||||
});
|
|
||||||
|
|
||||||
const TOKEN_KEY = 'search_service_token';
|
|
||||||
|
|
||||||
interface LoginResponse {
|
|
||||||
success: boolean;
|
|
||||||
code: number;
|
|
||||||
data: {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchResponse {
|
|
||||||
success: boolean;
|
|
||||||
code: number;
|
|
||||||
data: Array<{
|
|
||||||
list: Array<{
|
|
||||||
messageId: string;
|
|
||||||
title: string;
|
|
||||||
pubDate: string;
|
|
||||||
content: string;
|
|
||||||
image?: string;
|
|
||||||
cloudLinks?: Array<{
|
|
||||||
link: string;
|
|
||||||
cloudType: string;
|
|
||||||
}>;
|
|
||||||
tags?: string[];
|
|
||||||
channel: string;
|
|
||||||
channelId: string;
|
|
||||||
}>;
|
|
||||||
channelInfo: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
index: number;
|
|
||||||
channelLogo?: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchParams {
|
|
||||||
keyword: string;
|
|
||||||
types: string[];
|
|
||||||
page: number;
|
|
||||||
fileType?: string;
|
|
||||||
fileSize?: string;
|
|
||||||
fileTime?: string;
|
|
||||||
mode?: 'standard' | 'fuzzy';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchResult {
|
|
||||||
success: boolean;
|
|
||||||
code: number;
|
|
||||||
data: any[];
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LinkResult {
|
|
||||||
success: boolean;
|
|
||||||
code: number;
|
|
||||||
data: {
|
|
||||||
url: string;
|
|
||||||
expireTime: string;
|
|
||||||
};
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
@@ -98,7 +18,7 @@ export class SearchService {
|
|||||||
@handleError('获取认证 token 失败')
|
@handleError('获取认证 token 失败')
|
||||||
private async getToken(): Promise<string> {
|
private async getToken(): Promise<string> {
|
||||||
// 尝试从 Redis 获取 token
|
// 尝试从 Redis 获取 token
|
||||||
const cachedToken = await redis.get(TOKEN_KEY);
|
const cachedToken = await redis.get(SEARCH_SERVICE.TOKEN);
|
||||||
if (cachedToken) {
|
if (cachedToken) {
|
||||||
logger.debug('从缓存中获取到 token');
|
logger.debug('从缓存中获取到 token');
|
||||||
return cachedToken;
|
return cachedToken;
|
||||||
@@ -120,7 +40,7 @@ export class SearchService {
|
|||||||
|
|
||||||
logger.info('成功获取新 token');
|
logger.info('成功获取新 token');
|
||||||
// 将 token 缓存到 Redis
|
// 将 token 缓存到 Redis
|
||||||
await redis.set(TOKEN_KEY, response.data.data.token, {
|
await redis.set(SEARCH_SERVICE.TOKEN, response.data.data.token, {
|
||||||
EX: 21600, // 6 小时
|
EX: 21600, // 6 小时
|
||||||
});
|
});
|
||||||
return response.data.data.token;
|
return response.data.data.token;
|
||||||
|
|||||||
Reference in New Issue
Block a user