feat: 第一次生成

This commit is contained in:
2025-05-04 20:37:40 +08:00
commit 5c01df4a93
42 changed files with 12705 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
PORT=3005
SEARCH_SERVICE_URL=http://192.168.1.99:8008
REDIS_HOST=192.168.1.99
REDIS_PORT=6379
REDIS_PASSWORD=
MYSQL_HOST=192.168.1.126
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=rootroot
MYSQL_DATABASE=bps
+2287
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
{
"name": "search-service-backend",
"version": "1.0.0",
"description": "Backend service for search functionality",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"dev": "ts-node-dev src/index.ts",
"build": "tsc",
"test": "jest"
},
"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",
"@types/redis": "^4.0.11",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"ts-node-dev": "^2.0.0",
"typescript": "^5.8.3"
}
}
+34
View File
@@ -0,0 +1,34 @@
import express from 'express'
import cors from 'cors'
import rateLimit from 'express-rate-limit'
import swaggerUi from 'swagger-ui-express'
import { swaggerSpec } from './config/swagger'
import { getSearchResults, getLinkForResource } from './controllers/searchController'
const app = express()
app.use(cors())
app.use(express.json())
app.use(rateLimit({ windowMs: 1000, max: 5 }))
// 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)
})
// 搜索接口
app.get('/api/search', getSearchResults)
// 生成临时链接接口
app.get('/api/link/:resourceId', getLinkForResource)
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok' })
})
export default app
+39
View File
@@ -0,0 +1,39 @@
import dotenv from 'dotenv';
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
});
export const config = {
server: {
port: process.env.PORT || 3000,
},
searchService: {
baseUrl: process.env.SEARCH_SERVICE_URL || 'http://192.168.1.99:8008',
endpoints: {
login: '/api/user/login',
search: '/api/search',
}
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
},
mysql: {
host: process.env.MYSQL_HOST || 'localhost',
port: parseInt(process.env.MYSQL_PORT || '3306'),
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE || 'search_service',
},
jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: '6h',
}
};
+37
View File
@@ -0,0 +1,37 @@
import swaggerJsdoc from 'swagger-jsdoc';
const options: swaggerJsdoc.Options = {
definition: {
openapi: '3.0.0',
info: {
title: '百盘搜 API 文档',
version: '1.0.0',
description: '百盘搜系统的API接口文档',
contact: {
name: 'API Support',
email: 'support@example.com'
}
},
servers: [
{
url: 'http://localhost:3005',
description: '开发服务器'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
},
security: [{
bearerAuth: []
}]
},
apis: ['./src/routes/*.ts', './src/controllers/*.ts'] // 指定API注释文件的位置
};
export const swaggerSpec = swaggerJsdoc(options);
+171
View File
@@ -0,0 +1,171 @@
import { Request, Response } from 'express'
import { search, generateLink } from '../services/searchService'
/**
* @swagger
* /api/search:
* get:
* summary: 搜索资源
* description: 根据关键词搜索网盘资源
* tags: [Search]
* parameters:
* - in: query
* name: keyword
* required: true
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: types
* schema:
* type: array
* items:
* type: string
* description: 网盘类型数组 (例如:['baidu', 'aliyun'])
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: fileType
* schema:
* type: string
* description: 文件类型筛选
* - in: query
* name: fileSize
* schema:
* type: string
* description: 文件大小筛选
* - in: query
* name: fileTime
* schema:
* type: string
* description: 文件时间筛选
* - in: query
* name: mode
* schema:
* type: string
* enum: [standard, fuzzy]
* default: standard
* description: 搜索模式
* responses:
* 200:
* description: 搜索成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* data:
* type: array
* items:
* type: object
* message:
* type: string
* 500:
* description: 服务器错误
*/
export async function getSearchResults(req: Request, res: Response) {
try {
const {
keyword,
types,
page = 1,
fileType,
fileSize,
fileTime,
mode
} = req.query;
// 转换参数类型
const searchParams = {
keyword: keyword as string,
types: Array.isArray(types) ? types : types ? [types as string] : [],
page: Number(page),
fileType: fileType as string,
fileSize: fileSize as string,
fileTime: fileTime as string,
mode: mode as 'standard' | 'fuzzy'
};
const result = await search(searchParams);
res.json(result);
} catch (error) {
console.error('Search controller error:', error);
res.status(500).json({
success: false,
code: 500,
message: '服务器内部错误'
});
}
}
/**
* @swagger
* /api/link/{resourceId}:
* get:
* summary: 获取资源链接
* description: 根据资源ID生成临时下载链接
* tags: [Resource]
* parameters:
* - in: path
* name: resourceId
* required: true
* schema:
* type: string
* description: 资源ID
* responses:
* 200:
* description: 成功生成链接
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* data:
* type: object
* properties:
* url:
* type: string
* expireTime:
* type: string
* message:
* type: string
* 400:
* description: 请求参数错误
* 500:
* description: 服务器错误
*/
export async function getLinkForResource(req: Request, res: Response) {
try {
const { resourceId } = req.params;
if (!resourceId) {
return res.status(400).json({
success: false,
code: 400,
message: '资源ID不能为空'
});
}
const result = await generateLink(resourceId);
res.json(result);
} catch (error) {
console.error('Generate link controller error:', error);
res.status(500).json({
success: false,
code: 500,
message: '生成链接失败'
});
}
}
+26
View File
@@ -0,0 +1,26 @@
import express from 'express';
import { config } from './config/config';
import searchRoutes from './routes/searchRoutes';
const app = express();
// Middleware
app.use(express.json());
// Routes
app.use('/api', searchRoutes);
// Error handling middleware
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Error:', err);
res.status(500).json({
success: false,
code: 500,
message: 'Internal server error',
});
});
// Start server
app.listen(config.server.port, () => {
console.log(`Server is running on port ${config.server.port}`);
});
+40
View File
@@ -0,0 +1,40 @@
import { Router } from 'express';
import { SearchService } from '../services/searchService';
const router = Router();
const searchService = new SearchService();
router.get('/search', async (req, res) => {
try {
const { keyword } = req.query;
if (!keyword || typeof keyword !== 'string') {
return res.status(400).json({
success: false,
code: 400,
message: 'Keyword is required and must be a string',
});
}
const result = await searchService.search(keyword);
if (!result) {
return res.status(500).json({
success: false,
code: 500,
message: 'Search service 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',
});
}
});
export default router;
+7
View File
@@ -0,0 +1,7 @@
import app from './app'
const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`)
})
+196
View File
@@ -0,0 +1,196 @@
import axios from 'axios';
import { config } from '../config/config';
import { createClient } from 'redis';
const redis = createClient({
url: `redis://${config.redis.host}:${config.redis.port}`,
password: config.redis.password
});
// Redis error handling and connection monitoring
redis.on('error', err => console.error('Redis Client Error:', err));
redis.on('connect', () => console.log('Redis Client Connected'));
redis.on('ready', () => console.log('Redis Client Ready'));
redis.on('reconnecting', () => console.log('Redis Client Reconnecting'));
// Initialize Redis connection
redis.connect().catch(err => {
console.error('Redis Connection 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 {
private baseUrl: string;
constructor() {
this.baseUrl = config.searchService.baseUrl;
}
private async getToken(): Promise<string | null> {
try {
// Try to get token from Redis
const cachedToken = await redis.get(TOKEN_KEY);
if (cachedToken) {
return cachedToken;
}
// Login to get new token
const response = await axios.post<LoginResponse>(
`${this.baseUrl}${config.searchService.endpoints.login}`,
{
username: process.env.SEARCH_SERVICE_USERNAME,
password: process.env.SEARCH_SERVICE_PASSWORD,
}
);
if (response.data.success && response.data.data.token) {
// Cache token in Redis
await redis.set(TOKEN_KEY, response.data.data.token, {
EX: 21600, // 6 hours
});
return response.data.data.token;
}
} catch (error) {
console.error('Error getting token:', error);
}
return null;
}
public async search(keyword: string): Promise<SearchResponse | null> {
try {
const token = await this.getToken();
if (!token) {
throw new Error('Failed to get authentication token');
}
const response = await axios.get<SearchResponse>(
`${this.baseUrl}${config.searchService.endpoints.search}`,
{
params: { keyword },
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
} catch (error) {
console.error('Error performing search:', error);
if (axios.isAxiosError(error) && error.response?.status === 401) {
// Token expired, clear it from Redis
await redis.del(TOKEN_KEY);
}
return null;
}
}
public async searchByParams(params: SearchParams): Promise<SearchResult> {
try {
// TODO: 实现实际的搜索逻辑
return {
success: true,
code: 0,
data: [],
message: '搜索成功'
};
} catch (error) {
console.error('Search service error:', error);
throw error;
}
}
public async generateLink(resourceId: string): Promise<LinkResult> {
try {
// TODO: 实现实际的链接生成逻辑
return {
success: true,
code: 0,
data: {
url: `https://example.com/download/${resourceId}`,
expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30分钟后过期
},
message: '链接生成成功'
};
} catch (error) {
console.error('Generate link service error:', error);
throw error;
}
}
}
// 将这些函数从类中移出,作为独立的导出函数
export async function search(params: SearchParams): Promise<SearchResult> {
const searchService = new SearchService();
return searchService.searchByParams(params);
}
export async function generateLink(resourceId: string): Promise<LinkResult> {
const searchService = new SearchService();
return searchService.generateLink(resourceId);
}
+28
View File
@@ -0,0 +1,28 @@
const Redis = require('redis');
require('dotenv').config();
async function testRedis() {
const client = Redis.createClient({
url: `redis://${process.env.REDIS_HOST || '192.168.1.99'}:${process.env.REDIS_PORT || 6379}`,
password: process.env.REDIS_PASSWORD
});
client.on('error', (err) => {
console.error('Redis连接错误:', err);
});
try {
await client.connect();
console.log('Redis连接成功!');
await client.quit();
} catch (err) {
console.error('Redis连接失败:', err);
}
}
console.log('正在尝试连接Redis:', {
host: process.env.REDIS_HOST || '192.168.1.99',
port: process.env.REDIS_PORT || 6379
});
testRedis();
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}