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+2,15元 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 };