perf: add rate limiting, validation, logging, and error handling

- Add express-rate-limit with tiered rate limits per endpoint
- Add express-validator for input validation on all API routes
- Add winston + morgan for structured logging
- Add node-cache for lottery result caching (5min TTL, 1000 max keys)
- Convert OCR to async task queue with max 2 concurrent jobs
- Add global error handler middleware
- Add React ErrorBoundary for client-side error handling
- Fix memory leaks in OCR task queue and lottery cache
- Add per-item error handling in batch ticket updates
- Fix SQL UPDATE to include user_id in WHERE clause

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-19 11:35:12 +08:00
co-authored by Claude Opus 4.6
parent 7b1073e23b
commit 335f0fb8dc
17 changed files with 927 additions and 148 deletions
+58 -23
View File
@@ -1,7 +1,12 @@
const axios = require('axios');
const cheerio = require('cheerio');
const NodeCache = require('node-cache');
const logger = require('./logger');
const db = require('../db/init');
// 内存缓存 - 5分钟TTL, 最多1000个键防止内存溢出
const cache = new NodeCache({ stdTTL: 300, checkperiod: 60, maxKeys: 1000 });
// 获取预计开奖时间
function getEstimatedDrawTime(game, issue) {
const now = new Date();
@@ -51,10 +56,21 @@ function getEstimatedDrawTime(game, issue) {
// 获取开奖结果
async function fetchLotteryResult(game, issue) {
// 先查本地缓存
const cached = db.prepare('SELECT * FROM lottery_results WHERE game = ? AND issue = ?').get(game, issue);
const cacheKey = `lottery:${game}:${issue}`;
// 先查内存缓存
const cached = cache.get(cacheKey);
if (cached) {
return { ...cached, numbers: JSON.parse(cached.numbers) };
logger.debug(`内存缓存命中: ${cacheKey}`);
return cached;
}
// 查本地数据库
const dbCached = db.prepare('SELECT * FROM lottery_results WHERE game = ? AND issue = ?').get(game, issue);
if (dbCached) {
const result = { ...dbCached, numbers: JSON.parse(dbCached.numbers) };
cache.set(cacheKey, result);
return result;
}
// 从网络获取
@@ -66,26 +82,41 @@ async function fetchLotteryResult(game, issue) {
INSERT OR REPLACE INTO lottery_results (game, issue, numbers, draw_date)
VALUES (?, ?, ?, ?)
`).run(game, issue, JSON.stringify(result.numbers), result.draw_date);
cache.set(cacheKey, result);
}
return result;
} catch (err) {
console.error('获取开奖数据失败:', err.message);
logger.error('获取开奖数据失败:', err.message);
return null;
}
}
// 从500.com获取数据
async function fetchFromWeb(game, issue) {
// 检查是否在冷却期 (防止频繁请求)
const cooldownKey = `cooldown:${game}`;
if (cache.get(cooldownKey)) {
logger.warn(`请求冷却中: ${game}`);
return null;
}
let result = null;
try {
if (game === '双色球') {
return await fetchSSQ(issue);
result = await fetchSSQ(issue);
} else if (game === '大乐透') {
return await fetchDLT(issue);
result = await fetchDLT(issue);
}
} catch (err) {
console.error(`获取${game}开奖数据失败:`, err.message);
logger.error(`获取${game}开奖数据失败:`, err.message);
}
return null;
// 设置请求冷却 (5分钟)
if (!result) {
cache.set(cooldownKey, true, 300);
}
return result;
}
// 获取双色球开奖数据
@@ -224,25 +255,29 @@ async function updateTicketsStatus(userId) {
`).all(userId);
for (const ticket of pendingTickets) {
const result = await fetchLotteryResult(ticket.game, ticket.issue);
if (result) {
const ticketNumbers = ticket.numbers ? JSON.parse(ticket.numbers) : null;
if (ticketNumbers && ticketNumbers.length > 0) {
let totalPrize = 0;
for (const num of ticketNumbers) {
const winning = checkWinning(ticket.game, num, result.numbers);
if (winning.won) {
totalPrize += winning.prize * ticket.multiple;
// 大乐透只有1等奖和2等奖追加才有80%浮动奖金,其他等级没有
if (ticket.is_additional && winning.level >= 1 && winning.level <= 2) {
totalPrize += Math.floor(winning.prize * 0.8) * ticket.multiple;
try {
const result = await fetchLotteryResult(ticket.game, ticket.issue);
if (result) {
const ticketNumbers = ticket.numbers ? JSON.parse(ticket.numbers) : null;
if (ticketNumbers && ticketNumbers.length > 0) {
let totalPrize = 0;
for (const num of ticketNumbers) {
const winning = checkWinning(ticket.game, num, result.numbers);
if (winning.won) {
totalPrize += winning.prize * ticket.multiple;
if (ticket.is_additional && winning.level >= 1 && winning.level <= 2) {
totalPrize += Math.floor(winning.prize * 0.8) * ticket.multiple;
}
}
}
}
db.prepare('UPDATE tickets SET prize = ?, status = ? WHERE id = ?')
.run(totalPrize, totalPrize > 0 ? 'won' : 'lost', ticket.id);
db.prepare('UPDATE tickets SET prize = ?, status = ? WHERE id = ?')
.run(totalPrize, totalPrize > 0 ? 'won' : 'lost', ticket.id);
}
}
} catch (err) {
logger.error(`更新彩票 ${ticket.id} 状态失败`, { error: err.message });
// 继续处理其他彩票,不因单个失败中止
}
}
}