feat: 后端搜索接口开发
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# 百盘搜 (BPS)
|
||||
|
||||
一个聚合网盘资源搜索的 Web 应用程序。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 支持多个网盘源(百度网盘、夸克网盘、UC网盘、迅雷网盘)
|
||||
- 实时搜索和结果过滤
|
||||
- 一键获取临时分享链接
|
||||
- 自动清理过期资源
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 前端:Nuxt.js (Vue 3) + TypeScript + Tailwind CSS
|
||||
- 后端:Express.js + TypeScript
|
||||
- 数据库:MySQL 8.0 + Prisma ORM
|
||||
- 缓存:Redis
|
||||
- 部署:Docker + Docker Compose
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Node.js 20+
|
||||
- Docker 和 Docker Compose
|
||||
- MySQL 8.0
|
||||
- Redis 7.0
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 克隆仓库:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/bps.git
|
||||
cd bps
|
||||
```
|
||||
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
# 安装前端依赖
|
||||
cd frontend
|
||||
npm install
|
||||
|
||||
# 安装后端依赖
|
||||
cd ../backend
|
||||
npm install
|
||||
```
|
||||
|
||||
3. 配置环境变量:
|
||||
```bash
|
||||
# 后端
|
||||
cp backend/.env.example backend/.env
|
||||
# 编辑 .env 文件,填入必要的配置信息
|
||||
```
|
||||
|
||||
4. 初始化数据库:
|
||||
```bash
|
||||
cd backend
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
5. 启动开发服务器:
|
||||
```bash
|
||||
# 启动前端(在 frontend 目录下)
|
||||
npm run dev
|
||||
|
||||
# 启动后端(在 backend 目录下)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 使用 Docker 部署
|
||||
|
||||
1. 构建和启动所有服务:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. 初始化数据库(首次部署时):
|
||||
```bash
|
||||
docker-compose exec backend npx prisma migrate deploy
|
||||
```
|
||||
|
||||
3. 查看日志:
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
bps/
|
||||
├── frontend/ # Nuxt.js 前端应用
|
||||
├── backend/ # Express.js 后端服务
|
||||
│ ├── src/
|
||||
│ │ ├── adapters/ # 网盘适配器
|
||||
│ │ ├── api/ # API 路由
|
||||
│ │ ├── services/ # 业务逻辑
|
||||
│ │ └── utils/ # 工具函数
|
||||
│ └── prisma/ # 数据库模型和迁移
|
||||
├── doc/ # 项目文档
|
||||
└── docker-compose.yml # Docker 配置
|
||||
```
|
||||
|
||||
## API 文档
|
||||
|
||||
### 搜索接口
|
||||
|
||||
```
|
||||
GET /api/search?keyword={keyword}
|
||||
```
|
||||
|
||||
### 获取链接接口
|
||||
|
||||
```
|
||||
POST /api/generate-link
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"itemId": "string"
|
||||
}
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License - 详见 [LICENSE](LICENSE) 文件
|
||||
@@ -0,0 +1,28 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package*.json ./
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
|
||||
RUN npm install --production
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=4000
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
Generated
+3781
-283
File diff suppressed because it is too large
Load Diff
+32
-21
@@ -1,34 +1,45 @@
|
||||
{
|
||||
"name": "search-service-backend",
|
||||
"name": "bps-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend service for search functionality",
|
||||
"description": "Backend for BPS (Baidu Pan Search)",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev src/index.ts",
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev --respawn src/index.ts",
|
||||
"test": "jest",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^4.21.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.9.1",
|
||||
"redis": "^4.7.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-rate-limit": "^6.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/node": "^20.17.32",
|
||||
"@prisma/client": "^5.10.0",
|
||||
"@types/axios": "^0.14.4",
|
||||
"@types/redis": "^4.0.11",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"axios": "^1.9.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.0",
|
||||
"express": "^4.18.0",
|
||||
"express-rate-limit": "^7.1.0",
|
||||
"ioredis": "^5.3.0",
|
||||
"node-cron": "^3.0.0",
|
||||
"redis": "^5.0.1",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.0",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/node-cron": "^3.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"prisma": "^5.10.0",
|
||||
"ts-jest": "^29.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3054
-1237
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @id @default(autoincrement())
|
||||
driveType String @unique
|
||||
accountIdentifier String
|
||||
credentials String @db.Text
|
||||
status String @default("active")
|
||||
lastChecked DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
transcodeRecords TranscodeRecord[]
|
||||
activityLogs ActivityLog[]
|
||||
}
|
||||
|
||||
model TranscodeRecord {
|
||||
id Int @id @default(autoincrement())
|
||||
itemId String @index
|
||||
sourceLink String @db.Text
|
||||
driveType String
|
||||
managedAccountId Int
|
||||
status String @default("pending")
|
||||
tempLink String? @db.Text
|
||||
transferredFileId String?
|
||||
transferStartTime DateTime @default(now())
|
||||
transferEndTime DateTime?
|
||||
errorCode String?
|
||||
errorMessage String? @db.Text
|
||||
cleanupStatus String @default("pending") @index
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
account Account @relation(fields: [managedAccountId], references: [id])
|
||||
}
|
||||
|
||||
model ActivityLog {
|
||||
id BigInt @id @default(autoincrement())
|
||||
timestamp DateTime @default(now())
|
||||
operationType String
|
||||
clientIp String?
|
||||
keyword String?
|
||||
itemId String? @index
|
||||
driveType String?
|
||||
managedAccountId Int?
|
||||
generatedLink String? @db.Text
|
||||
errorCode String?
|
||||
errorMessage String? @db.Text
|
||||
durationMs Int?
|
||||
details String? @db.Text
|
||||
account Account? @relation(fields: [managedAccountId], references: [id])
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Account } from '@prisma/client';
|
||||
import { DriveAdapter, TransferResult } from './DriveAdapter';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export class BaiduDriveAdapter implements DriveAdapter {
|
||||
async transferAndShare(
|
||||
sourceLink: string,
|
||||
sourceLinkType: string,
|
||||
account: Account
|
||||
): Promise<TransferResult> {
|
||||
try {
|
||||
// TODO: 实现百度网盘的具体转存和分享逻辑
|
||||
// 1. 解析账号凭证
|
||||
const credentials = JSON.parse(account.credentials);
|
||||
|
||||
// 2. 转存文件
|
||||
// const transferredFileId = await this.transferFile(sourceLink, credentials);
|
||||
|
||||
// 3. 生成分享链接
|
||||
// const tempLink = await this.generateShareLink(transferredFileId, credentials);
|
||||
|
||||
// 临时返回模拟数据
|
||||
return {
|
||||
tempLink: 'https://pan.baidu.com/s/example',
|
||||
transferredFileId: 'example_file_id'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Baidu drive transfer failed', { error, accountId: account.id });
|
||||
throw new Error('Failed to transfer and share file');
|
||||
}
|
||||
}
|
||||
|
||||
async delete(fileId: string, account: Account): Promise<void> {
|
||||
try {
|
||||
// TODO: 实现百度网盘的文件删除逻辑
|
||||
const credentials = JSON.parse(account.credentials);
|
||||
// await this.deleteFile(fileId, credentials);
|
||||
} catch (error) {
|
||||
logger.error('Baidu drive delete failed', { error, accountId: account.id });
|
||||
throw new Error('Failed to delete file');
|
||||
}
|
||||
}
|
||||
|
||||
async checkAccountStatus(account: Account): Promise<boolean> {
|
||||
try {
|
||||
// TODO: 实现百度网盘的账号状态检查逻辑
|
||||
const credentials = JSON.parse(account.credentials);
|
||||
// const isValid = await this.checkAccount(credentials);
|
||||
// return isValid;
|
||||
return true; // 临时返回
|
||||
} catch (error) {
|
||||
logger.error('Baidu drive account check failed', { error, accountId: account.id });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Account } from '@prisma/client';
|
||||
|
||||
export interface TransferResult {
|
||||
tempLink: string;
|
||||
transferredFileId: string;
|
||||
}
|
||||
|
||||
export interface DriveAdapter {
|
||||
/**
|
||||
* 转存文件并生成临时分享链接
|
||||
* @param sourceLink 源文件链接
|
||||
* @param sourceLinkType 源链接类型
|
||||
* @param account 网盘账号信息
|
||||
* @returns 包含临时链接和转存文件ID的结果
|
||||
*/
|
||||
transferAndShare(
|
||||
sourceLink: string,
|
||||
sourceLinkType: string,
|
||||
account: Account
|
||||
): Promise<TransferResult>;
|
||||
|
||||
/**
|
||||
* 删除网盘中的文件
|
||||
* @param fileId 文件ID
|
||||
* @param account 网盘账号信息
|
||||
*/
|
||||
delete(fileId: string, account: Account): Promise<void>;
|
||||
|
||||
/**
|
||||
* 检查账号状态
|
||||
* @param account 网盘账号信息
|
||||
* @returns 账号是否有效
|
||||
*/
|
||||
checkAccountStatus(account: Account): Promise<boolean>;
|
||||
}
|
||||
@@ -4,9 +4,13 @@ dotenv.config();
|
||||
|
||||
// Add debug logging
|
||||
console.log('Environment variables loaded:', {
|
||||
REDIS_HOST: process.env.REDIS_HOST,
|
||||
REDIS_PORT: process.env.REDIS_PORT,
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD
|
||||
PORT: process.env.PORT,
|
||||
SEARCH_SERVICE_URL: process.env.SEARCH_SERVICE_URL,
|
||||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
MYSQL_HOST: process.env.MYSQL_HOST,
|
||||
MYSQL_PORT: process.env.MYSQL_PORT,
|
||||
MYSQL_USER: process.env.MYSQL_USER,
|
||||
MYSQL_PASSWORD: process.env.MYSQL_PASSWORD,
|
||||
});
|
||||
|
||||
export const config = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import swaggerJsdoc from 'swagger-jsdoc';
|
||||
import { config } from './config';
|
||||
|
||||
const options: swaggerJsdoc.Options = {
|
||||
definition: {
|
||||
@@ -14,7 +15,7 @@ const options: swaggerJsdoc.Options = {
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3005',
|
||||
url: `http://localhost:${config.server.port}`,
|
||||
description: '开发服务器'
|
||||
}
|
||||
],
|
||||
@@ -31,7 +32,7 @@ const options: swaggerJsdoc.Options = {
|
||||
bearerAuth: []
|
||||
}]
|
||||
},
|
||||
apis: ['./src/routes/*.ts', './src/controllers/*.ts'] // 指定API注释文件的位置
|
||||
apis: ['./src/routes/*.ts', './src/controllers/*.ts']
|
||||
};
|
||||
|
||||
export const swaggerSpec = swaggerJsdoc(options);
|
||||
+17
-2
@@ -1,12 +1,25 @@
|
||||
import express from 'express';
|
||||
import { config } from './config/config';
|
||||
import searchRoutes from './routes/searchRoutes';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import { swaggerSpec } from './config/swagger';
|
||||
import cors from 'cors';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Swagger UI
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
|
||||
// API Documentation in JSON format
|
||||
app.get('/api-docs.json', (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(swaggerSpec);
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api', searchRoutes);
|
||||
|
||||
@@ -20,7 +33,9 @@ app.use((err: Error, req: express.Request, res: express.Response, next: express.
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = config.server.port;
|
||||
// Start server
|
||||
app.listen(config.server.port, () => {
|
||||
console.log(`Server is running on port ${config.server.port}`);
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
console.log(`Swagger documentation available at http://localhost:${PORT}/api-docs`);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import winston from 'winston';
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: 'error.log',
|
||||
level: 'error'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: 'combined.log'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
export { logger };
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
# ---
|
||||
|
||||
**百盘搜 \- V1.0 需求文档**
|
||||
|
||||
**最后更新时间:** 2025年5月6日
|
||||
|
||||
## **1\. 项目概述**
|
||||
|
||||
### **1.1. 目标**
|
||||
|
||||
构建一个Web应用程序,主要目标是聚合来自特定外部源的网盘资源搜索结果,提供统一的搜索界面。用户可以根据网盘类型筛选结果,并能为选定的资源触发一个“获取链接”操作,该操作通过后端管理的网盘账号进行文件转存,并生成一个有时效性的临时分享链接供用户使用。
|
||||
|
||||
### **1.2. 目标用户**
|
||||
|
||||
面向普通互联网用户,无需注册或登录即可使用核心搜索和获取链接功能。
|
||||
|
||||
### **1.3. 初始范围**
|
||||
|
||||
* **支持网盘:** 百度网盘 (baidu), 夸克网盘 (quark), UC网盘 (uc), 迅雷网盘 (xunlei)。搜索结果需能按这些类型筛选。
|
||||
* **核心功能:**
|
||||
* 关键词搜索。
|
||||
* 按网盘类型筛选。
|
||||
* 获取临时分享链接(通过后端转存)。
|
||||
* **后台:** 实现必要的API、缓存、数据库记录和日志。
|
||||
|
||||
## **2\. 技术栈**
|
||||
|
||||
* **前端:** Nuxt.js (使用TypeScript)
|
||||
* **后端:** Express.js (使用TypeScript)
|
||||
* **数据库:** MySQL 8.0 (ORM: Prisma)
|
||||
* **缓存:** Redis
|
||||
|
||||
## **3\. 架构概述**
|
||||
|
||||
* **前端:** 负责用户界面展示、接收用户输入、向后端请求数据、存储从后端获取的完整搜索结果列表、在本地实现所有筛选和分页逻辑。
|
||||
* **后端:**
|
||||
* 提供RESTful API供前端调用。
|
||||
* 与唯一的外部搜索API (http://192.168.1.99:8008/api/search) 交互。
|
||||
* 处理和缓存搜索结果。
|
||||
* 管理系统自有的网盘账号(每种类型一个)。
|
||||
* 实现核心的“转存与分享”逻辑(DriveAdapter模式)。
|
||||
* 执行资源清理的后台任务。
|
||||
* 记录详细的操作日志到数据库。
|
||||
* **Drive Adapters:** 后端采用适配器模式,为每种支持的网盘(Baidu, Quark, UC, Xunlei)实现统一接口,封装其特定的API调用逻辑(如转存、生成分享链接、删除文件)。
|
||||
|
||||
## **4\. 功能需求 \- 前端**
|
||||
|
||||
### **4.1. 搜索页面 (/)**
|
||||
|
||||
* **界面元素:**
|
||||
* 顶部包含一个关键词输入框和搜索按钮。
|
||||
* 提供按**网盘类型**(百度、夸克、UC、迅雷)筛选的控件(如按钮组或复选框)。
|
||||
* 搜索结果列表区域。
|
||||
* 分页控件。
|
||||
* **结果展示:**
|
||||
* 搜索结果以列表形式展示。
|
||||
* 每项结果至少清晰显示:
|
||||
* **标题/文件名 (displayTitle)**
|
||||
* **来源网盘 (driveType)**: 可以用文字或小图标表示。
|
||||
* **发布日期 (publishDate)**
|
||||
* 每项结果旁边提供一个 **“获取链接”** 按钮。
|
||||
* **交互逻辑:**
|
||||
* 用户输入关键词并点击搜索,前端调用后端 GET /api/search 接口。
|
||||
* 前端接收并**存储全部**搜索结果数据。
|
||||
* 用户点击网盘类型筛选控件,前端**在本地**对已存储的数据进行过滤显示。
|
||||
* 用户使用分页控件,前端**在本地**计算并显示对应页码的数据。
|
||||
* 用户点击某项结果的“获取链接”按钮,前端调用后端 POST /api/generate-link 接口,并传递该项对应的 itemId。前端需要处理等待状态,并在获取到链接或错误码后给用户反馈。
|
||||
|
||||
## **5\. 功能需求 \- 后端 API**
|
||||
|
||||
### **5.1. GET /api/search**
|
||||
|
||||
* **输入:** keyword (Query Parameter, String, Required)
|
||||
* **外部API认证配置:**
|
||||
* 后端应用需要安全地配置用于登录外部搜索接口 (http://192.168.1.99:8008/api/user/login) 的 username 和 password。这些凭证**绝不能**硬编码,应通过环境变量或加密配置文件提供。
|
||||
* 后端需要一个机制来存储和管理从外部登录接口获取的当前有效的 token (例如,存储在内存变量中,并考虑持久化到Redis以支持多实例部署和重启后恢复,或者每次应用启动时重新登录获取)。
|
||||
* **处理流程:**
|
||||
1. **确保外部API Token有效:** a. 检查当前是否已持有有效的 token。可以通过检查 token 的过期时间(如果JWT token中包含exp声明)或通过一个简单的状态标记来判断。 b. **如果 token 不存在、无效或即将过期:** i. 调用外部登录接口 POST http://192.168.1.99:8008/api/user/login,请求体包含配置好的 username 和 password。 ii. 如果登录成功,获取返回的新 token,并更新后端持有的当前 token 及其过期信息。记录登录成功日志。 iii.如果登录失败,记录严重错误日志(例如,无法连接外部认证服务,凭证错误),并向上游(即我们的前端)返回一个特定的错误码,表明搜索服务暂时不可用。
|
||||
2. **执行搜索(结合缓存逻辑):** a. 构建Redis缓存Key (例如: search:${keyword})。尝试从Redis获取缓存。若命中,直接返回缓存数据(包含itemId的列表)。 b. 若未命中缓存: i. **调用外部搜索API (携带Token):** GET http://192.168.1.99:8008/api/search?keyword={keyword} 在请求头中添加 Authorization: Bearer \<当前有效的token\>。 ii. **处理Token失效(重试机制):** 如果调用外部搜索API返回401或403错误(表示Token无效或过期): 1\. 立即执行上述**步骤 1.b (重新登录获取新Token)**。 2\. 如果成功获取新Token,则**重试一次**步骤 2.b.i (调用外部搜索API)。 3\. 如果重试仍然失败,或重新登录失败,则记录严重错误日志,并向上游返回错误。 iii.处理外部API的响应数据。对于返回的每个资源项: 1\. 生成一个全局唯一的 itemId (例如: UUID)。 2\. **数据提取与格式化:** 确保包含 driveType (统一格式), publishDate (ISO 8601), displayTitle, 以及后续“获取链接”所需的原始信息(如 cloudLinks, 原始 messageId)。 3\. 将该项的**完整处理后信息**存入Redis,Key为 itemId,**TTL设置为 1-2 天**。 iv. 记录本次搜索操作(成功或因外部API问题失败)到 ActivityLog 表(包含关键词、来源IP等)。 v. 如果外部API调用成功,将包含所有处理后结果项(至少含 itemId, driveType, publishDate, displayTitle)的**完整列表**存入Redis,Key为 search:${keyword},**TTL建议设置为 5 分钟**。 vi. 将此完整列表返回给前端(或在发生不可恢复错误时返回错误响应)。
|
||||
* **输出:** \[{ itemId, displayTitle, driveType, publishDate, ... }, ...\] 或错误响应。
|
||||
* **并发管理 (建议):** 当多个并发请求发现token失效时,应有机制避免同时多次调用外部登录接口(例如,使用一个简单的锁或请求队列来管理token的更新过程)。
|
||||
|
||||
### **5.2. POST /api/generate-link**
|
||||
|
||||
* **输入:** JSON Body { "itemId": "..." } (String, Required)
|
||||
* **处理流程 (同步执行):**
|
||||
1. 记录操作开始 (GET\_LINK\_START) 到 ActivityLog(包含 itemId, IP等)。
|
||||
2. 使用 itemId 从Redis获取资源的完整信息。若获取失败(Key不存在或过期),返回错误码。
|
||||
3. 从资源信息中确定所需的 driveType。
|
||||
4. 从数据库 Account 表中查找该 driveType 对应的**唯一**管理账号。若找不到或账号状态无效,记录错误并返回错误码。
|
||||
5. 根据 driveType 获取相应的 DriveAdapter 实例。
|
||||
6. 调用 adapter.transferAndShare(sourceLink, sourceLinkType, managedAccount) 方法执行核心逻辑。
|
||||
7. **若成功:** a. transferAndShare 方法应返回生成的 tempLink 和用于后续删除的 transferredFileId。 b. 在 TranscodeRecord 表中记录或更新本次操作的状态为 'success',并保存 tempLink, transferredFileId, 完成时间等信息。 c. 记录操作成功 (GET\_LINK\_SUCCESS) 到 ActivityLog。 d. 向前端返回 { "success": true, "tempLink": "..." }。
|
||||
8. **若失败 (在任何步骤):** a. 在 TranscodeRecord 表中记录或更新本次操作的状态为 'failed',并保存错误信息。 b. 记录操作失败 (GET\_LINK\_FAILURE) 到 ActivityLog,包含返回给前端的错误码和详细的内部错误信息。 c. 向前端返回 { "success": false, "errorCode": "SOME\_ERROR\_CODE" } (错误码应预定义,不暴露内部细节)。
|
||||
* **输出:** 成功时返回包含临时链接的JSON,失败时返回包含错误码的JSON。
|
||||
|
||||
## **6\. 功能需求 \- 后台任务**
|
||||
|
||||
### **6.1. 资源清理任务**
|
||||
|
||||
* **触发:** 定时执行(例如: 使用 node-cron 或类似库,每5分钟执行一次)。
|
||||
* **逻辑:**
|
||||
1. 查询 TranscodeRecord 表,筛选条件为:status \== 'success', cleanupStatus \== 'pending', 且 transferEndTime 早于当前时间减去约10分钟。
|
||||
2. 对每个符合条件的记录: a. 获取 transferredFileId, driveType, managedAccountId。 b. 获取对应的 Account 信息和 DriveAdapter 实例。 c. 调用 adapter.delete(transferredFileId, managedAccount) 方法删除网盘中的文件。 d. **若删除成功:** 更新 TranscodeRecord 的 cleanupStatus 为 'done'。记录清理成功 (CLEANUP\_SUCCESS) 到 ActivityLog。 e. **若删除失败:** 更新 TranscodeRecord 的 cleanupStatus 为 'failed'。记录清理失败 (CLEANUP\_FAILURE) 到 ActivityLog,包含错误信息。
|
||||
|
||||
## **7\. 数据模型 (Database \- MySQL/Prisma)**
|
||||
|
||||
### **7.1. Account 表 (管理系统自有的网盘账号)**
|
||||
|
||||
* id: Int @id @default(autoincrement())
|
||||
* driveType: String @unique (例如: 'baidu', 'quark', 'uc', 'xunlei')
|
||||
* accountIdentifier: String (用于标识账号, 如用户名)
|
||||
* credentials: String @db.Text (存储加密后的JSON字符串,包含API Key/Secret, Cookie等)
|
||||
* status: String @default("active") (例如: 'active', 'inactive', 'error')
|
||||
* lastChecked: DateTime?
|
||||
* createdAt: DateTime @default(now())
|
||||
* updatedAt: DateTime @updatedAt
|
||||
|
||||
### **7.2. TranscodeRecord 表 (跟踪转存和清理状态)**
|
||||
|
||||
* id: Int @id @default(autoincrement())
|
||||
* itemId: String @index (关联的资源项ID)
|
||||
* sourceLink: String @db.Text
|
||||
* driveType: String
|
||||
* managedAccountId: Int (外键关联 Account.id)
|
||||
* status: String @default("pending") ('pending', 'success', 'failed')
|
||||
* tempLink: String? @db.Text
|
||||
* transferredFileId: String? (由Adapter返回,用于删除)
|
||||
* transferStartTime: DateTime @default(now())
|
||||
* transferEndTime: DateTime?
|
||||
* errorCode: String?
|
||||
* errorMessage: String? @db.Text
|
||||
* cleanupStatus: String @default("pending") ('pending', 'done', 'failed') @index
|
||||
* createdAt: DateTime @default(now())
|
||||
* updatedAt: DateTime @updatedAt
|
||||
* account: Account @relation(fields: \[managedAccountId\], references: \[id\])
|
||||
|
||||
### **7.3. ActivityLog 表 (操作审计日志)**
|
||||
|
||||
* id: BigInt @id @default(autoincrement())
|
||||
* timestamp: DateTime @default(now())
|
||||
* operationType: String ('SEARCH', 'GET\_LINK\_START', 'GET\_LINK\_SUCCESS', 'GET\_LINK\_FAILURE', 'CLEANUP\_SUCCESS', 'CLEANUP\_FAILURE')
|
||||
* clientIp: String?
|
||||
* keyword: String?
|
||||
* itemId: String? @index
|
||||
* driveType: String?
|
||||
* managedAccountId: Int? (关联 Account.id)
|
||||
* generatedLink: String? @db.Text
|
||||
* errorCode: String?
|
||||
* errorMessage: String? @db.Text
|
||||
* durationMs: Int?
|
||||
* details: String? @db.Text (存储额外JSON格式的上下文信息)
|
||||
|
||||
## **8\. 非功能性需求**
|
||||
|
||||
* **缓存:**
|
||||
* 搜索结果列表缓存 (search:{keyword} \-\> Item List): Redis, TTL \~5分钟。
|
||||
* 单个项目详细信息缓存 (itemId \-\> Full Item Data): Redis, TTL 1-2天。
|
||||
* **安全:**
|
||||
* Account 表中的 credentials 字段必须加密存储 (例如: 使用Node.js的 crypto 模块进行 AES 加密,密钥需妥善管理)。
|
||||
* 后端API需实施速率限制 (例如: 使用 express-rate-limit 中间件,限制 IP 频率,特别是 POST /api/generate-link)。
|
||||
* 记录请求来源IP地址到 ActivityLog。
|
||||
* **日志:**
|
||||
* 后端应使用成熟的日志库 (如 Winston, pino) 进行结构化日志记录。
|
||||
* 关键操作、错误均需记录到 ActivityLog 数据库表。
|
||||
* **错误处理:**
|
||||
* 后端API在发生内部错误时,应返回预定义的、不暴露细节的错误码给前端。
|
||||
* 详细的错误信息和堆栈应记录在后端日志和 ActivityLog 的 errorMessage 字段中。
|
||||
* 前端根据错误码显示统一的、用户友好的错误提示。
|
||||
|
||||
## **9\. V1.0 版本范围之外**
|
||||
|
||||
* 管理后台界面及相关API (/api/stats, /api/accounts)。
|
||||
* 用户注册、登录系统。
|
||||
* 除“网盘类型”外的其他高级搜索筛选(文件类型、大小、时间等)。
|
||||
* 搜索结果中的文件列表预览功能。
|
||||
* 异步处理“获取链接”请求。
|
||||
* 支持每种网盘类型配置多个管理账号及选择策略。
|
||||
* 与日志聚合系统(如Loki)的集成。
|
||||
@@ -0,0 +1,50 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "4000:4000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=mysql://bps:bps_password@db:3306/bps
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root_password
|
||||
- MYSQL_DATABASE=bps
|
||||
- MYSQL_USER=bps
|
||||
- MYSQL_PASSWORD=bps_password
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
|
||||
redis:
|
||||
image: redis:7.0
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
@@ -0,0 +1,26 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/.output ./.output
|
||||
COPY --from=builder /app/package*.json ./
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", ".output/server/index.mjs"]
|
||||
@@ -11,6 +11,6 @@ export default defineNuxtConfig({
|
||||
port: 3000
|
||||
},
|
||||
app: {
|
||||
baseURL: '/absproxy/3000'
|
||||
// baseURL: '/absproxy/3000'
|
||||
}
|
||||
})
|
||||
|
||||
+11
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"name": "bps-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
@@ -10,8 +10,14 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^3.17.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.1"
|
||||
"@nuxtjs/tailwindcss": "^6.11.0",
|
||||
"nuxt": "^3.10.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/devtools": "latest",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4710
-5232
File diff suppressed because it is too large
Load Diff
Generated
+356
-1
@@ -5,7 +5,138 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"dotenv": "^16.5.0"
|
||||
"@types/axios": "^0.14.4",
|
||||
"@types/redis": "^4.0.11",
|
||||
"axios": "^1.9.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"redis": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.0.1.tgz",
|
||||
"integrity": "sha512-F7L+rnuJvq/upKaVoEgsf8VT7g5pLQYWRqSUOV3uO4vpVtARzSKJ7CLyJjVsQS+wZVCGxsLMh8DwAIDcny1B+g==",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.0.1.tgz",
|
||||
"integrity": "sha512-k0EJvlMGEyBqUD3orKe0UMZ66fPtfwqPIr+ZSd853sXj2EyhNtPXSx+J6sENXJNgAlEBhvD+57Dwt0qTisKB0A==",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/json": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.0.1.tgz",
|
||||
"integrity": "sha512-t94HOTk5myfhvaHZzlUzk2hoUvH2jsjftcnMgJWuHL/pzjAJQoZDCUJzjkoXIUjWXuyJixTguaaDyOZWwqH2Kg==",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/search": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.0.1.tgz",
|
||||
"integrity": "sha512-wipK6ZptY7K68B7YLVhP5I/wYCDUU+mDJMyJiUcQLuOs7/eKOBc8lTXKUSssor8QnzZSPy4A5ulcC5PZY22Zgw==",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/time-series": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.0.1.tgz",
|
||||
"integrity": "sha512-k6PgbrakhnohsEWEAdQZYt3e5vSKoIzpKvgQt8//lnWLrTZx+c3ed2sj0+pKIF4FvnSeuXLo4bBWcH0Z7Urg1A==",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/axios": {
|
||||
"version": "0.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz",
|
||||
"integrity": "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==",
|
||||
"deprecated": "This is a stub types definition. axios provides its own type definitions, so you do not need this installed.",
|
||||
"dependencies": {
|
||||
"axios": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/redis": {
|
||||
"version": "4.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz",
|
||||
"integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==",
|
||||
"deprecated": "This is a stub types definition. redis provides its own type definitions, so you do not need this installed.",
|
||||
"dependencies": {
|
||||
"redis": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
@@ -18,6 +149,230 @@
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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/redis": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-5.0.1.tgz",
|
||||
"integrity": "sha512-J8nqUjrfSq0E8NQkcHDZ4HdEQk5RMYjP3jZq02PE+ERiRxolbDNxPaTT4xh6tdrme+lJ86Goje9yMt9uzh23hQ==",
|
||||
"dependencies": {
|
||||
"@redis/bloom": "5.0.1",
|
||||
"@redis/client": "5.0.1",
|
||||
"@redis/json": "5.0.1",
|
||||
"@redis/search": "5.0.1",
|
||||
"@redis/time-series": "5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"dotenv": "^16.5.0"
|
||||
"@types/axios": "^0.14.4",
|
||||
"@types/redis": "^4.0.11",
|
||||
"axios": "^1.9.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"redis": "^5.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user