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:
co-authored by
Claude Opus 4.6
parent
7b1073e23b
commit
335f0fb8dc
+58
-23
@@ -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 });
|
||||
// 继续处理其他彩票,不因单个失败中止
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user