1062 lines
30 KiB
JavaScript
1062 lines
30 KiB
JavaScript
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 || 3001;
|
||
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);
|
||
}
|
||
}); |