feat: 第一次生成
This commit is contained in:
@@ -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
|
||||
Generated
+2287
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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',
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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: '生成链接失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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}`)
|
||||
})
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user