Files
lottery/server/services/lottery.js
T
DeveloperandClaude Opus 4.6 335f0fb8dc 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>
2026-03-19 11:35:12 +08:00

286 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
const currentDay = now.getDay();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
let drawDays = []; // 开奖日期(周日=0,周一=1...)
if (game === '双色球') {
drawDays = [2, 4, 0]; // 周二、周四、周日
} else if (game === '大乐透') {
drawDays = [1, 3, 5]; // 周一、周三、周六
}
if (drawDays.length === 0) return null;
// 按日期排序
drawDays.sort((a, b) => a - b);
// 找到最近的下一个开奖日
let daysUntilDraw = -1;
for (const day of drawDays) {
if (day > currentDay) {
daysUntilDraw = day - currentDay;
break;
}
}
// 如果今天就是开奖日,检查是否已过开奖时间
if (daysUntilDraw === -1) {
// 没有找到更晚的日期,循环回到第一个开奖日
daysUntilDraw = (drawDays[0] + 7) - currentDay;
} else if (daysUntilDraw === 0) {
// 今天是开奖日,检查是否已过21:15
if (currentHour > 21 || (currentHour === 21 && currentMinute >= 15)) {
daysUntilDraw = (drawDays[0] + 7) - currentDay;
}
}
const nextDraw = new Date(now);
nextDraw.setDate(now.getDate() + daysUntilDraw);
nextDraw.setHours(21, 15, 0, 0);
return nextDraw.toISOString();
}
// 获取开奖结果
async function fetchLotteryResult(game, issue) {
const cacheKey = `lottery:${game}:${issue}`;
// 先查内存缓存
const cached = cache.get(cacheKey);
if (cached) {
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;
}
// 从网络获取
try {
const result = await fetchFromWeb(game, issue);
if (result) {
// 缓存到数据库
db.prepare(`
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) {
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 === '双色球') {
result = await fetchSSQ(issue);
} else if (game === '大乐透') {
result = await fetchDLT(issue);
}
} catch (err) {
logger.error(`获取${game}开奖数据失败:`, err.message);
}
// 设置请求冷却 (5分钟)
if (!result) {
cache.set(cooldownKey, true, 300);
}
return result;
}
// 获取双色球开奖数据
async function fetchSSQ(issue) {
const url = 'https://datachart.500.com/ssq/history/newinc/history.php?limit=50';
const response = await axios.get(url, {
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
timeout: 10000
});
const $ = cheerio.load(response.data);
let result = null;
// 处理期号格式:2026019 -> 26019 或 26019 -> 26019
const shortIssue = issue.length > 5 ? issue.slice(-5) : issue;
$('#tdata tr').each((i, row) => {
const tds = $(row).find('td');
const rowIssue = $(tds[0]).text().trim();
if (rowIssue === shortIssue || rowIssue === issue) {
const red = [];
for (let j = 1; j <= 6; j++) {
red.push($(tds[j]).text().trim());
}
const blue = $(tds[7]).text().trim();
const drawDate = $(tds[15]).text().trim();
result = {
game: '双色球',
issue: issue, // 返回原始期号
numbers: { red, blue },
draw_date: drawDate
};
return false;
}
});
return result;
}
// 获取大乐透开奖数据
async function fetchDLT(issue) {
const url = 'https://datachart.500.com/dlt/history/newinc/history.php?limit=50';
const response = await axios.get(url, {
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
timeout: 10000
});
const $ = cheerio.load(response.data);
let result = null;
// 处理期号格式:2026019 -> 26019 或 26019 -> 26019
const shortIssue = issue.length > 5 ? issue.slice(-5) : issue;
$('#tdata tr').each((i, row) => {
const tds = $(row).find('td');
const rowIssue = $(tds[0]).text().trim();
if (rowIssue === shortIssue || rowIssue === issue) {
const front = [];
for (let j = 1; j <= 5; j++) {
front.push($(tds[j]).text().trim());
}
const back = [];
for (let j = 6; j <= 7; j++) {
back.push($(tds[j]).text().trim());
}
const drawDate = $(tds[14]).text().trim();
result = {
game: '大乐透',
issue: issue, // 返回原始期号
numbers: { front, back },
draw_date: drawDate
};
return false;
}
});
return result;
}
// 检查中奖
function checkWinning(game, ticketNumbers, resultNumbers) {
if (!ticketNumbers || !resultNumbers) return { won: false, prize: 0 };
if (game === '双色球') {
return checkSSQWinning(ticketNumbers, resultNumbers);
} else if (game === '大乐透') {
return checkDLTWinning(ticketNumbers, resultNumbers);
}
return { won: false, prize: 0 };
}
// 双色球中奖检查
function checkSSQWinning(ticket, result) {
const redMatch = ticket.red.filter(n => result.red.includes(n)).length;
const blueMatch = ticket.blue === result.blue;
if (redMatch === 6 && blueMatch) return { won: true, level: 1, prize: 5000000 };
if (redMatch === 6) return { won: true, level: 2, prize: 100000 };
if (redMatch === 5 && blueMatch) return { won: true, level: 3, prize: 3000 };
if (redMatch === 5 || (redMatch === 4 && blueMatch)) return { won: true, level: 4, prize: 200 };
if (redMatch === 4 || (redMatch === 3 && blueMatch)) return { won: true, level: 5, prize: 10 };
if (blueMatch) return { won: true, level: 6, prize: 5 };
return { won: false, prize: 0 };
}
// 大乐透中奖检查
function checkDLTWinning(ticket, result) {
const frontMatch = ticket.front.filter(n => result.front.includes(n)).length;
const backMatch = ticket.back.filter(n => result.back.includes(n)).length;
if (frontMatch === 5 && backMatch === 2) return { won: true, level: 1, prize: 10000000 };
if (frontMatch === 5 && backMatch === 1) return { won: true, level: 2, prize: 100000 };
if (frontMatch === 5) return { won: true, level: 3, prize: 10000 };
if (frontMatch === 4 && backMatch === 2) return { won: true, level: 4, prize: 3000 };
if (frontMatch === 4 && backMatch === 1) return { won: true, level: 5, prize: 300 };
if (frontMatch === 3 && backMatch === 2) return { won: true, level: 6, prize: 200 };
if (frontMatch === 4) return { won: true, level: 7, prize: 100 };
// 8等奖:3+1 或 2+215元
if ((frontMatch === 3 && backMatch === 1) || (frontMatch === 2 && backMatch === 2)) return { won: true, level: 8, prize: 15 };
// 9等奖:3+0 或 2+1 或 1+2 或 后区2个,5元
if (frontMatch === 3 || (frontMatch === 2 && backMatch === 1) || (frontMatch === 1 && backMatch === 2) || backMatch === 2) return { won: true, level: 9, prize: 5 };
return { won: false, prize: 0 };
}
// 批量更新彩票状态
async function updateTicketsStatus(userId) {
const pendingTickets = db.prepare(`
SELECT * FROM tickets WHERE user_id = ? AND status = 'pending' AND issue IS NOT NULL
`).all(userId);
for (const ticket of pendingTickets) {
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);
}
}
} catch (err) {
logger.error(`更新彩票 ${ticket.id} 状态失败`, { error: err.message });
// 继续处理其他彩票,不因单个失败中止
}
}
}
module.exports = { fetchLotteryResult, checkWinning, updateTicketsStatus, getEstimatedDrawTime };