import express from 'express'; import cors from 'cors'; import sqlite3 from 'sqlite3'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { createServer } from 'http'; import { Server as SocketIOServer } from 'socket.io'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = process.env.PORT || 3010; const server = createServer(app); const io = new SocketIOServer(server, { cors: { origin: function(origin, callback) { callback(null, true); }, credentials: true, methods: ['GET', 'POST'] } }); // 数据库文件路径 const DB_PATH = path.join(__dirname, 'lottery.db'); // 中间件 app.use(cors({ origin: function(origin, callback) { // 允许所有来源,包括localhost和IP地址访问 callback(null, true); }, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); // JSON解析中间件,添加错误处理 app.use(express.json({ limit: '10mb' })); app.use((err, req, res, next) => { if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { console.error('JSON解析错误:', err.message); return res.status(400).json({ success: false, error: 'Invalid JSON format', message: '请求数据格式错误' }); } next(); }); app.use(express.static(path.join(__dirname, 'dist'))); // 添加预检请求处理 app.options('*', cors()); // 请求日志中间件 app.use((req, res, next) => { console.log(`${new Date().toISOString()} ${req.method} ${req.path}`); next(); }); // 数据库连接 let db; // 初始化数据库 function initDatabase() { return new Promise((resolve, reject) => { db = new sqlite3.Database(DB_PATH, (err) => { if (err) { // 数据库连接失败 reject(err); return; } // SQLite数据库连接成功 // 创建表结构 createTables() .then(() => { // 数据库表创建完成 return initDefaultData(); }) .then(() => { // 默认数据初始化完成 resolve(); }) .catch(reject); }); }); } // 创建表结构 function createTables() { return new Promise((resolve, reject) => { const tables = [ `CREATE TABLE IF NOT EXISTS system_config ( id TEXT PRIMARY KEY, admin_password TEXT NOT NULL, login_password TEXT NOT NULL, max_draw_times INTEGER NOT NULL, background_config TEXT, hide_positions TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL )`, `CREATE TABLE IF NOT EXISTS prizes ( id TEXT PRIMARY KEY, prize_name TEXT NOT NULL, prize_level INTEGER NOT NULL, total_quantity INTEGER NOT NULL, remaining_quantity INTEGER NOT NULL, probability REAL NOT NULL, is_active BOOLEAN NOT NULL, created_at TEXT NOT NULL )`, `CREATE TABLE IF NOT EXISTS students ( student_id TEXT PRIMARY KEY, draw_count INTEGER NOT NULL DEFAULT 0, first_draw_at TEXT, last_draw_at TEXT )`, `CREATE TABLE IF NOT EXISTS records ( id TEXT PRIMARY KEY, student_id TEXT NOT NULL, prize_id TEXT NOT NULL, prize_name TEXT NOT NULL, draw_time TEXT NOT NULL, FOREIGN KEY (student_id) REFERENCES students (student_id), FOREIGN KEY (prize_id) REFERENCES prizes (id) )` ]; let completed = 0; tables.forEach((sql, index) => { db.run(sql, (err) => { if (err) { // 创建表失败 reject(err); return; } completed++; if (completed === tables.length) { resolve(); } }); }); }); } // 初始化默认数据 function initDefaultData() { return new Promise((resolve, reject) => { // 检查是否已有系统配置 db.get('SELECT * FROM system_config LIMIT 1', (err, row) => { if (err) { reject(err); return; } if (!row) { // 插入默认系统配置 const defaultConfig = { id: generateId(), admin_password: '123456', login_password: '123456', max_draw_times: 1, background_config: '{}', hide_positions: '3,4,5,6', created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; db.run( `INSERT INTO system_config (id, admin_password, login_password, max_draw_times, background_config, hide_positions, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [defaultConfig.id, defaultConfig.admin_password, defaultConfig.login_password, defaultConfig.max_draw_times, defaultConfig.background_config, defaultConfig.hide_positions, defaultConfig.created_at, defaultConfig.updated_at], (err) => { if (err) { reject(err); return; } initDefaultPrizes().then(resolve).catch(reject); } ); } else { initDefaultPrizes().then(resolve).catch(reject); } }); }); } // 初始化默认奖项 function initDefaultPrizes() { return new Promise((resolve, reject) => { db.get('SELECT COUNT(*) as count FROM prizes', (err, row) => { if (err) { reject(err); return; } if (row.count === 0) { const defaultPrizes = [ { id: generateId(), prize_name: '一等奖-iPad', prize_level: 1, total_quantity: 1, remaining_quantity: 1, probability: 0.0010, is_active: true, created_at: new Date().toISOString() }, { id: generateId(), prize_name: '二等奖-蓝牙耳机', prize_level: 2, total_quantity: 5, remaining_quantity: 5, probability: 0.0050, is_active: true, created_at: new Date().toISOString() }, { id: generateId(), prize_name: '三等奖-保温杯', prize_level: 3, total_quantity: 20, remaining_quantity: 20, probability: 0.0200, is_active: true, created_at: new Date().toISOString() }, { id: generateId(), prize_name: '谢谢参与', prize_level: 4, total_quantity: 9974, remaining_quantity: 9974, probability: 0.9740, is_active: true, created_at: new Date().toISOString() } ]; let completed = 0; defaultPrizes.forEach(prize => { db.run( `INSERT INTO prizes (id, prize_name, prize_level, total_quantity, remaining_quantity, probability, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [prize.id, prize.prize_name, prize.prize_level, prize.total_quantity, prize.remaining_quantity, prize.probability, prize.is_active, prize.created_at], (err) => { if (err) { reject(err); return; } completed++; if (completed === defaultPrizes.length) { resolve(); } } ); }); } else { resolve(); } }); }); } // 生成唯一ID function generateId() { return Math.random().toString(36).substr(2, 9) + Date.now().toString(36); } // API路由 // 获取系统配置 app.get('/api/system-config', (req, res) => { db.get('SELECT * FROM system_config LIMIT 1', (err, row) => { if (err) { console.error('获取系统配置失败:', err); res.status(500).json({ success: false, error: 'Database Error', message: '获取系统配置失败' }); return; } res.json({ success: true, data: row || null }); }); }); // 更新系统配置 app.put('/api/system-config', (req, res) => { const { admin_password, login_password, max_draw_times, background_config, hide_positions } = req.body; const updated_at = new Date().toISOString(); db.run( `UPDATE system_config SET admin_password = COALESCE(?, admin_password), login_password = COALESCE(?, login_password), max_draw_times = COALESCE(?, max_draw_times), background_config = COALESCE(?, background_config), hide_positions = COALESCE(?, hide_positions), updated_at = ? WHERE id = (SELECT id FROM system_config LIMIT 1)`, [admin_password, login_password, max_draw_times, background_config, hide_positions, updated_at], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, changes: this.changes }); // 广播系统配置更新 if (this.changes > 0) { emitSystemConfigUpdate(); } } ); }); // 获取所有奖项 app.get('/api/prizes', (req, res) => { db.all('SELECT * FROM prizes ORDER BY prize_level', (err, rows) => { if (err) { console.error('获取奖项列表失败:', err); res.status(500).json({ success: false, error: 'Database Error', message: '获取奖项列表失败' }); return; } res.json({ success: true, data: rows || [] }); }); }); // 添加奖项 app.post('/api/prizes', (req, res) => { const { prize_name, prize_level, total_quantity, remaining_quantity, probability, is_active } = req.body; const id = generateId(); const created_at = new Date().toISOString(); db.run( `INSERT INTO prizes (id, prize_name, prize_level, total_quantity, remaining_quantity, probability, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, prize_name, prize_level, total_quantity, remaining_quantity, probability, is_active, created_at], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, id, lastID: this.lastID }); // 广播数据更新 emitDataUpdate(); } ); }); // 更新奖项 app.put('/api/prizes/:id', (req, res) => { const { id } = req.params; const { prize_name, prize_level, total_quantity, remaining_quantity, probability, is_active } = req.body; db.run( `UPDATE prizes SET prize_name = COALESCE(?, prize_name), prize_level = COALESCE(?, prize_level), total_quantity = COALESCE(?, total_quantity), remaining_quantity = COALESCE(?, remaining_quantity), probability = COALESCE(?, probability), is_active = COALESCE(?, is_active) WHERE id = ?`, [prize_name, prize_level, total_quantity, remaining_quantity, probability, is_active, id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, changes: this.changes }); // 广播数据更新 emitDataUpdate(); } ); }); // 删除奖项 app.delete('/api/prizes/:id', (req, res) => { const { id } = req.params; db.run('DELETE FROM prizes WHERE id = ?', [id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, changes: this.changes }); // 广播数据更新 emitDataUpdate(); }); }); // 获取学生信息 app.get('/api/students/:studentId', (req, res) => { const { studentId } = req.params; db.get('SELECT * FROM students WHERE student_id = ?', [studentId], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json(row || null); }); }); // 更新学生抽奖次数 app.put('/api/students/:studentId', (req, res) => { const { studentId } = req.params; const { draw_count } = req.body; const now = new Date().toISOString(); // 先检查学生是否存在 db.get('SELECT * FROM students WHERE student_id = ?', [studentId], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } if (row) { // 更新现有学生 db.run( 'UPDATE students SET draw_count = ?, last_draw_at = ? WHERE student_id = ?', [draw_count, now, studentId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, changes: this.changes }); // 广播学生数据更新 if (this.changes > 0) { emitStudentUpdate(studentId); } } ); } else { // 创建新学生记录 db.run( 'INSERT INTO students (student_id, draw_count, first_draw_at, last_draw_at) VALUES (?, ?, ?, ?)', [studentId, draw_count, now, now], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, lastID: this.lastID }); // 广播新学生创建 emitStudentUpdate(studentId); } ); } }); }); // 获取抽奖记录 app.get('/api/records', (req, res) => { db.all('SELECT * FROM records ORDER BY draw_time DESC', (err, rows) => { if (err) { console.error('获取抽奖记录失败:', err); res.status(500).json({ success: false, error: 'Database Error', message: '获取抽奖记录失败' }); return; } res.json({ success: true, data: rows || [] }); }); }); // 添加抽奖记录 app.post('/api/records', (req, res) => { const { student_id, prize_id, prize_name } = req.body; const id = generateId(); const draw_time = new Date().toISOString(); db.run( 'INSERT INTO records (id, student_id, prize_id, prize_name, draw_time) VALUES (?, ?, ?, ?, ?)', [id, student_id, prize_id, prize_name, draw_time], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, id, lastID: this.lastID }); // 使用优化的增量广播 emitNewRecord(id); // 同时更新相关奖项数据 if (prize_id) { emitPrizeUpdate(prize_id); } } ); }); // 清空抽奖记录 app.delete('/api/records', (req, res) => { db.run('DELETE FROM records', function(err) { if (err) { res.status(500).json({ error: err.message }); return; } // 同时重置学生抽奖次数 db.run('DELETE FROM students', function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, changes: this.changes }); // 广播数据清空更新 emitDataClear(); }); }); }); // 重置系统数据 app.post('/api/reset', (req, res) => { // 清空所有数据表 const tables = ['records', 'students']; let completed = 0; tables.forEach(table => { db.run(`DELETE FROM ${table}`, function(err) { if (err) { res.status(500).json({ error: err.message }); return; } completed++; if (completed === tables.length) { // 重置奖项剩余数量 db.run( 'UPDATE prizes SET remaining_quantity = total_quantity', function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ success: true, message: '系统数据重置成功' }); // 广播系统重置 emitSystemReset(); } ); } }); }); }); // 强制重置数据库 app.post('/api/force-reset', (req, res) => { // 重置所有表数据并恢复默认配置 const tables = ['records', 'students', 'prizes', 'system_config']; let completed = 0; tables.forEach(table => { db.run(`DELETE FROM ${table}`, function(err) { if (err) { res.status(500).json({ error: err.message }); return; } completed++; if (completed === tables.length) { // 重新初始化默认数据 initDefaultData() .then(() => { res.json({ success: true, message: '数据库强制重置成功' }); // 广播强制重置 emitForceReset(); }) .catch(err => { res.status(500).json({ error: err.message }); }); } }); }); }); // 抽奖API - 核心功能 app.post('/api/lottery/draw', (req, res) => { const { studentId } = req.body; if (!studentId) { res.status(400).json({ success: false, message: '学号不能为空' }); return; } console.log(`🎯 收到抽奖请求,学号: ${studentId}`); // 获取系统配置 db.get('SELECT * FROM system_config LIMIT 1', (err, config) => { if (err) { // 获取系统配置失败 res.status(500).json({ success: false, message: '系统配置获取失败' }); return; } if (!config) { res.status(500).json({ success: false, message: '系统配置未找到' }); return; } // 检查学生抽奖次数 db.get('SELECT * FROM students WHERE student_id = ?', [studentId], (err, student) => { if (err) { console.error('获取学生信息失败:', err); res.status(500).json({ success: false, message: '学生信息获取失败' }); return; } const currentDrawCount = student ? student.draw_count : 0; if (currentDrawCount >= config.max_draw_times) { res.status(400).json({ success: false, message: `已达到最大抽奖次数(${config.max_draw_times}次)` }); return; } // 获取可用奖项 db.all('SELECT * FROM prizes WHERE is_active = 1 AND remaining_quantity > 0', (err, prizes) => { if (err) { console.error('获取奖项失败:', err); res.status(500).json({ success: false, message: '奖项获取失败' }); return; } if (prizes.length === 0) { res.status(400).json({ success: false, message: '暂无可用奖项' }); return; } // 加权随机抽奖 const totalWeight = prizes.reduce((sum, prize) => sum + prize.probability, 0); const random = Math.random() * totalWeight; let cumulativeWeight = 0; let selectedPrize = null; for (const prize of prizes) { cumulativeWeight += prize.probability; if (random <= cumulativeWeight) { selectedPrize = prize; break; } } if (!selectedPrize) { res.status(500).json({ success: false, message: '抽奖算法异常' }); return; } console.log(`🎉 抽中奖品: ${selectedPrize.prize_name}`); // 更新奖项剩余数量 const newRemainingQuantity = selectedPrize.remaining_quantity - 1; db.run( 'UPDATE prizes SET remaining_quantity = ? WHERE id = ?', [newRemainingQuantity, selectedPrize.id], function(err) { if (err) { console.error('更新奖项数量失败:', err); res.status(500).json({ success: false, message: '奖项更新失败' }); return; } // 添加抽奖记录 const recordId = generateId(); const drawTime = new Date().toISOString(); db.run( 'INSERT INTO records (id, student_id, prize_id, prize_name, draw_time) VALUES (?, ?, ?, ?, ?)', [recordId, studentId, selectedPrize.id, selectedPrize.prize_name, drawTime], function(err) { if (err) { console.error('添加抽奖记录失败:', err); // 回滚奖项数量 db.run('UPDATE prizes SET remaining_quantity = ? WHERE id = ?', [selectedPrize.remaining_quantity, selectedPrize.id]); res.status(500).json({ success: false, message: '记录添加失败' }); return; } // 更新学生抽奖次数 const newDrawCount = currentDrawCount + 1; const now = new Date().toISOString(); if (student) { // 更新现有学生 db.run( 'UPDATE students SET draw_count = ?, last_draw_at = ? WHERE student_id = ?', [newDrawCount, now, studentId], function(err) { if (err) { console.error('更新学生信息失败:', err); } // 返回抽奖结果 const result = { success: true, message: '抽奖成功', data: { prize: { id: selectedPrize.id, name: selectedPrize.prize_name, level: selectedPrize.prize_level }, remaining: newRemainingQuantity, drawCount: newDrawCount, maxDrawTimes: config.max_draw_times } }; console.log('✅ 抽奖完成,返回结果:', result); res.json(result); // 广播数据更新 emitNewRecord(recordId); emitPrizeUpdate(selectedPrize.id); emitStudentUpdate(studentId); } ); } else { // 创建新学生记录 db.run( 'INSERT INTO students (student_id, draw_count, first_draw_at, last_draw_at) VALUES (?, ?, ?, ?)', [studentId, newDrawCount, now, now], function(err) { if (err) { console.error('创建学生记录失败:', err); } // 返回抽奖结果 const result = { success: true, message: '抽奖成功', data: { prize: { id: selectedPrize.id, name: selectedPrize.prize_name, level: selectedPrize.prize_level }, remaining: newRemainingQuantity, drawCount: newDrawCount, maxDrawTimes: config.max_draw_times } }; console.log('✅ 抽奖完成,返回结果:', result); res.json(result); // 广播数据更新 emitNewRecord(recordId); emitPrizeUpdate(selectedPrize.id); emitStudentUpdate(studentId); } ); } } ); } ); }); }); }); }); // 模拟抽奖 app.post('/api/simulate', (req, res) => { const { times = 500 } = req.body; // 获取奖项列表进行模拟 db.all('SELECT * FROM prizes WHERE is_active = 1', (err, prizes) => { if (err) { res.status(500).json({ error: err.message }); return; } const results = {}; const totalProbability = prizes.reduce((sum, prize) => sum + prize.probability, 0); // 模拟抽奖 for (let i = 0; i < times; i++) { const random = Math.random() * totalProbability; let cumulativeProbability = 0; for (const prize of prizes) { cumulativeProbability += prize.probability; if (random <= cumulativeProbability) { results[prize.prize_name] = (results[prize.prize_name] || 0) + 1; break; } } } // 计算概率 const simulationResult = Object.keys(results).map(prizeName => ({ prizeName, count: results[prizeName], percentage: ((results[prizeName] / times) * 100).toFixed(2) })); res.json({ totalSimulations: times, results: simulationResult, summary: `模拟${times}次抽奖完成` }); }); }); // 全局错误处理中间件 app.use((err, req, res, next) => { console.error('服务器错误:', err); // 数据库错误 if (err.code === 'SQLITE_ERROR' || err.code === 'SQLITE_BUSY') { return res.status(500).json({ success: false, error: 'Database Error', message: '数据库操作失败,请稍后重试' }); } // 默认服务器错误 res.status(500).json({ success: false, error: 'Internal Server Error', message: '服务器内部错误' }); }); // 404处理中间件 app.use('/api/*', (req, res) => { res.status(404).json({ success: false, error: 'API Not Found', message: `API接口 ${req.path} 不存在` }); }); // 处理前端路由 app.get('*', (req, res) => { if (!req.path.startsWith('/api')) { res.sendFile(path.join(__dirname, 'dist', 'index.html')); } }); // WebSocket连接处理 io.on('connection', (socket) => { console.log('客户端连接:', socket.id); // 客户端加入房间 socket.join('lottery_room'); // 处理断开连接 socket.on('disconnect', () => { console.log('客户端断开连接:', socket.id); }); // 处理客户端请求最新数据 socket.on('request_data_update', () => { emitDataUpdate(); }); }); // 广播数据更新(全量) function emitDataUpdate() { // 开始广播数据更新 // 获取最新的奖项数据 db.all('SELECT * FROM prizes ORDER BY prize_level', (err, prizes) => { if (!err && prizes) { // 发送包含updatedPrizes字段的对象,确保前端能正确解析 io.to('lottery_room').emit('prizes_updated', { updatedPrizes: prizes }); // 广播奖项更新 } else { // 获取奖项数据失败 } }); // 获取最新的抽奖记录 db.all('SELECT * FROM records ORDER BY draw_time DESC LIMIT 10', (err, records) => { if (!err && records) { io.to('lottery_room').emit('records_updated', records); // 广播记录更新 } else { // 获取记录数据失败 } }); } // 广播单个奖项更新(优化版) function emitPrizeUpdate(prizeId) { if (prizeId) { db.get('SELECT * FROM prizes WHERE id = ?', [prizeId], (err, prize) => { if (!err && prize) { io.to('lottery_room').emit('prize_updated', { prize, action: 'update' }); // 广播单个奖项更新 } }); } else { // 如果没有指定ID,发送全量数据 emitDataUpdate(); } } // 广播新增抽奖记录(优化版) function emitNewRecord(recordId) { if (recordId) { db.get('SELECT * FROM records WHERE id = ?', [recordId], (err, record) => { if (!err && record) { io.to('lottery_room').emit('new_record', { record, action: 'add' }); // 广播新增抽奖记录 } }); } } // 广播系统配置更新 function emitSystemConfigUpdate() { db.get('SELECT * FROM system_config LIMIT 1', (err, config) => { if (!err && config) { io.to('lottery_room').emit('system_config_updated', config); // 广播系统配置更新 } }); } // 广播学生数据更新 function emitStudentUpdate(studentId) { db.get('SELECT * FROM students WHERE student_id = ?', [studentId], (err, student) => { if (!err) { io.to('lottery_room').emit('student_updated', { studentId, student }); // 广播学生数据更新 } }); } // 广播数据清空 function emitDataClear() { io.to('lottery_room').emit('data_cleared', { type: 'records_and_students', timestamp: new Date().toISOString() }); // 广播数据清空事件 // 同时发送最新的空数据 emitDataUpdate(); } // 广播系统重置 function emitSystemReset() { io.to('lottery_room').emit('system_reset', { type: 'partial_reset', timestamp: new Date().toISOString() }); console.log('广播系统重置事件'); // 发送最新数据 emitDataUpdate(); } // 广播强制重置 function emitForceReset() { io.to('lottery_room').emit('force_reset', { type: 'full_reset', timestamp: new Date().toISOString() }); // 广播强制重置事件 // 发送最新数据 emitDataUpdate(); emitSystemConfigUpdate(); } // 启动服务器 initDatabase() .then(() => { server.listen(PORT, () => { console.log(`服务器运行在端口 ${PORT}`); console.log(`数据库文件位置: ${DB_PATH}`); console.log('WebSocket服务已启动'); }); }) .catch(err => { console.error('服务器启动失败:', err); process.exit(1); }); // 优雅关闭 process.on('SIGINT', () => { console.log('\n正在关闭服务器...'); if (db) { db.close((err) => { if (err) { console.error('关闭数据库连接失败:', err.message); } else { console.log('数据库连接已关闭'); } process.exit(0); }); } else { process.exit(0); } });