增加作者页面的路由缓存

This commit is contained in:
2025-08-30 13:16:48 +08:00
parent 3a00c9dce7
commit 2a980c83d2
4 changed files with 596 additions and 1 deletions
+18
View File
@@ -96,6 +96,15 @@ backend/
- `PUT /api/proxy/cache/config` - 更新缓存配置 - `PUT /api/proxy/cache/config` - 更新缓存配置
- `POST /api/proxy/cache/config/reset` - 重置缓存配置为默认值 - `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` - 初始化仓库 - `POST /api/repository/initialize` - 初始化仓库
@@ -165,6 +174,7 @@ backend/
- **download.js**: 下载服务,处理文件下载 - **download.js**: 下载服务,处理文件下载
- **repository.js**: 仓库管理服务,处理文件管理和配置 - **repository.js**: 仓库管理服务,处理文件管理和配置
- **image-cache.js**: 图片缓存服务,管理图片代理缓存 - **image-cache.js**: 图片缓存服务,管理图片代理缓存
- **api-cache.js**: API缓存服务,管理API请求缓存
### 工具类 ### 工具类
@@ -218,6 +228,14 @@ backend/
- 缓存统计信息查看 - 缓存统计信息查看
- 手动缓存清理功能 - 手动缓存清理功能
### 8. API缓存管理
- 作者相关API请求缓存功能
- 自动缓存过期清理(默认5分钟)
- 缓存大小限制管理(默认50MB
- 缓存统计信息查看
- 手动缓存清理功能
- 支持配置缓存策略和端点白名单
## 🔒 安全特性 ## 🔒 安全特性
- 统一的错误处理 - 统一的错误处理
+119
View File
@@ -1,9 +1,11 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const ImageCacheService = require('../services/image-cache'); const ImageCacheService = require('../services/image-cache');
const ApiCacheService = require('../services/api-cache');
// 创建缓存服务实例 // 创建缓存服务实例
const imageCache = new ImageCacheService(); 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 * @param {string} url 图片URL
+429
View File
@@ -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<boolean>} 缓存是否有效
*/
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<Object|null>} 缓存数据,如果缓存不存在则返回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<void>}
*/
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<Object|null>} 缓存数据,如果不存在则返回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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<Object>} 缓存统计信息
*/
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<Object>} 缓存配置
*/
async getConfig() {
return await this.configManager.loadConfig();
}
/**
* 更新缓存配置
* @param {Object} updates 配置更新
* @returns {Promise<Object>} 更新后的配置
*/
async updateConfig(updates) {
const newConfig = await this.configManager.updateConfig(updates);
this.config = { ...this.config, ...newConfig };
return newConfig;
}
/**
* 重置缓存配置
* @returns {Promise<Object>} 重置后的配置
*/
async resetConfig() {
const defaultConfig = await this.configManager.resetToDefault();
this.config = { ...this.config, ...defaultConfig };
return defaultConfig;
}
}
module.exports = ApiCacheService;
+30 -1
View File
@@ -1,10 +1,14 @@
const axios = require('axios'); const axios = require('axios');
const { stringify } = require('qs'); const { stringify } = require('qs');
const ApiCacheService = require('./api-cache');
class ArtistService { class ArtistService {
constructor(auth) { constructor(auth) {
this.auth = auth; this.auth = auth;
this.baseURL = 'https://app-api.pixiv.net'; this.baseURL = 'https://app-api.pixiv.net';
// 创建API缓存服务实例
this.apiCache = new ApiCacheService();
} }
/** /**
@@ -326,6 +330,19 @@ class ArtistService {
* 发送API请求 * 发送API请求
*/ */
async makeRequest(method, endpoint, data = null) { 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 = { const headers = {
Authorization: `Bearer ${this.auth.accessToken}`, Authorization: `Bearer ${this.auth.accessToken}`,
'Accept-Language': 'en-us', 'Accept-Language': 'en-us',
@@ -355,7 +372,19 @@ class ArtistService {
try { try {
// 发送API请求 // 发送API请求
const response = await axios(config); 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) { } catch (error) {
console.error('API请求失败:', { console.error('API请求失败:', {
method, method,