18.Python实战:实现年会抽奖系统

目录结构

python/
├── sql/
│    └── table.sql          # 创建数据库及数据表
├── config/
│   └── __init__.py         # 数据库和Flask配置
├── static/
│   ├── style.css           # 样式文件
│   └── script.js           # JavaScript脚本
├── templates/
│   └── index.html          # 主页面模板
└── lucky_draw.py           # 主应用程序

1.table.sql

sql 复制代码
table.sql
CREATE DATABASE lucky_draw;
USE lucky_draw;

CREATE TABLE participants (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    department VARCHAR(50),
    employee_id VARCHAR(20),
    join_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE winners (
    id INT AUTO_INCREMENT PRIMARY KEY,
    participant_id INT,
    draw_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (participant_id) REFERENCES participants(id)
);

2.__init__.py

python 复制代码
DB_CONFIG = {
    'host': 'localhost',
    'user': 'your_username',
    'password': 'your_password',
    'database': 'lucky_draw'
}

3.style.css

css 复制代码
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    margin: 0;
    padding: 20px;
    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
    min-height: 100vh;
}

.container {
    max-width: 1000px;
    margin: 0 auto;
    padding: 30px;
    background-color: white;
    border-radius: 15px;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}

header {
    text-align: center;
    margin-bottom: 40px;
}

h1 {
    color: #2c3e50;
    font-size: 2.5em;
    margin-bottom: 10px;
}

.subtitle {
    color: #7f8c8d;
    font-size: 1.2em;
    margin: 0;
}

h2 {
    color: #34495e;
    font-size: 1.5em;
    margin-bottom: 20px;
}

.section {
    margin: 30px 0;
    padding: 25px;
    border-radius: 10px;
    background-color: #f8f9fa;
    box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}

.draw-section {
    background: linear-gradient(to right, #fff5f5, #fff0f0);
}

.form-inline {
    display: flex;
    justify-content: center;
}

.input-group {
    display: flex;
    align-items: center;
    gap: 10px;
}

.input-group input {
    padding: 12px;
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    font-size: 1em;
    transition: all 0.3s ease;
}

.input-group input:focus {
    border-color: #3498db;
    outline: none;
    box-shadow: 0 0 5px rgba(52,152,219,0.3);
}

.draw-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    align-items: center;
    justify-content: center;
}

.checkbox-group {
    display: flex;
    align-items: center;
    gap: 5px;
}

.btn {
    padding: 12px 24px;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 1em;
    transition: all 0.3s ease;
    display: flex;
    align-items: center;
    gap: 8px;
}

.btn-primary {
    background-color: #3498db;
    color: white;
}

.btn-primary:hover {
    background-color: #2980b9;
    transform: translateY(-2px);
}

.btn-danger {
    background-color: #e74c3c;
    color: white;
}

.btn-danger:hover {
    background-color: #c0392b;
}

.reset-form {
    text-align: center;
    margin-top: 20px;
}

.dashboard {
    display: grid;
    grid-template-columns: 1fr;
    gap: 30px;
    margin-top: 40px;
}

.name-list {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    padding: 15px;
    background-color: white;
    border-radius: 8px;
    min-height: 50px;
}

.name-tag {
    padding: 8px 16px;
    background-color: #f0f2f5;
    border-radius: 20px;
    font-size: 0.9em;
    display: flex;
    align-items: center;
    gap: 8px;
    transition: all 0.3s ease;
}

.name-tag:hover {
    transform: translateY(-2px);
}

.winner {
    background: linear-gradient(45deg, #ffd700, #ffa500);
    color: #000;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.alert {
    padding: 15px 20px;
    margin: 20px 0;
    border-radius: 8px;
    background-color: #d4edda;
    color: #155724;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    animation: slideIn 0.5s ease;
}

.count {
    font-size: 0.8em;
    color: #666;
    font-weight: normal;
}

footer {
    text-align: center;
    margin-top: 40px;
    color: #7f8c8d;
}

@keyframes slideIn {
    from {
        transform: translateY(-20px);
        opacity: 0;
    }
    to {
        transform: translateY(0);
        opacity: 1;
    }
}

@media (max-width: 768px) {
    .container {
        padding: 20px;
    }
    
    .draw-controls {
        flex-direction: column;
    }
    
    .input-group {
        width: 100%;
    }
}

/* 深色模式 */
[data-theme="dark"] {
    background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
}

[data-theme="dark"] .container {
    background-color: #2c3e50;
    color: #ecf0f1;
}

[data-theme="dark"] .section {
    background-color: #34495e;
}

[data-theme="dark"] .draw-section {
    background: linear-gradient(to right, #2c3e50, #34495e);
}

[data-theme="dark"] h1,
[data-theme="dark"] h2 {
    color: #ecf0f1;
}

[data-theme="dark"] .name-tag {
    background-color: #465c74;
    color: #ecf0f1;
}

/* 主题切换开关 */
.theme-switch {
    position: fixed;
    top: 20px;
    right: 20px;
    display: flex;
    align-items: center;
    gap: 10px;
}

.switch {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 34px;
}

.switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #ccc;
    transition: .4s;
}

.slider:before {
    position: absolute;
    content: "";
    height: 26px;
    width: 26px;
    left: 4px;
    bottom: 4px;
    background-color: white;
    transition: .4s;
}

input:checked + .slider {
    background-color: #2196F3;
}

input:checked + .slider:before {
    transform: translateX(26px);
}

.slider.round {
    border-radius: 34px;
}

.slider.round:before {
    border-radius: 50%;
}

/* 抽奖动画 */
.lottery-animation {
    margin: 20px 0;
    padding: 20px;
    text-align: center;
}

.lottery-box {
    position: relative;
    overflow: hidden;
    display: inline-block;
    padding: 30px 60px;
    background: linear-gradient(45deg, #f1c40f, #f39c12);
    border-radius: 15px;
    box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    animation: pulse 1.5s infinite;
}

.rolling-name-text {
    font-size: 2.5em;
    color: #fff;
    text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
    margin-bottom: 10px;
}

.rolling-dept-text {
    font-size: 1.2em;
    color: rgba(255, 255, 255, 0.9);
    text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
}

@keyframes pulse {
    0% {
        transform: scale(1);
        box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    50% {
        transform: scale(1.05);
        box-shadow: 0 8px 25px rgba(0,0,0,0.3);
    }
    100% {
        transform: scale(1);
        box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
}

/* 优化中奖名单样式 */
.winners-section .name-tag {
    font-size: 1.2em;
    padding: 10px 20px;
    background: linear-gradient(45deg, #ffd700, #ffa500);
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    transition: all 0.3s ease;
}

.winners-section .name-tag:hover {
    transform: translateY(-3px) rotate(3deg);
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

/* 统计图表 */
.charts-container {
    display: flex;
    justify-content: center;
    margin-top: 20px;
}

.chart-wrapper {
    width: 300px;
    height: 300px;
}

/* 响应式调整 */
@media (max-width: 768px) {
    .chart-wrapper {
        width: 100%;
        height: auto;
    }
}

/* 中奖高亮效果 */
.winner-highlight {
    animation: winner-glow 1s ease-in-out infinite alternate;
    transform: scale(1.1);
    transition: all 0.3s ease;
}

@keyframes winner-glow {
    from {
        box-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #e60073, 0 0 40px #e60073;
    }
    to {
        box-shadow: 0 0 20px #fff, 0 0 30px #ff4da6, 0 0 40px #ff4da6, 0 0 50px #ff4da6;
    }
}

/* 庆祝效果 */
.celebration {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    background: rgba(0, 0, 0, 0.8);
    animation: fadeIn 0.3s ease-out;
    border-radius: 15px;
}

.celebration-content {
    text-align: center;
    animation: popIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

.celebration-icon {
    font-size: 4em;
    margin-bottom: 10px;
    animation: bounce 1s infinite;
}

.winner-name {
    font-size: 2.5em;
    color: #fff;
    text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
    margin-bottom: 5px;
}

.winner-dept {
    font-size: 1.2em;
    color: rgba(255, 255, 255, 0.9);
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes popIn {
    0% {
        transform: scale(0.3);
        opacity: 0;
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

@keyframes bounce {
    0%, 100% {
        transform: translateY(0);
    }
    50% {
        transform: translateY(-20px);
    }
} 

4.script.js

javascript 复制代码
// 主题切换
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;

themeToggle.addEventListener('change', () => {
    if (themeToggle.checked) {
        html.setAttribute('data-theme', 'dark');
        localStorage.setItem('theme', 'dark');
    } else {
        html.setAttribute('data-theme', 'light');
        localStorage.setItem('theme', 'light');
    }
});

// 加载保存的主题
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
themeToggle.checked = savedTheme === 'dark';

// 抽奖动画
const startDrawBtn = document.getElementById('start-draw');
const drawForm = document.getElementById('draw-form');
const rollingName = document.getElementById('rolling-name');
const rollingSound = document.getElementById('rolling-sound');
const winnerSound = document.getElementById('winner-sound');

let isRolling = false;

function getAvailableParticipants() {
    const excludeWinners = document.querySelector('input[name="exclude_winners"]').checked;
    if (excludeWinners) {
        return participants.filter(p => !p.is_winner);
    }
    return participants;
}

function showParticipant(participant) {
    rollingName.innerHTML = `
        <div class="rolling-name-text">${participant.name}</div>
        <div class="rolling-dept-text">${participant.department}</div>
    `;
}

function getRandomParticipant(available) {
    const randomIndex = Math.floor(Math.random() * available.length);
    return available[randomIndex];
}

// 随机抽取指定数量的参与者
function getRandomWinners(available, count) {
    const shuffled = [...available].sort(() => 0.5 - Math.random());
    return shuffled.slice(0, Math.min(count, available.length));
}

startDrawBtn.addEventListener('click', async () => {
    if (isRolling) return;
    
    const available = getAvailableParticipants();
    if (available.length === 0) {
        alert('没有合适的抽奖人选!');
        return;
    }

    isRolling = true;
    startDrawBtn.disabled = true;
    rollingSound.currentTime = 0;
    rollingSound.play();
    
    // 先决定中奖者
    const numWinners = parseInt(document.querySelector('input[name="num_winners"]').value);
    const winners = getRandomWinners(available, numWinners);
    const finalWinner = winners[0]; // 显示第一个中奖者的动画
    
    // 开始动画
    let duration = 3000; // 总持续时间
    let interval = 50; // 初始间隔时间
    let startTime = Date.now();
    
    function roll() {
        let currentTime = Date.now();
        let elapsed = currentTime - startTime;
        
        // 逐渐减慢滚动速度
        interval = Math.min(500, 50 + (elapsed / duration) * 450);
        
        // 最后一次显示实际中奖者
        if (elapsed >= duration - interval) {
            // 显示最终中奖者
            showParticipant(finalWinner);
            
            // 添加中奖效果
            rollingName.classList.add('winner-highlight');
            
            // 停止动画
            rollingSound.pause();
            winnerSound.play();
            
            // 显示庆祝效果
            showCelebration(finalWinner);
            
            // 将中奖者ID添加到表单
            const winnerIdsInput = document.createElement('input');
            winnerIdsInput.type = 'hidden';
            winnerIdsInput.name = 'winner_ids';
            winnerIdsInput.value = winners.map(w => w.id).join(',');
            drawForm.appendChild(winnerIdsInput);
            
            // 延迟提交表单
            setTimeout(() => {
                rollingName.classList.remove('winner-highlight');
                drawForm.submit();
            }, 2000);
            
            return;
        }
        
        // 随机显示参与者
        showParticipant(getRandomParticipant(available));
        
        if (elapsed < duration) {
            setTimeout(roll, interval);
        }
    }
    
    roll();
});

// 显示庆祝效果
function showCelebration(winner) {
    // 创建庆祝动画容器
    const celebration = document.createElement('div');
    celebration.className = 'celebration';
    
    // 添加中奖信息
    celebration.innerHTML = `
        <div class="celebration-content">
            <div class="celebration-icon">🎉</div>
            <div class="winner-name">${winner.name}</div>
            <div class="winner-dept">${winner.department}</div>
        </div>
    `;
    
    // 添加到页面
    document.querySelector('.lottery-animation').appendChild(celebration);
    
    // 2秒后移除庆祝效果
    setTimeout(() => {
        celebration.remove();
    }, 2000);
}

// 数据可视化
const ctx = document.getElementById('winnersPieChart').getContext('2d');
new Chart(ctx, {
    type: 'pie',
    data: {
        labels: ['已中奖', '未中奖'],
        datasets: [{
            data: [winners.length, participants.length - winners.length],
            backgroundColor: [
                'rgba(255, 206, 86, 0.8)',
                'rgba(75, 192, 192, 0.8)'
            ]
        }]
    },
    options: {
        responsive: true,
        plugins: {
            legend: {
                position: 'bottom'
            },
            title: {
                display: true,
                text: '中奖情况统计'
            }
        }
    }
});

// 添加动画效果
document.querySelectorAll('.name-tag').forEach(tag => {
    tag.addEventListener('mouseover', () => {
        tag.style.transform = 'scale(1.1) rotate(5deg)';
    });
    
    tag.addEventListener('mouseout', () => {
        tag.style.transform = 'translateY(-2px)';
    });
}); 

5.index.html

html 复制代码
<!DOCTYPE html>
<html data-theme="light">
<head>
     <!-- 设置网页标题 -->
    <title>年会抽奖系统</title>
    <!-- 引入自定义的 CSS 样式文件,使用 Flask 的 url_for 函数生成静态文件的 URL -->
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <!-- 引入 Font Awesome 图标库,用于显示各种图标 -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <!-- 引入 Chart.js 库,用于绘制图表 -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
    <!-- 主题切换区域 -->
    <div class="theme-switch">
        <!-- 显示月亮图标,代表暗色模式 -->
        <i class="fas fa-moon"></i>
        <!-- 切换开关 -->
        <label class="switch">
            <!-- 复选框,用于切换主题 -->
            <input type="checkbox" id="theme-toggle">
            <!-- 开关滑块 -->
            <span class="slider round"></span>
        </label>
        <!-- 显示太阳图标,代表亮色模式 -->
        <i class="fas fa-sun"></i>
    </div>

    <div class="container">
        <header>
            <h1><i class="fas fa-gift"></i> 年会抽奖系统</h1>
            <p class="subtitle">让我们看看谁是今天的幸运儿!</p>
        </header>

        <!-- 获取并显示 Flask 闪现消息 -->
        {% with messages = get_flashed_messages() %}
            <!-- 如果有闪现消息 -->
            {% if messages %}
                <!-- 遍历每条消息 -->
                {% for message in messages %}
                    <!-- 显示消息的提示框 -->
                    <div class="alert">
                        <!-- 显示铃铛图标 -->
                        <i class="fas fa-bell"></i>
                        <!-- 显示消息内容 -->
                        {{ message }}
                    </div>
                {% endfor %}
            {% endif %}
        {% endwith %}


        <div class="section">
            <h2><i class="fas fa-user-plus"></i> 添加参与者</h2>
            <form method="POST" action="{{ url_for('add_participant') }}" class="form-inline">
                <div class="input-group">
                    <input type="text" name="name" placeholder="姓名" required>
                    <input type="text" name="department" placeholder="部门" required>
                    <input type="text" name="employee_id" placeholder="工号" required>
                    <button type="submit" class="btn">
                        <i class="fas fa-plus"></i> 添加
                    </button>
                </div>
            </form>
        </div>

        <div class="section draw-section">
            <h2><i class="fas fa-random"></i> 抽奖</h2>
            <div id="lottery-animation" class="lottery-animation">
                <div class="lottery-box">
                    <div class="lottery-name" id="rolling-name">准备开始</div>
                </div>
            </div>
            <form id="draw-form" method="POST" action="{{ url_for('draw') }}" class="form-inline">
                <div class="draw-controls">
                    <div class="input-group">
                        <label>抽取人数:</label>
                        <input type="number" name="num_winners" value="1" min="1" required>
                    </div>
                    <div class="checkbox-group">
                        <label>
                            <input type="checkbox" name="exclude_winners" value="true" checked>
                            <span>排除已中奖者</span>
                        </label>
                    </div>
                    <button type="button" class="btn btn-primary" id="start-draw">
                        <i class="fas fa-dice"></i> 开始抽奖
                    </button>
                </div>
            </form>
            <form method="POST" action="{{ url_for('reset') }}" class="reset-form">
                <button type="submit" class="btn btn-danger">
                    <i class="fas fa-redo"></i> 重置中奖记录
                </button>
            </form>
        </div>

        <div class="dashboard">
            <div class="section participants-section">
                <h2>
                    <i class="fas fa-users"></i>
                    参与者名单
                    <span class="count">({{ participants|length }}人)</span>
                </h2>
                {% for dept, members in participants|groupby('department') %}
                <div class="department-group">
                    <h3>{{ dept }} ({{ members|length }}人)</h3>
                    <div class="name-list">
                        {% for p in members %}
                        <span class="name-tag {% if p.is_winner %}winner{% endif %}">
                            <i class="fas fa-user"></i>
                            {{ p.name }}
                            <small>{{ p.employee_id }}</small>
                        </span>
                        {% endfor %}
                    </div>
                </div>
                {% endfor %}
            </div>

            <div class="section winners-section">
                <h2>
                    <i class="fas fa-crown"></i>
                    中奖名单
                    <span class="count">({{ winners|length }}人)</span>
                </h2>
                <div class="name-list">
                    {% for winner in winners %}
                    <span class="name-tag winner">
                        <i class="fas fa-star"></i>
                        {{ winner.name }}
                    </span>
                    {% endfor %}
                </div>
            </div>

            <div class="section stats-section">
                <h2><i class="fas fa-chart-pie"></i> 数据统计</h2>
                <div class="charts-container">
                    <div class="chart-wrapper">
                        <canvas id="winnersPieChart"></canvas>
                    </div>
                    <div class="chart-wrapper">
                        <canvas id="departmentChart"></canvas>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <footer>
        <p>祝大家好运!</p>
    </footer>
    
    <audio id="rolling-sound" preload="auto"></audio>
    <audio id="winner-sound" preload="auto"></audio>
    
    <script>
        // 传递Python数据到JavaScript
        const participants = {{ participants|tojson|safe }};
        const winners = {{ winners|tojson|safe }};
    </script>
    <script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

5.lucky_draw.py

python 复制代码
# 从 flask 模块导入必要的类和函数
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash
# 导入 mysql.connector 模块用于连接和操作 MySQL 数据库
import mysql.connector
# 导入 random 模块用于生成随机数,用于抽奖功能
import random
# 从 config 模块导入 DB_CONFIG,DB_CONFIG 存储了数据库连接信息
from config import DB_CONFIG  # 创建配置文件存储数据库连接信息

# 创建 Flask 应用实例
app = Flask(__name__)
# 设置应用的密钥,用于会话管理和闪现消息
app.secret_key = 'your_secret_key'


# 定义 Database 类,用于管理数据库连接
class Database:
    def __init__(self):
        # 初始化数据库连接对象
        self.conn = None
        # 调用 connect 方法建立数据库连接
        self.connect()

    def connect(self):
        try:
            # 使用 DB_CONFIG 中的配置信息建立数据库连接
            self.conn = mysql.connector.connect(**DB_CONFIG)
        except Exception as e:
            # 若连接失败,打印错误信息
            print(f"数据库连接错误:{str(e)}")

    def get_connection(self):
        try:
            # 检查数据库连接是否正常,若不正常则尝试重连,最多尝试 3 次,每次间隔 5 秒
            self.conn.ping(reconnect=True, attempts=3, delay=5)
        except:
            # 若重连失败,重新调用 connect 方法建立连接
            self.connect()
        # 返回数据库连接对象
        return self.conn


# 定义 LuckyDraw 类,封装抽奖系统的主要功能
class LuckyDraw:
    def __init__(self):
        # 创建 Database 类的实例,用于管理数据库连接
        self.db = Database()

    def get_all_participants(self):
        """获取所有参与者"""
        # 获取数据库连接对象
        conn = self.db.get_connection()
        # 创建游标对象,设置返回结果为字典形式
        cursor = conn.cursor(dictionary=True)
        try:
            # 执行 SQL 查询,获取所有参与者信息,并标记是否为中奖者
            cursor.execute("""
                SELECT p.*, CASE WHEN w.id IS NOT NULL THEN 1 ELSE 0 END as is_winner 
                FROM participants p 
                LEFT JOIN winners w ON p.id = w.participant_id
                ORDER BY p.department, p.name
            """)
            # 获取查询结果
            return cursor.fetchall()
        except Exception as e:
            # 若查询失败,打印错误信息
            print(f"获取参与者失败:{str(e)}")
            # 返回空列表
            return []
        finally:
            # 关闭游标
            cursor.close()

    def get_winners(self):
        """获取所有中奖者"""
        # 获取数据库连接对象
        conn = self.db.get_connection()
        # 创建游标对象,设置返回结果为字典形式
        cursor = conn.cursor(dictionary=True)
        try:
            # 执行 SQL 查询,获取所有中奖者信息,并按抽奖时间降序排列
            cursor.execute("""
                SELECT p.*, w.draw_time 
                FROM winners w 
                JOIN participants p ON w.participant_id = p.id 
                ORDER BY w.draw_time DESC
            """)
            # 获取查询结果
            return cursor.fetchall()
        except Exception as e:
            # 若查询失败,打印错误信息
            print(f"获取中奖者失败:{str(e)}")
            # 返回空列表
            return []
        finally:
            # 关闭游标
            cursor.close()

    def add_participant(self, name, department, employee_id):
        """添加参与者"""
        # 获取数据库连接对象
        conn = self.db.get_connection()
        # 创建游标对象
        cursor = conn.cursor()
        try:
            # 执行 SQL 插入语句,将参与者信息插入到 participants 表中
            cursor.execute("""
                INSERT INTO participants (name, department, employee_id) 
                VALUES (%s, %s, %s)
            """, (name, department, employee_id))
            # 提交数据库事务
            conn.commit()
            # 返回添加成功标志
            return True
        except Exception as e:
            # 若插入失败,打印错误信息
            print(f"添加参与者失败:{str(e)}")
            # 回滚数据库事务
            conn.rollback()
            # 返回添加失败标志
            return False
        finally:
            # 关闭游标
            cursor.close()

    def draw(self, num_winners=1, exclude_winners=True):
        """抽奖"""
        # 获取数据库连接对象
        conn = self.db.get_connection()
        # 创建游标对象,设置返回结果为字典形式
        cursor = conn.cursor(dictionary=True)
        try:
            # 获取可抽奖的参与者
            if exclude_winners:
                # 若排除已中奖者,执行 SQL 查询,获取未中奖的参与者
                cursor.execute("""
                    SELECT p.* FROM participants p 
                    LEFT JOIN winners w ON p.id = w.participant_id 
                    WHERE w.id IS NULL
                """)
            else:
                # 若不排除已中奖者,执行 SQL 查询,获取所有参与者
                cursor.execute("SELECT * FROM participants")

            # 获取查询结果
            available = cursor.fetchall()
            if not available:
                # 若没有可抽奖的参与者,返回空列表
                return []

            # 随机抽取
            # 从可抽奖的参与者中随机抽取指定数量的中奖者
            winners = random.sample(available, min(num_winners, len(available)))

            # 记录中奖者
            for winner in winners:
                # 执行 SQL 插入语句,将中奖者信息插入到 winners 表中
                cursor.execute("""
                    INSERT INTO winners (participant_id) VALUES (%s)
                """, (winner['id'],))

            # 提交数据库事务
            conn.commit()
            # 返回中奖者列表
            return winners

        except Exception as e:
            # 若抽奖失败,打印错误信息
            print(f"抽奖失败:{str(e)}")
            # 回滚数据库事务
            conn.rollback()
            # 返回空列表
            return []
        finally:
            # 关闭游标
            cursor.close()

    def reset_winners(self):
        """重置中奖记录"""
        # 获取数据库连接对象
        conn = self.db.get_connection()
        # 创建游标对象
        cursor = conn.cursor()
        try:
            # 执行 SQL 语句,清空 winners 表中的数据
            cursor.execute("TRUNCATE TABLE winners")
            # 提交数据库事务
            conn.commit()
            # 返回重置成功标志
            return True
        except Exception as e:
            # 若重置失败,打印错误信息
            print(f"重置中奖记录失败:{str(e)}")
            # 回滚数据库事务
            conn.rollback()
            # 返回重置失败标志
            return False
        finally:
            # 关闭游标
            cursor.close()


# 创建抽奖系统实例
lucky_draw = LuckyDraw()


# 定义首页路由
@app.route('/')
def index():
    """首页"""
    # 获取所有参与者信息
    participants = lucky_draw.get_all_participants()
    # 获取所有中奖者信息
    winners = lucky_draw.get_winners()

    # 按部门统计数据
    department_stats = {}
    for p in participants:
        # 获取参与者所在部门
        dept = p['department']
        if dept not in department_stats:
            # 若部门不在统计字典中,初始化该部门的统计信息
            department_stats[dept] = {'total': 0, 'winners': 0}
        # 该部门总人数加 1
        department_stats[dept]['total'] += 1
        if p['is_winner']:
            # 若参与者为中奖者,该部门中奖人数加 1
            department_stats[dept]['winners'] += 1

    # 渲染 index.html 模板,并传递参与者、中奖者和部门统计信息
    return render_template('index.html',
                           participants=participants,
                           winners=winners,
                           department_stats=department_stats)


# 定义添加参与者的路由,只接受 POST 请求
@app.route('/add_participant', methods=['POST'])
def add_participant():
    """添加参与者"""
    # 获取表单提交的姓名,并去除前后空格
    name = request.form.get('name', '').strip()
    # 获取表单提交的部门,并去除前后空格
    department = request.form.get('department', '').strip()
    # 获取表单提交的员工编号,并去除前后空格
    employee_id = request.form.get('employee_id', '').strip()

    if name and department and employee_id:
        # 若姓名、部门和员工编号都不为空
        if lucky_draw.add_participant(name, department, employee_id):
            # 若添加参与者成功,闪现成功消息
            flash(f'成功添加参与者:{name}')
        else:
            # 若添加参与者失败,闪现失败消息
            flash('添加参与者失败')
    else:
        # 若信息不完整,闪现提示消息
        flash('请填写完整信息')
    # 重定向到首页
    return redirect(url_for('index'))


# 定义抽奖的路由,只接受 POST 请求
@app.route('/draw', methods=['POST'])
def draw():
    """进行抽奖"""
    # 获取表单提交的中奖人数,默认为 1
    num_winners = int(request.form.get('num_winners', 1))
    # 获取表单提交的是否排除已中奖者的标志,默认为 true
    exclude_winners = request.form.get('exclude_winners', 'true') == 'true'
    # 获取表单提交的中奖者 ID 列表,以逗号分隔
    winner_ids = request.form.get('winner_ids', '').split(',')

    if winner_ids and winner_ids[0]:
        # 若中奖者 ID 列表不为空
        # 获取数据库连接对象
        conn = lucky_draw.db.get_connection()
        # 创建游标对象
        cursor = conn.cursor()
        try:
            # 记录中奖者
            for winner_id in winner_ids:
                # 执行 SQL 插入语句,将中奖者信息插入到 winners 表中
                cursor.execute("""
                    INSERT INTO winners (participant_id) VALUES (%s)
                """, (int(winner_id),))
            # 提交数据库事务
            conn.commit()

            # 获取中奖者信息
            cursor.execute("""
                SELECT name FROM participants 
                WHERE id IN (%s)
            """ % ','.join(['%s'] * len(winner_ids)), tuple(map(int, winner_ids)))

            # 获取中奖者姓名列表
            winner_names = [row[0] for row in cursor.fetchall()]
            # 闪现中奖消息
            flash(f'恭喜中奖者:{", ".join(winner_names)}')
        except Exception as e:
            # 若记录中奖失败,打印错误信息
            print(f"记录中奖失败:{str(e)}")
            # 回滚数据库事务
            conn.rollback()
            # 闪现错误消息
            flash('抽奖过程出现错误')
        finally:
            # 关闭游标
            cursor.close()
    else:
        # 若没有合适的抽奖人选,闪现提示消息
        flash('没有合适的抽奖人选')
    # 重定向到首页
    return redirect(url_for('index'))


# 定义重置中奖记录的路由,只接受 POST 请求
@app.route('/reset', methods=['POST'])
def reset():
    """重置中奖记录"""
    if lucky_draw.reset_winners():
        # 若重置中奖记录成功,闪现成功消息
        flash('已重置所有中奖记录')
    else:
        # 若重置中奖记录失败,闪现失败消息
        flash('重置失败')
    # 重定向到首页
    return redirect(url_for('index'))


# 若该脚本作为主程序运行
if __name__ == '__main__':
    # 启动 Flask 应用,开启调试模式
    app.run(debug=True)
相关推荐
鸡鸭扣35 分钟前
Docker:3、在VSCode上安装并运行python程序或JavaScript程序
运维·vscode·python·docker·容器·js
paterWang1 小时前
基于 Python 和 OpenCV 的酒店客房入侵检测系统设计与实现
开发语言·python·opencv
东方佑1 小时前
使用Python和OpenCV实现图像像素压缩与解压
开发语言·python·opencv
神秘_博士2 小时前
自制AirTag,支持安卓/鸿蒙/PC/Home Assistant,无需拥有iPhone
arm开发·python·物联网·flutter·docker·gitee
Moutai码农3 小时前
机器学习-生命周期
人工智能·python·机器学习·数据挖掘
小白教程4 小时前
python学习笔记,python处理 Excel、Word、PPT 以及邮件自动化办公
python·python学习·python安装
武陵悭臾4 小时前
网络爬虫学习:借助DeepSeek完善爬虫软件,实现模拟鼠标右键点击,将链接另存为本地文件
python·selenium·网络爬虫·pyautogui·deepseek·鼠标右键模拟·保存链接为htm
代码猪猪傻瓜coding5 小时前
关于 形状信息提取的说明
人工智能·python·深度学习
码界筑梦坊6 小时前
基于Flask的第七次人口普查数据分析系统的设计与实现
后端·python·信息可视化·flask·毕业设计
微笑的Java7 小时前
Python - 爬虫利器 - BeautifulSoup4常用 API
开发语言·爬虫·python