mirror of
https://github.com/woodchen-ink/Random-Api.git
synced 2025-07-18 22:12:03 +08:00
199 lines
5.4 KiB
JavaScript
199 lines
5.4 KiB
JavaScript
import express from 'express';
|
|
import fetch from 'node-fetch';
|
|
import { LRUCache } from 'lru-cache'; // 使用命名导入
|
|
import compression from 'compression';
|
|
import winston from 'winston';
|
|
import 'winston-daily-rotate-file';
|
|
import cluster from 'cluster';
|
|
import { cpus } from 'os';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
// 处理 __dirname
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const numCPUs = cpus().length;
|
|
|
|
const app = express();
|
|
const port = 5003;
|
|
|
|
// 设置静态文件目录
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// 外部 JSON 文件的 URL
|
|
const CSV_PATHS_URL = 'https://random-api.czl.net/url.json';
|
|
|
|
// 设置缓存
|
|
let csvPathsCache = null;
|
|
let lastFetchTime = 0;
|
|
const CACHE_DURATION = 60 * 1000 * 60 * 24; // 24小时
|
|
|
|
// 使用新的 LRUCache 构造函数
|
|
const csvCache = new LRUCache({
|
|
max: 100, // 最多缓存100个CSV文件
|
|
ttl: 1000 * 60 * 60 * 24 // 缓存24小时
|
|
});
|
|
|
|
// 日志名称格式
|
|
const consoleFormat = winston.format.printf(({ level, message, timestamp }) => {
|
|
return `${timestamp} ${level}: ${message}`;
|
|
});
|
|
// 自定义日志格式
|
|
const customFormat = winston.format.printf(({ level, message, timestamp, ...metadata }) => {
|
|
let msg = `${timestamp} [${level}]: `;
|
|
|
|
if (typeof message === 'object') {
|
|
msg += JSON.stringify(message, null, 2);
|
|
} else {
|
|
msg += message;
|
|
}
|
|
|
|
if (metadata.logs) {
|
|
msg += '\n' + metadata.logs.map(log => JSON.stringify(log, null, 2)).join('\n');
|
|
}
|
|
|
|
return msg;
|
|
});
|
|
// 设置日志
|
|
const logger = winston.createLogger({
|
|
level: 'info',
|
|
format: winston.format.combine(
|
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
customFormat
|
|
),
|
|
transports: [
|
|
new winston.transports.Console(),
|
|
new winston.transports.DailyRotateFile({
|
|
filename: 'logs/application-%DATE%.log',
|
|
datePattern: 'YYYY-MM-DD-HH',
|
|
maxSize: '20m',
|
|
maxFiles: '7d', // 日志保存的时间,超过这个时间的会自动删除旧文件
|
|
zippedArchive: false // 禁用压缩
|
|
})
|
|
]
|
|
});
|
|
|
|
// 日志缓冲
|
|
let logBuffer = [];
|
|
|
|
// 增强的日志中间件
|
|
app.use((req, res, next) => {
|
|
const logEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
ip: req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress,
|
|
method: req.method,
|
|
path: req.originalUrl,
|
|
protocol: req.protocol,
|
|
host: req.get('Host'),
|
|
userAgent: req.get('User-Agent'),
|
|
referer: req.get('Referer') || 'N/A',
|
|
accept: req.get('Accept'),
|
|
acceptEncoding: req.get('Accept-Encoding'),
|
|
acceptLanguage: req.get('Accept-Language')
|
|
};
|
|
|
|
// 立即输出到控制台
|
|
logger.info(logEntry);
|
|
|
|
// 添加到缓冲区,用于每小时写入文件
|
|
logBuffer.push(logEntry);
|
|
|
|
next();
|
|
});
|
|
|
|
// 每小时输出一次日志到文件
|
|
setInterval(() => {
|
|
if (logBuffer.length > 0) {
|
|
logger.info('Hourly log', { logs: logBuffer });
|
|
logBuffer = [];
|
|
}
|
|
}, 60 * 60 * 1000); // 每小时
|
|
|
|
async function getCsvPaths() {
|
|
const now = Date.now();
|
|
if (!csvPathsCache || now - lastFetchTime > CACHE_DURATION) {
|
|
const response = await fetch(CSV_PATHS_URL);
|
|
if (response.ok) {
|
|
csvPathsCache = await response.json();
|
|
lastFetchTime = now;
|
|
}
|
|
}
|
|
return csvPathsCache;
|
|
}
|
|
|
|
async function getCsvContent(url) {
|
|
if (csvCache.has(url)) {
|
|
return csvCache.get(url);
|
|
}
|
|
const response = await fetch(url);
|
|
if (response.ok) {
|
|
const content = await response.text();
|
|
csvCache.set(url, content);
|
|
return content;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function handleRequest(req, res) {
|
|
try {
|
|
const csvPaths = await getCsvPaths();
|
|
if (!csvPaths) {
|
|
return res.status(500).send('CSV paths configuration could not be fetched.');
|
|
}
|
|
|
|
let path = req.path.slice(1);
|
|
path = path.split('?')[0];
|
|
if (path.endsWith('/')) {
|
|
path = path.slice(0, -1);
|
|
}
|
|
|
|
const pathSegments = path.split('/');
|
|
const prefix = pathSegments[0];
|
|
const suffix = pathSegments.slice(1).join('/');
|
|
|
|
if (prefix in csvPaths && suffix in csvPaths[prefix]) {
|
|
const csvUrl = csvPaths[prefix][suffix];
|
|
const fileArrayText = await getCsvContent(csvUrl);
|
|
if (fileArrayText) {
|
|
const fileArray = fileArrayText.split('\n').filter(line => Boolean(line) && !line.trim().startsWith('#'));
|
|
const randomUrl = fileArray[Math.floor(Math.random() * fileArray.length)];
|
|
return res.redirect(302, randomUrl);
|
|
} else {
|
|
return res.status(500).send('CSV file could not be fetched.');
|
|
}
|
|
} else {
|
|
const indexHtmlResponse = await fetch('https://random-api.czl.net');
|
|
const indexHtml = await indexHtmlResponse.text();
|
|
return res.type('html').send(indexHtml);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
logger.error('Error:', error);
|
|
res.status(500).send('Internal Server Error');
|
|
}
|
|
}
|
|
|
|
// 处理所有路由
|
|
app.get('*', handleRequest);
|
|
|
|
// 使用 cluster 模块
|
|
if (cluster.isMaster) {
|
|
console.log(`Master ${process.pid} is running`);
|
|
|
|
// Fork workers.
|
|
for (let i = 0; i < numCPUs; i++) {
|
|
cluster.fork();
|
|
}
|
|
|
|
cluster.on('exit', (worker, code, signal) => {
|
|
console.log(`worker ${worker.process.pid} died`);
|
|
});
|
|
} else {
|
|
// Workers can share any TCP connection
|
|
// In this case it is an HTTP server
|
|
app.listen(port, () => {
|
|
console.log(`Worker ${process.pid} started on port ${port}`);
|
|
});
|
|
}
|