diff --git a/backend/README.md b/backend/README.md index 4c090ef..ea605d4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -96,6 +96,15 @@ backend/ - `PUT /api/proxy/cache/config` - 更新缓存配置 - `POST /api/proxy/cache/config/reset` - 重置缓存配置为默认值 +### API缓存管理相关 + +- `GET /api/proxy/api-cache/stats` - 获取API缓存统计信息 +- `DELETE /api/proxy/api-cache` - 清理所有API缓存 +- `DELETE /api/proxy/api-cache/expired` - 清理过期API缓存 +- `GET /api/proxy/api-cache/config` - 获取API缓存配置 +- `PUT /api/proxy/api-cache/config` - 更新API缓存配置 +- `POST /api/proxy/api-cache/config/reset` - 重置API缓存配置为默认值 + ### 仓库管理相关 - `POST /api/repository/initialize` - 初始化仓库 @@ -165,6 +174,7 @@ backend/ - **download.js**: 下载服务,处理文件下载 - **repository.js**: 仓库管理服务,处理文件管理和配置 - **image-cache.js**: 图片缓存服务,管理图片代理缓存 +- **api-cache.js**: API缓存服务,管理API请求缓存 ### 工具类 @@ -218,6 +228,14 @@ backend/ - 缓存统计信息查看 - 手动缓存清理功能 +### 8. API缓存管理 +- 作者相关API请求缓存功能 +- 自动缓存过期清理(默认5分钟) +- 缓存大小限制管理(默认50MB) +- 缓存统计信息查看 +- 手动缓存清理功能 +- 支持配置缓存策略和端点白名单 + ## 🔒 安全特性 - 统一的错误处理 diff --git a/backend/routes/proxy.js b/backend/routes/proxy.js index df7c3da..930e0b2 100644 --- a/backend/routes/proxy.js +++ b/backend/routes/proxy.js @@ -1,9 +1,11 @@ const express = require('express'); const router = express.Router(); const ImageCacheService = require('../services/image-cache'); +const ApiCacheService = require('../services/api-cache'); // 创建缓存服务实例 const imageCache = new ImageCacheService(); +const apiCache = new ApiCacheService(); /** * 图片代理 @@ -163,6 +165,123 @@ router.post('/cache/config/reset', async (req, res) => { } }); +/** + * API缓存管理 - 获取缓存统计信息 + * GET /api/proxy/api-cache/stats + */ +router.get('/api-cache/stats', async (req, res) => { + try { + const stats = await apiCache.getCacheStats(); + res.json({ + success: true, + data: stats + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * API缓存管理 - 清理所有缓存 + * DELETE /api/proxy/api-cache + */ +router.delete('/api-cache', async (req, res) => { + try { + await apiCache.clearAllCache(); + res.json({ + success: true, + message: '所有API缓存已清理' + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * API缓存管理 - 清理过期缓存 + * DELETE /api/proxy/api-cache/expired + */ +router.delete('/api-cache/expired', async (req, res) => { + try { + await apiCache.cleanupExpiredCache(); + res.json({ + success: true, + message: '过期API缓存已清理' + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * API缓存管理 - 获取缓存配置 + * GET /api/proxy/api-cache/config + */ +router.get('/api-cache/config', async (req, res) => { + try { + const config = await apiCache.getConfig(); + res.json({ + success: true, + data: config + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * API缓存管理 - 更新缓存配置 + * PUT /api/proxy/api-cache/config + */ +router.put('/api-cache/config', async (req, res) => { + try { + const updates = req.body; + const config = await apiCache.updateConfig(updates); + res.json({ + success: true, + data: config, + message: 'API缓存配置已更新' + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * API缓存管理 - 重置缓存配置 + * POST /api/proxy/api-cache/config/reset + */ +router.post('/api-cache/config/reset', async (req, res) => { + try { + const config = await apiCache.resetConfig(); + res.json({ + success: true, + data: config, + message: 'API缓存配置已重置为默认值' + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + /** * 获取文件内容类型 * @param {string} url 图片URL diff --git a/backend/services/api-cache.js b/backend/services/api-cache.js new file mode 100644 index 0000000..6141c77 --- /dev/null +++ b/backend/services/api-cache.js @@ -0,0 +1,429 @@ +const fs = require('fs').promises; +const path = require('path'); +const crypto = require('crypto'); +const CacheConfigManager = require('../config/cache-config'); + +/** + * API缓存服务 + * 负责管理API请求的缓存功能,特别是作者相关的API + */ +class ApiCacheService { + constructor() { + // 检测是否在pkg打包环境中运行 + const isPkg = process.pkg !== undefined; + + if (isPkg) { + // 在打包环境中,使用可执行文件所在目录 + this.cacheDir = path.join(process.cwd(), 'data', 'api-cache'); + } else { + // 在开发环境中,使用项目根目录的data文件夹 + this.cacheDir = path.join(__dirname, '..', '..', 'data', 'api-cache'); + } + + // 确保路径是绝对路径 + this.cacheDir = path.resolve(this.cacheDir); + + // 创建配置管理器 + this.configManager = new CacheConfigManager(); + + // 默认缓存配置 + this.config = { + maxAge: 5 * 60 * 1000, // 5分钟缓存(API数据变化较快) + maxSize: 50 * 1024 * 1024, // 50MB最大缓存大小 + cleanupInterval: 30 * 60 * 1000, // 30分钟清理一次 + enabled: true, + allowedEndpoints: [ + '/v1/user/detail', + '/v1/user/illusts', + '/v1/user/following', + '/v1/user/follower', + '/v1/search/user' + ], + // 缓存键生成策略 + keyStrategy: { + includeQueryParams: true, // 包含查询参数 + includeHeaders: false, // 不包含请求头 + hashAlgorithm: 'md5' + } + }; + + // 初始化配置 + this.initializeConfig(); + } + + /** + * 初始化配置 + */ + async initializeConfig() { + try { + await this.configManager.initialize(); + const config = await this.configManager.loadConfig(); + this.config = { ...this.config, ...config }; + + // 确保缓存目录存在 + await this.ensureCacheDir(); + + // 启动定期清理任务 + this.startCleanupTask(); + + console.log('API缓存服务初始化完成'); + } catch (error) { + console.error('API缓存服务初始化失败:', error); + } + } + + /** + * 确保缓存目录存在 + */ + async ensureCacheDir() { + try { + await fs.mkdir(this.cacheDir, { recursive: true }); + console.log('API缓存目录创建成功:', this.cacheDir); + } catch (error) { + console.error('创建API缓存目录失败:', error); + } + } + + /** + * 生成缓存键 + * @param {string} method HTTP方法 + * @param {string} endpoint API端点 + * @param {Object} params 查询参数 + * @returns {string} 缓存键 + */ + generateCacheKey(method, endpoint, params = {}) { + // 构建缓存键的基础部分 + let keyBase = `${method.toUpperCase()}:${endpoint}`; + + // 确保params是对象 + const safeParams = params || {}; + + // 如果endpoint已经包含查询参数(包含?),则直接使用endpoint作为键 + if (endpoint.includes('?')) { + keyBase = `${method.toUpperCase()}:${endpoint}`; + } else if (this.config.keyStrategy.includeQueryParams && Object.keys(safeParams).length > 0) { + // 如果endpoint不包含查询参数,且params不为空,则添加查询参数 + const sortedParams = Object.keys(safeParams) + .sort() + .map(key => `${key}=${safeParams[key]}`) + .join('&'); + keyBase += `?${sortedParams}`; + } + + // 使用指定的哈希算法生成最终的键 + const hash = crypto.createHash(this.config.keyStrategy.hashAlgorithm).update(keyBase).digest('hex'); + return `${hash}.json`; + } + + /** + * 获取缓存文件路径 + * @param {string} cacheKey 缓存键 + * @returns {string} 缓存文件路径 + */ + getCacheFilePath(cacheKey) { + return path.join(this.cacheDir, cacheKey); + } + + /** + * 检查缓存是否存在且有效 + * @param {string} cacheKey 缓存键 + * @returns {Promise} 缓存是否有效 + */ + async isCacheValid(cacheKey) { + try { + const cachePath = this.getCacheFilePath(cacheKey); + const stats = await fs.stat(cachePath); + + // 检查文件是否过期 + const age = Date.now() - stats.mtime.getTime(); + return age < this.config.maxAge; + } catch (error) { + return false; + } + } + + /** + * 从缓存获取数据 + * @param {string} cacheKey 缓存键 + * @returns {Promise} 缓存数据,如果缓存不存在则返回null + */ + async getFromCache(cacheKey) { + try { + if (!(await this.isCacheValid(cacheKey))) { + return null; + } + + const cachePath = this.getCacheFilePath(cacheKey); + const data = await fs.readFile(cachePath, 'utf8'); + + // 更新文件访问时间 + await fs.utimes(cachePath, new Date(), new Date()); + + return JSON.parse(data); + } catch (error) { + console.error('读取API缓存失败:', error); + return null; + } + } + + /** + * 将数据保存到缓存 + * @param {string} cacheKey 缓存键 + * @param {Object} data 要缓存的数据 + * @returns {Promise} + */ + async saveToCache(cacheKey, data) { + try { + const cachePath = this.getCacheFilePath(cacheKey); + const jsonData = JSON.stringify(data, null, 2); + await fs.writeFile(cachePath, jsonData, 'utf8'); + + // 检查缓存大小,如果超过限制则清理 + await this.checkCacheSize(); + } catch (error) { + console.error('保存API缓存失败:', error); + } + } + + /** + * 检查是否应该缓存该请求 + * @param {string} method HTTP方法 + * @param {string} endpoint API端点 + * @returns {boolean} 是否应该缓存 + */ + shouldCache(method, endpoint) { + // 只缓存GET请求 + if (method.toUpperCase() !== 'GET') { + return false; + } + + // 检查端点是否在允许列表中 + return this.config.allowedEndpoints.some(allowedEndpoint => + endpoint.includes(allowedEndpoint) + ); + } + + /** + * 获取缓存数据(如果存在且有效) + * @param {string} method HTTP方法 + * @param {string} endpoint API端点 + * @param {Object} params 查询参数 + * @returns {Promise} 缓存数据,如果不存在则返回null + */ + async get(method, endpoint, params = {}) { + // 检查缓存是否启用 + if (!this.config.enabled) { + return null; + } + + // 检查是否应该缓存该请求 + if (!this.shouldCache(method, endpoint)) { + return null; + } + + // 确保params是对象 + const safeParams = params || {}; + const cacheKey = this.generateCacheKey(method, endpoint, safeParams); + return await this.getFromCache(cacheKey); + } + + /** + * 设置缓存数据 + * @param {string} method HTTP方法 + * @param {string} endpoint API端点 + * @param {Object} params 查询参数 + * @param {Object} data 要缓存的数据 + * @returns {Promise} + */ + async set(method, endpoint, params = {}, data) { + // 检查缓存是否启用 + if (!this.config.enabled) { + return; + } + + // 检查是否应该缓存该请求 + if (!this.shouldCache(method, endpoint)) { + return; + } + + // 确保params是对象 + const safeParams = params || {}; + const cacheKey = this.generateCacheKey(method, endpoint, safeParams); + await this.saveToCache(cacheKey, data); + } + + /** + * 检查缓存大小并清理 + * @returns {Promise} + */ + async checkCacheSize() { + try { + const files = await fs.readdir(this.cacheDir); + let totalSize = 0; + const fileStats = []; + + // 计算总大小和收集文件信息 + for (const file of files) { + const filePath = path.join(this.cacheDir, file); + const stats = await fs.stat(filePath); + totalSize += stats.size; + fileStats.push({ + path: filePath, + size: stats.size, + mtime: stats.mtime + }); + } + + // 如果超过最大大小,删除最旧的文件 + if (totalSize > this.config.maxSize) { + console.log(`API缓存大小 ${totalSize} 超过限制 ${this.config.maxSize},开始清理...`); + + // 按修改时间排序,删除最旧的文件 + fileStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime()); + + for (const file of fileStats) { + await fs.unlink(file.path); + totalSize -= file.size; + + if (totalSize <= this.config.maxSize * 0.8) { // 清理到80% + break; + } + } + + console.log(`API缓存清理完成,当前大小: ${totalSize}`); + } + } catch (error) { + console.error('检查API缓存大小失败:', error); + } + } + + /** + * 清理过期缓存 + * @returns {Promise} + */ + async cleanupExpiredCache() { + try { + const files = await fs.readdir(this.cacheDir); + let cleanedCount = 0; + + for (const file of files) { + const filePath = path.join(this.cacheDir, file); + const stats = await fs.stat(filePath); + + const age = Date.now() - stats.mtime.getTime(); + if (age > this.config.maxAge) { + await fs.unlink(filePath); + cleanedCount++; + } + } + + if (cleanedCount > 0) { + console.log(`清理了 ${cleanedCount} 个过期API缓存文件`); + } + } catch (error) { + console.error('清理过期API缓存失败:', error); + } + } + + /** + * 启动定期清理任务 + */ + startCleanupTask() { + setInterval(() => { + this.cleanupExpiredCache().catch(error => { + console.error('定期清理API缓存任务失败:', error); + }); + }, this.config.cleanupInterval); + } + + /** + * 手动清理所有缓存 + * @returns {Promise} + */ + async clearAllCache() { + try { + const files = await fs.readdir(this.cacheDir); + + for (const file of files) { + const filePath = path.join(this.cacheDir, file); + await fs.unlink(filePath); + } + + console.log('所有API缓存已清理'); + } catch (error) { + console.error('清理所有API缓存失败:', error); + throw error; + } + } + + /** + * 获取缓存统计信息 + * @returns {Promise} 缓存统计信息 + */ + async getCacheStats() { + try { + const files = await fs.readdir(this.cacheDir); + let totalSize = 0; + let fileCount = 0; + + for (const file of files) { + const filePath = path.join(this.cacheDir, file); + const stats = await fs.stat(filePath); + totalSize += stats.size; + fileCount++; + } + + return { + fileCount, + totalSize, + maxSize: this.config.maxSize, + maxAge: this.config.maxAge, + enabled: this.config.enabled, + allowedEndpoints: this.config.allowedEndpoints, + config: this.config + }; + } catch (error) { + console.error('获取API缓存统计失败:', error); + return { + fileCount: 0, + totalSize: 0, + maxSize: this.config.maxSize, + maxAge: this.config.maxAge, + enabled: this.config.enabled, + allowedEndpoints: this.config.allowedEndpoints, + config: this.config + }; + } + } + + /** + * 获取缓存配置 + * @returns {Promise} 缓存配置 + */ + async getConfig() { + return await this.configManager.loadConfig(); + } + + /** + * 更新缓存配置 + * @param {Object} updates 配置更新 + * @returns {Promise} 更新后的配置 + */ + async updateConfig(updates) { + const newConfig = await this.configManager.updateConfig(updates); + this.config = { ...this.config, ...newConfig }; + return newConfig; + } + + /** + * 重置缓存配置 + * @returns {Promise} 重置后的配置 + */ + async resetConfig() { + const defaultConfig = await this.configManager.resetToDefault(); + this.config = { ...this.config, ...defaultConfig }; + return defaultConfig; + } +} + +module.exports = ApiCacheService; \ No newline at end of file diff --git a/backend/services/artist.js b/backend/services/artist.js index 0f9324e..4ae7f9a 100644 --- a/backend/services/artist.js +++ b/backend/services/artist.js @@ -1,10 +1,14 @@ const axios = require('axios'); const { stringify } = require('qs'); +const ApiCacheService = require('./api-cache'); class ArtistService { constructor(auth) { this.auth = auth; this.baseURL = 'https://app-api.pixiv.net'; + + // 创建API缓存服务实例 + this.apiCache = new ApiCacheService(); } /** @@ -326,6 +330,19 @@ class ArtistService { * 发送API请求 */ async makeRequest(method, endpoint, data = null) { + // 对于GET请求,尝试从缓存获取 + if (method === 'GET') { + try { + const cachedData = await this.apiCache.get(method, endpoint, data || {}); + if (cachedData) { + console.log(`API缓存命中: ${method} ${endpoint}`); + return cachedData; + } + } catch (error) { + console.error('读取API缓存失败:', error); + } + } + const headers = { Authorization: `Bearer ${this.auth.accessToken}`, 'Accept-Language': 'en-us', @@ -355,7 +372,19 @@ class ArtistService { try { // 发送API请求 const response = await axios(config); - return response.data; + const responseData = response.data; + + // 对于GET请求,将响应数据缓存 + if (method === 'GET') { + try { + await this.apiCache.set(method, endpoint, data || {}, responseData); + console.log(`API缓存已保存: ${method} ${endpoint}`); + } catch (error) { + console.error('保存API缓存失败:', error); + } + } + + return responseData; } catch (error) { console.error('API请求失败:', { method,