年会抽奖系统

年会抽奖系统 - 完整技术实现

一个基于 Spring Boot + Vue.js 的实时互动年会抽奖系统,支持二维码报名、九宫格抽奖、实时数据同步等功能。


目录


项目概述

这是一个为企业年会量身打造的抽奖系统,提供了从参与者报名到抽奖结果展示的完整流程。系统采用前后端分离架构,支持高并发访问,实时数据同步,为年会现场营造热烈的互动氛围。


核心功能

1. 参与者报名系统

功能描述:

  • 扫描二维码进入报名页面

  • 填写姓名、部门等信息

  • 防重复提交保护(同一设备/IP只能提交一次)

  • 高并发报名支持(信号量限流 + 批量写入)

技术实现:

复制代码
// 前端防抖处理
submitForm() {
    // 防止重复点击
    if (this.isSubmitting) {
        return;
    }
    
    // 2秒内不能重复提交(防抖)
    const now = Date.now();
    if (now - this.lastSubmitTime < 2000) {
        this.$message.warning('请勿频繁提交!');
        return;
    }
    
    this.isSubmitting = true;
    this.lastSubmitTime = now;
    
    // 提交逻辑...
}

2. 实时参与统计

功能描述:

  • 实时显示总参与人数、已中奖人数、待中奖人数

  • 新加入参与者动态提示(NEW标记 + 动画效果)

  • 自动滚动展示参与者列表(类似直播弹幕效果)

技术实现:

复制代码
// WebSocket 实时推送
updateParticipantsData(data) {
    const newParticipants = data.participants || [];
    const oldParticipants = this.participants;
    
    // 标记新加入的参与者
    newParticipants.forEach(newP => {
        const isNewUser = !oldParticipants.some(
            oldP => oldP.name === newP.name
        );
        if (isNewUser) {
            newP.isNew = true;
            // 3秒后移除NEW标记
            setTimeout(() => {
                newP.isNew = false;
            }, 3000);
        }
    });
    
    this.participants = newParticipants;
}

3. 九宫格抽奖动画

功能描述:

  • 九宫格布局展示所有奖品

  • 点击开始按钮,奖品高速旋转切换

  • 减速停止效果,增强期待感

  • 中奖弹窗 + 撒花动效 + 音效

技术实现:

复制代码
// 九宫格抽奖核心逻辑
function startLottery() {
    const cells = document.querySelectorAll('.grid-cell');
    let speed = 50; // 初始速度
    
    lotteryInterval = setInterval(() => {
        // 移除上一个高亮
        cells[currentIndex].classList.remove('active');
        
        // 计算下一个位置(跳过中心格)
        currentIndex = (currentIndex + 1) % 9;
        if (currentIndex === 4) currentIndex = 5;
        
        // 添加新高亮
        cells[currentIndex].classList.add('active');
        
        // 逐渐减速
        speed *= 1.05;
    }, speed);
}
​
// 停止并展示结果
function endLottery() {
    clearInterval(lotteryInterval);
    
    // 调用后端抽奖接口
    fetch('/api/draw', { method: 'POST' })
        .then(response => response.json())
        .then(data => {
            const winner = data.participant;
            
            // 保存中奖记录
            saveWinner(winner, prize);
            
            // 显示中奖弹窗
            showWinModal(winner, prize);
            
            // 创建撒花效果
            createConfetti();
        });
}

4. 中奖记录展示

功能描述:

  • 卡片式布局,展示奖品图片

  • 显示中奖者姓名、部门、时间

  • 悬停效果增强视觉交互

  • 实时更新中奖列表

页面结构:

复制代码
<div class="history-item">
    <!-- 奖品图片 -->
    <div class="history-prize-image">
        <img :src="winner.prizeIcon" :alt="winner.prize">
        <div class="prize-name-overlay">{{ winner.prize }}</div>
    </div>
    
    <!-- 获奖者信息 -->
    <div class="history-info">
        <div class="history-name">
            <i class="el-icon-user-solid"></i>
            {{ winner.name }}
        </div>
        <div class="history-department">
            <i class="el-icon-office-building"></i>
            {{ winner.department }}
        </div>
        <div class="history-time">
            <i class="el-icon-time"></i>
            {{ formatTime(winner.time) }}
        </div>
    </div>
</div>

技术栈

后端技术

技术 版本 说明
Spring Boot 3.x 核心框架
Java 17+ 开发语言
Jackson 2.x JSON处理
WebSocket - 实时通信
Apache POI 5.x Excel导出

前端技术

技术 版本 说明
Vue.js 2.x 渐进式框架
Element UI 2.x UI组件库
WebSocket API - 实时通信
CSS3 Animation - 动画效果
QRCode.js - 二维码生成

开发工具

  • 构建工具: Maven

  • IDE: IntelliJ IDEA / VS Code

  • 版本控制: Git

  • 接口测试: Postman


系统架构

复制代码
┌─────────────────────────────────────────────────────┐
│                    前端展示层                        │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │ 报名页面  │  │ 抽奖主页  │  │ 统计展示  │          │
│  └──────────┘  └──────────┘  └──────────┘          │
└─────────────────────────────────────────────────────┘
                        ↕ WebSocket / HTTP
┌─────────────────────────────────────────────────────┐
│                   Spring Boot 后端                   │
│  ┌──────────────────────────────────────────────┐  │
│  │           Controller 控制层                   │  │
│  │  - ParticipantController (参与者管理)        │  │
│  │  - WebSocket Handler (实时推送)              │  │
│  └──────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────┐  │
│  │           Service 业务层                      │  │
│  │  - 参与者注册验证                             │  │
│  │  - 抽奖算法实现                               │  │
│  │  - 奖品库存管理                               │  │
│  │  - 数据统计分析                               │  │
│  └──────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────┐  │
│  │           并发控制层                          │  │
│  │  - Semaphore (信号量限流)                    │  │
│  │  - ReentrantLock (读写锁)                    │  │
│  │  - ConcurrentHashMap (缓存)                  │  │
│  │  - BlockingQueue (异步队列)                  │  │
│  └──────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘
                        ↕
┌─────────────────────────────────────────────────────┐
│                   数据持久层                         │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐   │
│  │participants│  │  winners   │  │   prizes   │   │
│  │   .json    │  │   .json    │  │   .json    │   │
│  └────────────┘  └────────────┘  └────────────┘   │
└─────────────────────────────────────────────────────┘

核心功能实现

1. 高并发报名处理

需求场景: 年会现场几百人同时扫码报名,需要保证系统稳定性和数据一致性。

解决方案:

1.1 信号量限流
复制代码
// 限制同时处理的请求数量
private final Semaphore submitSemaphore = new Semaphore(100);
​
@PostMapping("/submit")
public ResponseEntity<?> submitParticipant(@RequestBody Participant participant) {
    // 尝试获取信号量,超时5秒
    boolean acquired = submitSemaphore.tryAcquire(5, TimeUnit.SECONDS);
    if (!acquired) {
        return ResponseEntity.status(429).body("系统繁忙,请稍后重试");
    }
    
    try {
        // 处理提交逻辑
        processSubmit(participant);
    } finally {
        submitSemaphore.release();
    }
}
1.2 内存缓存去重
复制代码
// 使用 ConcurrentHashMap 缓存已提交的设备/IP
private final ConcurrentHashMap<String, Boolean> deviceCheckCache = 
    new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Boolean> ipCheckCache = 
    new ConcurrentHashMap<>();
​
// 快速校验是否重复提交
if (deviceCheckCache.containsKey(deviceId)) {
    return ResponseEntity.badRequest().body("该设备已提交过");
}
if (ipCheckCache.containsKey(ipAddress)) {
    return ResponseEntity.badRequest().body("该IP已提交过");
}
1.3 批量写入队列
复制代码
// 使用阻塞队列缓冲提交请求
private final BlockingQueue<Participant> participantBuffer = 
    new LinkedBlockingQueue<>(5000);
​
// 异步批量写入
@Scheduled(fixedRate = 50)
public void flushBuffer() {
    List<Participant> batch = new ArrayList<>();
    participantBuffer.drainTo(batch, 100); // 每次最多100条
    
    if (!batch.isEmpty()) {
        writeBatchToFile(batch);
    }
}
​
// 原子化文件写入
private void writeBatchToFile(List<Participant> batch) {
    File tempFile = new File(TEMP_FILE_PATH);
    File targetFile = new File(PARTICIPANTS_FILE_PATH);
    
    // 写入临时文件
    objectMapper.writeValue(tempFile, participantsRoot);
    
    // 原子替换(避免文件损坏)
    Files.move(tempFile.toPath(), targetFile.toPath(), 
        StandardCopyOption.REPLACE_EXISTING, 
        StandardCopyOption.ATOMIC_MOVE);
}

2. WebSocket 实时推送

需求场景: 当有新参与者报名时,大屏幕实时显示参与者信息,无需手动刷新。

实现代码:

2.1 后端 WebSocket 配置
复制代码
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(participantWebSocketHandler(), "/websocket/participants")
                .setAllowedOrigins("*");
    }
    
    @Bean
    public ParticipantWebSocket participantWebSocketHandler() {
        return new ParticipantWebSocket();
    }
}
2.2 消息广播
复制代码
@Component
public class ParticipantWebSocket extends TextWebSocketHandler {
    private static final Set<WebSocketSession> sessions = 
        ConcurrentHashMap.newKeySet();
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
        // 发送当前参与者列表
        sendParticipantData(session);
    }
    
    // 广播给所有客户端
    public void broadcastUpdate() {
        String message = buildParticipantMessage();
        sessions.forEach(session -> {
            if (session.isOpen()) {
                session.sendMessage(new TextMessage(message));
            }
        });
    }
}
2.3 前端 WebSocket 连接
复制代码
connectWebSocket() {
    const wsUrl = `ws://192.168.10.34:8250/websocket/participants`;
    this.ws = new WebSocket(wsUrl);
    
    this.ws.onopen = () => {
        console.log('WebSocket 连接成功');
    };
    
    this.ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        // 更新参与者数据,Vue 自动重新渲染
        this.updateParticipantsData(data);
    };
    
    // 断线重连机制
    this.ws.onclose = () => {
        setTimeout(() => {
            this.reconnectAttempts++;
            this.connectWebSocket();
        }, 3000);
    };
}

3. 抽奖公平性保证

需求场景: 确保抽奖过程公平、透明,已中奖者不能重复中奖。

实现方案:

3.1 随机抽取算法
复制代码
@PostMapping("/draw")
public ResponseEntity<?> drawParticipant() {
    // 读取所有参与者
    List<Participant> allParticipants = loadParticipants();
    
    // 读取已中奖名单
    Set<String> winnerNames = loadWinnerNames();
    
    // 过滤出未中奖的参与者
    List<Participant> availableParticipants = allParticipants.stream()
        .filter(p -> !winnerNames.contains(p.getName()))
        .collect(Collectors.toList());
    
    if (availableParticipants.isEmpty()) {
        return ResponseEntity.badRequest()
            .body(Map.of("success", false, "message", "没有可参与抽奖的人员"));
    }
    
    // 使用 SecureRandom 确保随机性
    SecureRandom random = new SecureRandom();
    int index = random.nextInt(availableParticipants.size());
    Participant winner = availableParticipants.get(index);
    
    return ResponseEntity.ok(Map.of(
        "success", true,
        "participant", winner
    ));
}
3.2 奖品库存管理
复制代码
@PostMapping("/winners")
public ResponseEntity<?> addWinner(@RequestBody Map<String, Object> winnerData) {
    lock.lock();
    try {
        // 查找奖品
        String targetPrize = (String) winnerData.get("prize");
        ObjectNode prize = findPrize(targetPrize);
        
        // 检查库存
        int count = prize.get("count").asInt();
        if (count <= 0) {
            return ResponseEntity.badRequest()
                .body(Map.of("message", "该奖品已抽完"));
        }
        
        // 减少库存
        prize.put("count", count - 1);
        savePrizes(prizes);
        
        // 保存中奖记录
        saveWinner(winnerData);
        
        // 广播更新
        broadcastUpdate();
        
        return ResponseEntity.ok(Map.of("success", true));
    } finally {
        lock.unlock();
    }
}

4. 数据一致性保证

需求场景: 多用户并发操作时,确保数据文件不被损坏,数据不丢失。

解决方案:

4.1 读写锁机制
复制代码
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
​
// 写操作加锁
public void writeData() {
    lock.lock();
    try {
        // 写入操作
    } finally {
        lock.unlock();
    }
}
4.2 原子化文件操作
复制代码
private void atomicWriteFile(JsonNode data, String filePath) {
    // 先写入临时文件
    File tempFile = new File(filePath + ".tmp");
    objectMapper.writerWithDefaultPrettyPrinter()
        .writeValue(tempFile, data);
    
    // 原子移动(操作系统级别保证)
    Files.move(
        tempFile.toPath(), 
        new File(filePath).toPath(),
        StandardCopyOption.REPLACE_EXISTING,
        StandardCopyOption.ATOMIC_MOVE
    );
}
4.3 数据完整性校验
复制代码
private JsonNode loadJsonFile(String filePath) {
    File file = new File(filePath);
    
    if (!file.exists() || file.length() == 0) {
        // 返回默认空结构
        return createEmptyStructure();
    }
    
    try {
        JsonNode node = objectMapper.readTree(file);
        
        // 校验数据结构
        if (node == null || !node.isObject()) {
            logger.warn("数据格式异常,使用默认结构");
            return createEmptyStructure();
        }
        
        return node;
    } catch (IOException e) {
        logger.error("读取文件失败: " + e.getMessage());
        return createEmptyStructure();
    }
}

关键代码解析

1. 九宫格布局生成

复制代码
function createGrid() {
    const container = document.querySelector('.lottery-grid');
    container.innerHTML = '';
    
    // 3x3 网格,中间是"开始"按钮
    for (let i = 0; i < 9; i++) {
        const cell = document.createElement('div');
        cell.className = 'grid-cell';
        
        if (i === 4) {
            // 中心格子 - 开始按钮
            cell.classList.add('center-cell');
            cell.innerHTML = `
                <button class="start-btn">
                    <i class="el-icon-video-play"></i>
                    <span>开始抽奖</span>
                </button>
            `;
        } else {
            // 奖品格子
            const prize = prizes[i > 4 ? i - 1 : i];
            cell.innerHTML = `
                <div class="prize-icon">
                    <img src="${prize.icon}" alt="${prize.text}">
                </div>
                <div class="prize-text">${prize.text}</div>
                <div class="prize-count">剩余: ${prize.count}</div>
            `;
        }
        
        container.appendChild(cell);
    }
}

2. 平滑滚动动画

复制代码
function smoothAutoScroll() {
    if (isPaused) {
        requestAnimationFrame(smoothAutoScroll);
        return;
    }
    
    const list = document.querySelector('.participants-list');
    const maxScroll = list.scrollHeight - list.clientHeight;
    
    // 向下滚动
    if (list.scrollTop < maxScroll) {
        list.scrollTop += scrollSpeed;
    } else {
        // 到底部,暂停后返回顶部
        isPaused = true;
        setTimeout(() => {
            list.scrollTop = 0;
            setTimeout(() => {
                isPaused = false;
            }, 1000);
        }, pauseAtBottom);
    }
    
    requestAnimationFrame(smoothAutoScroll);
}

3. 撒花动画效果

复制代码
function createConfetti() {
    const colors = ['#FFD700', '#FFA500', '#FF6347', '#FF69B4'];
    const confettiCount = 100;
    
    for (let i = 0; i < confettiCount; i++) {
        setTimeout(() => {
            const confetti = document.createElement('div');
            confetti.className = 'confetti';
            confetti.style.left = Math.random() * 100 + '%';
            confetti.style.backgroundColor = 
                colors[Math.floor(Math.random() * colors.length)];
            confetti.style.animationDuration = 
                (Math.random() * 3 + 2) + 's';
            
            document.body.appendChild(confetti);
            
            // 动画结束后移除
            setTimeout(() => confetti.remove(), 5000);
        }, i * 30);
    }
}

4. 响应式统计数据

复制代码
// Vue 计算属性
computed: {
    totalParticipants() {
        return this.participants.length;
    },
    pendingCount() {
        return this.totalParticipants - this.winnersCount;
    },
    participationRate() {
        const rate = (this.totalParticipants / 500) * 100;
        return Math.min(rate, 100).toFixed(1);
    }
}

性能优化

1. 前端优化

1.1 防抖节流
复制代码
// 表单提交防抖
const now = Date.now();
if (now - this.lastSubmitTime < 2000) {
    return;
}
1.2 虚拟滚动
复制代码
.participants-list {
    height: 400px;
    overflow-y: auto;
    will-change: scroll-position; /* GPU加速 */
}
1.3 CSS动画优化
复制代码
.history-item {
    /* 使用 transform 而非 top/left */
    transform: translateY(-8px) scale(1.03);
    
    /* 启用硬件加速 */
    will-change: transform;
}

2. 后端优化

2.1 批量操作
复制代码
// 批量写入,减少IO次数
List<Participant> batch = new ArrayList<>();
participantBuffer.drainTo(batch, 100);
writeBatchToFile(batch);
2.2 缓存机制
复制代码
// 内存缓存,避免频繁读文件
private final ConcurrentHashMap<String, Boolean> cache = 
    new ConcurrentHashMap<>();
2.3 异步处理
复制代码
@Async
public CompletableFuture<Void> asyncBroadcast() {
    broadcastUpdate();
    return CompletableFuture.completedFuture(null);
}

项目亮点

1. 用户体验优化

  • 流畅动画:所有动画使用 CSS3 transition/animation,60fps 流畅度

  • 实时反馈:报名、抽奖结果即时显示,无需刷新

  • 视觉设计:金色主题、渐变背景、阴影效果营造年会氛围

  • 交互友好:悬停效果、点击反馈、加载状态一应俱全

2. 系统稳定性

  • 高并发支持:信号量限流 + 队列缓冲,支持百人同时操作

  • 数据一致性:读写锁 + 原子操作,避免数据损坏

  • 容错机制:异常捕获、默认值处理、自动重连

  • 防重复提交:多重校验机制,防止刷票

3. 代码质量

  • 模块化设计:前后端分离,职责清晰

  • 注释完善:关键逻辑都有详细注释

  • 日志记录:操作日志、错误日志便于排查问题

  • 可扩展性:易于添加新奖品、修改抽奖规则

4. 技术创新

  • WebSocket推送:实现真正的实时数据同步

  • 批量队列写入:提升高并发场景性能

  • 原子化文件操作:保证数据不丢失

  • 图片卡片展示:创新的中奖记录展示方式


部署说明

1. 环境要求

  • JDK 17+

  • Maven 3.6+

  • 浏览器支持 ES6+

2. 配置修改

修改 application.yml

复制代码
server:
  port: 8250
​
spring:
  application:
    name: lottery-system

修改前端 IP 地址(index.js):

复制代码
// 修改为实际服务器IP
const API_BASE_URL = 'http://YOUR_SERVER_IP:8250';

3. 启动步骤

复制代码
# 1. 编译项目
mvn clean compile
​
# 2. 运行项目
mvn spring-boot:run
​
# 3. 访问系统
浏览器打开: http://localhost:8250/
报名页面: http://localhost:8250/froms

4. 数据准备

data/prizes.json 中配置奖品:

复制代码
{
  "prizes": [
    {
      "text": "一等奖",
      "icon": "/img/img/prize1.png",
      "count": 1,
      "initialCount": 1
    }
  ]
}

未来优化方向

  1. 数据库支持:从文件存储迁移到 MySQL/PostgreSQL

  2. 管理后台:添加奖品管理、人员管理、数据导出功能

  3. 多轮抽奖:支持分轮次抽奖,每轮独立配置

  4. 中奖概率:支持设置不同奖品的中奖概率

  5. 移动端优化:独立开发移动端抽奖界面

  6. 分布式部署:支持多实例部署,Redis 共享会话


总结

这个年会抽奖系统是一个完整的前后端项目,涵盖了从需求分析、架构设计、编码实现到性能优化的完整流程。通过这个项目,可以学习到:

  • Spring Boot 构建 RESTful API 和 WebSocket 服务

  • Vue.js 实现响应式用户界面

  • 高并发处理 的多种解决方案

  • 实时通信 技术的应用

  • CSS 动画 和用户体验设计

  • 系统架构 和模块化设计思想

项目代码结构清晰,注释详细,适合作为学习案例或二次开发的基础。

效果图

年会抽奖系统 - 完整技术实现

一个基于 Spring Boot + Vue.js 的实时互动年会抽奖系统,支持二维码报名、九宫格抽奖、实时数据同步等功能。


目录


项目概述

这是一个为企业年会量身打造的抽奖系统,提供了从参与者报名到抽奖结果展示的完整流程。系统采用前后端分离架构,支持高并发访问,实时数据同步,为年会现场营造热烈的互动氛围。


核心功能

1. 参与者报名系统

功能描述:

  • 扫描二维码进入报名页面

  • 填写姓名、部门等信息

  • 防重复提交保护(同一设备/IP只能提交一次)

  • 高并发报名支持(信号量限流 + 批量写入)

技术实现:

复制代码
// 前端防抖处理
submitForm() {
    // 防止重复点击
    if (this.isSubmitting) {
        return;
    }
    
    // 2秒内不能重复提交(防抖)
    const now = Date.now();
    if (now - this.lastSubmitTime < 2000) {
        this.$message.warning('请勿频繁提交!');
        return;
    }
    
    this.isSubmitting = true;
    this.lastSubmitTime = now;
    
    // 提交逻辑...
}

2. 实时参与统计

功能描述:

  • 实时显示总参与人数、已中奖人数、待中奖人数

  • 新加入参与者动态提示(NEW标记 + 动画效果)

  • 自动滚动展示参与者列表(类似直播弹幕效果)

技术实现:

复制代码
// WebSocket 实时推送
updateParticipantsData(data) {
    const newParticipants = data.participants || [];
    const oldParticipants = this.participants;
    
    // 标记新加入的参与者
    newParticipants.forEach(newP => {
        const isNewUser = !oldParticipants.some(
            oldP => oldP.name === newP.name
        );
        if (isNewUser) {
            newP.isNew = true;
            // 3秒后移除NEW标记
            setTimeout(() => {
                newP.isNew = false;
            }, 3000);
        }
    });
    
    this.participants = newParticipants;
}

3. 九宫格抽奖动画

功能描述:

  • 九宫格布局展示所有奖品

  • 点击开始按钮,奖品高速旋转切换

  • 减速停止效果,增强期待感

  • 中奖弹窗 + 撒花动效 + 音效

技术实现:

复制代码
// 九宫格抽奖核心逻辑
function startLottery() {
    const cells = document.querySelectorAll('.grid-cell');
    let speed = 50; // 初始速度
    
    lotteryInterval = setInterval(() => {
        // 移除上一个高亮
        cells[currentIndex].classList.remove('active');
        
        // 计算下一个位置(跳过中心格)
        currentIndex = (currentIndex + 1) % 9;
        if (currentIndex === 4) currentIndex = 5;
        
        // 添加新高亮
        cells[currentIndex].classList.add('active');
        
        // 逐渐减速
        speed *= 1.05;
    }, speed);
}
​
// 停止并展示结果
function endLottery() {
    clearInterval(lotteryInterval);
    
    // 调用后端抽奖接口
    fetch('/api/draw', { method: 'POST' })
        .then(response => response.json())
        .then(data => {
            const winner = data.participant;
            
            // 保存中奖记录
            saveWinner(winner, prize);
            
            // 显示中奖弹窗
            showWinModal(winner, prize);
            
            // 创建撒花效果
            createConfetti();
        });
}

4. 中奖记录展示

功能描述:

  • 卡片式布局,展示奖品图片

  • 显示中奖者姓名、部门、时间

  • 悬停效果增强视觉交互

  • 实时更新中奖列表

页面结构:

复制代码
<div class="history-item">
    <!-- 奖品图片 -->
    <div class="history-prize-image">
        <img :src="winner.prizeIcon" :alt="winner.prize">
        <div class="prize-name-overlay">{{ winner.prize }}</div>
    </div>
    
    <!-- 获奖者信息 -->
    <div class="history-info">
        <div class="history-name">
            <i class="el-icon-user-solid"></i>
            {{ winner.name }}
        </div>
        <div class="history-department">
            <i class="el-icon-office-building"></i>
            {{ winner.department }}
        </div>
        <div class="history-time">
            <i class="el-icon-time"></i>
            {{ formatTime(winner.time) }}
        </div>
    </div>
</div>

技术栈

后端技术

技术 版本 说明
Spring Boot 3.x 核心框架
Java 17+ 开发语言
Jackson 2.x JSON处理
WebSocket - 实时通信
Apache POI 5.x Excel导出

前端技术

技术 版本 说明
Vue.js 2.x 渐进式框架
Element UI 2.x UI组件库
WebSocket API - 实时通信
CSS3 Animation - 动画效果
QRCode.js - 二维码生成

开发工具

  • 构建工具: Maven

  • IDE: IntelliJ IDEA / VS Code

  • 版本控制: Git

  • 接口测试: Postman


系统架构

复制代码
┌─────────────────────────────────────────────────────┐
│                    前端展示层                        │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │ 报名页面  │  │ 抽奖主页  │  │ 统计展示  │          │
│  └──────────┘  └──────────┘  └──────────┘          │
└─────────────────────────────────────────────────────┘
                        ↕ WebSocket / HTTP
┌─────────────────────────────────────────────────────┐
│                   Spring Boot 后端                   │
│  ┌──────────────────────────────────────────────┐  │
│  │           Controller 控制层                   │  │
│  │  - ParticipantController (参与者管理)        │  │
│  │  - WebSocket Handler (实时推送)              │  │
│  └──────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────┐  │
│  │           Service 业务层                      │  │
│  │  - 参与者注册验证                             │  │
│  │  - 抽奖算法实现                               │  │
│  │  - 奖品库存管理                               │  │
│  │  - 数据统计分析                               │  │
│  └──────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────┐  │
│  │           并发控制层                          │  │
│  │  - Semaphore (信号量限流)                    │  │
│  │  - ReentrantLock (读写锁)                    │  │
│  │  - ConcurrentHashMap (缓存)                  │  │
│  │  - BlockingQueue (异步队列)                  │  │
│  └──────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘
                        ↕
┌─────────────────────────────────────────────────────┐
│                   数据持久层                         │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐   │
│  │participants│  │  winners   │  │   prizes   │   │
│  │   .json    │  │   .json    │  │   .json    │   │
│  └────────────┘  └────────────┘  └────────────┘   │
└─────────────────────────────────────────────────────┘

核心功能实现

1. 高并发报名处理

需求场景: 年会现场几百人同时扫码报名,需要保证系统稳定性和数据一致性。

解决方案:

1.1 信号量限流
复制代码
// 限制同时处理的请求数量
private final Semaphore submitSemaphore = new Semaphore(100);
​
@PostMapping("/submit")
public ResponseEntity<?> submitParticipant(@RequestBody Participant participant) {
    // 尝试获取信号量,超时5秒
    boolean acquired = submitSemaphore.tryAcquire(5, TimeUnit.SECONDS);
    if (!acquired) {
        return ResponseEntity.status(429).body("系统繁忙,请稍后重试");
    }
    
    try {
        // 处理提交逻辑
        processSubmit(participant);
    } finally {
        submitSemaphore.release();
    }
}
1.2 内存缓存去重
复制代码
// 使用 ConcurrentHashMap 缓存已提交的设备/IP
private final ConcurrentHashMap<String, Boolean> deviceCheckCache = 
    new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Boolean> ipCheckCache = 
    new ConcurrentHashMap<>();
​
// 快速校验是否重复提交
if (deviceCheckCache.containsKey(deviceId)) {
    return ResponseEntity.badRequest().body("该设备已提交过");
}
if (ipCheckCache.containsKey(ipAddress)) {
    return ResponseEntity.badRequest().body("该IP已提交过");
}
1.3 批量写入队列
复制代码
// 使用阻塞队列缓冲提交请求
private final BlockingQueue<Participant> participantBuffer = 
    new LinkedBlockingQueue<>(5000);
​
// 异步批量写入
@Scheduled(fixedRate = 50)
public void flushBuffer() {
    List<Participant> batch = new ArrayList<>();
    participantBuffer.drainTo(batch, 100); // 每次最多100条
    
    if (!batch.isEmpty()) {
        writeBatchToFile(batch);
    }
}
​
// 原子化文件写入
private void writeBatchToFile(List<Participant> batch) {
    File tempFile = new File(TEMP_FILE_PATH);
    File targetFile = new File(PARTICIPANTS_FILE_PATH);
    
    // 写入临时文件
    objectMapper.writeValue(tempFile, participantsRoot);
    
    // 原子替换(避免文件损坏)
    Files.move(tempFile.toPath(), targetFile.toPath(), 
        StandardCopyOption.REPLACE_EXISTING, 
        StandardCopyOption.ATOMIC_MOVE);
}

2. WebSocket 实时推送

需求场景: 当有新参与者报名时,大屏幕实时显示参与者信息,无需手动刷新。

实现代码:

2.1 后端 WebSocket 配置
复制代码
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(participantWebSocketHandler(), "/websocket/participants")
                .setAllowedOrigins("*");
    }
    
    @Bean
    public ParticipantWebSocket participantWebSocketHandler() {
        return new ParticipantWebSocket();
    }
}
2.2 消息广播
复制代码
@Component
public class ParticipantWebSocket extends TextWebSocketHandler {
    private static final Set<WebSocketSession> sessions = 
        ConcurrentHashMap.newKeySet();
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
        // 发送当前参与者列表
        sendParticipantData(session);
    }
    
    // 广播给所有客户端
    public void broadcastUpdate() {
        String message = buildParticipantMessage();
        sessions.forEach(session -> {
            if (session.isOpen()) {
                session.sendMessage(new TextMessage(message));
            }
        });
    }
}
2.3 前端 WebSocket 连接
复制代码
connectWebSocket() {
    const wsUrl = `ws://192.168.10.34:8250/websocket/participants`;
    this.ws = new WebSocket(wsUrl);
    
    this.ws.onopen = () => {
        console.log('WebSocket 连接成功');
    };
    
    this.ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        // 更新参与者数据,Vue 自动重新渲染
        this.updateParticipantsData(data);
    };
    
    // 断线重连机制
    this.ws.onclose = () => {
        setTimeout(() => {
            this.reconnectAttempts++;
            this.connectWebSocket();
        }, 3000);
    };
}

3. 抽奖公平性保证

需求场景: 确保抽奖过程公平、透明,已中奖者不能重复中奖。

实现方案:

3.1 随机抽取算法
复制代码
@PostMapping("/draw")
public ResponseEntity<?> drawParticipant() {
    // 读取所有参与者
    List<Participant> allParticipants = loadParticipants();
    
    // 读取已中奖名单
    Set<String> winnerNames = loadWinnerNames();
    
    // 过滤出未中奖的参与者
    List<Participant> availableParticipants = allParticipants.stream()
        .filter(p -> !winnerNames.contains(p.getName()))
        .collect(Collectors.toList());
    
    if (availableParticipants.isEmpty()) {
        return ResponseEntity.badRequest()
            .body(Map.of("success", false, "message", "没有可参与抽奖的人员"));
    }
    
    // 使用 SecureRandom 确保随机性
    SecureRandom random = new SecureRandom();
    int index = random.nextInt(availableParticipants.size());
    Participant winner = availableParticipants.get(index);
    
    return ResponseEntity.ok(Map.of(
        "success", true,
        "participant", winner
    ));
}
3.2 奖品库存管理
复制代码
@PostMapping("/winners")
public ResponseEntity<?> addWinner(@RequestBody Map<String, Object> winnerData) {
    lock.lock();
    try {
        // 查找奖品
        String targetPrize = (String) winnerData.get("prize");
        ObjectNode prize = findPrize(targetPrize);
        
        // 检查库存
        int count = prize.get("count").asInt();
        if (count <= 0) {
            return ResponseEntity.badRequest()
                .body(Map.of("message", "该奖品已抽完"));
        }
        
        // 减少库存
        prize.put("count", count - 1);
        savePrizes(prizes);
        
        // 保存中奖记录
        saveWinner(winnerData);
        
        // 广播更新
        broadcastUpdate();
        
        return ResponseEntity.ok(Map.of("success", true));
    } finally {
        lock.unlock();
    }
}

4. 数据一致性保证

需求场景: 多用户并发操作时,确保数据文件不被损坏,数据不丢失。

解决方案:

4.1 读写锁机制
复制代码
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
​
// 写操作加锁
public void writeData() {
    lock.lock();
    try {
        // 写入操作
    } finally {
        lock.unlock();
    }
}
4.2 原子化文件操作
复制代码
private void atomicWriteFile(JsonNode data, String filePath) {
    // 先写入临时文件
    File tempFile = new File(filePath + ".tmp");
    objectMapper.writerWithDefaultPrettyPrinter()
        .writeValue(tempFile, data);
    
    // 原子移动(操作系统级别保证)
    Files.move(
        tempFile.toPath(), 
        new File(filePath).toPath(),
        StandardCopyOption.REPLACE_EXISTING,
        StandardCopyOption.ATOMIC_MOVE
    );
}
4.3 数据完整性校验
复制代码
private JsonNode loadJsonFile(String filePath) {
    File file = new File(filePath);
    
    if (!file.exists() || file.length() == 0) {
        // 返回默认空结构
        return createEmptyStructure();
    }
    
    try {
        JsonNode node = objectMapper.readTree(file);
        
        // 校验数据结构
        if (node == null || !node.isObject()) {
            logger.warn("数据格式异常,使用默认结构");
            return createEmptyStructure();
        }
        
        return node;
    } catch (IOException e) {
        logger.error("读取文件失败: " + e.getMessage());
        return createEmptyStructure();
    }
}

关键代码解析

1. 九宫格布局生成

复制代码
function createGrid() {
    const container = document.querySelector('.lottery-grid');
    container.innerHTML = '';
    
    // 3x3 网格,中间是"开始"按钮
    for (let i = 0; i < 9; i++) {
        const cell = document.createElement('div');
        cell.className = 'grid-cell';
        
        if (i === 4) {
            // 中心格子 - 开始按钮
            cell.classList.add('center-cell');
            cell.innerHTML = `
                <button class="start-btn">
                    <i class="el-icon-video-play"></i>
                    <span>开始抽奖</span>
                </button>
            `;
        } else {
            // 奖品格子
            const prize = prizes[i > 4 ? i - 1 : i];
            cell.innerHTML = `
                <div class="prize-icon">
                    <img src="${prize.icon}" alt="${prize.text}">
                </div>
                <div class="prize-text">${prize.text}</div>
                <div class="prize-count">剩余: ${prize.count}</div>
            `;
        }
        
        container.appendChild(cell);
    }
}

2. 平滑滚动动画

复制代码
function smoothAutoScroll() {
    if (isPaused) {
        requestAnimationFrame(smoothAutoScroll);
        return;
    }
    
    const list = document.querySelector('.participants-list');
    const maxScroll = list.scrollHeight - list.clientHeight;
    
    // 向下滚动
    if (list.scrollTop < maxScroll) {
        list.scrollTop += scrollSpeed;
    } else {
        // 到底部,暂停后返回顶部
        isPaused = true;
        setTimeout(() => {
            list.scrollTop = 0;
            setTimeout(() => {
                isPaused = false;
            }, 1000);
        }, pauseAtBottom);
    }
    
    requestAnimationFrame(smoothAutoScroll);
}

3. 撒花动画效果

复制代码
function createConfetti() {
    const colors = ['#FFD700', '#FFA500', '#FF6347', '#FF69B4'];
    const confettiCount = 100;
    
    for (let i = 0; i < confettiCount; i++) {
        setTimeout(() => {
            const confetti = document.createElement('div');
            confetti.className = 'confetti';
            confetti.style.left = Math.random() * 100 + '%';
            confetti.style.backgroundColor = 
                colors[Math.floor(Math.random() * colors.length)];
            confetti.style.animationDuration = 
                (Math.random() * 3 + 2) + 's';
            
            document.body.appendChild(confetti);
            
            // 动画结束后移除
            setTimeout(() => confetti.remove(), 5000);
        }, i * 30);
    }
}

4. 响应式统计数据

复制代码
// Vue 计算属性
computed: {
    totalParticipants() {
        return this.participants.length;
    },
    pendingCount() {
        return this.totalParticipants - this.winnersCount;
    },
    participationRate() {
        const rate = (this.totalParticipants / 500) * 100;
        return Math.min(rate, 100).toFixed(1);
    }
}

性能优化

1. 前端优化

1.1 防抖节流
复制代码
// 表单提交防抖
const now = Date.now();
if (now - this.lastSubmitTime < 2000) {
    return;
}
1.2 虚拟滚动
复制代码
.participants-list {
    height: 400px;
    overflow-y: auto;
    will-change: scroll-position; /* GPU加速 */
}
1.3 CSS动画优化
复制代码
.history-item {
    /* 使用 transform 而非 top/left */
    transform: translateY(-8px) scale(1.03);
    
    /* 启用硬件加速 */
    will-change: transform;
}

2. 后端优化

2.1 批量操作
复制代码
// 批量写入,减少IO次数
List<Participant> batch = new ArrayList<>();
participantBuffer.drainTo(batch, 100);
writeBatchToFile(batch);
2.2 缓存机制
复制代码
// 内存缓存,避免频繁读文件
private final ConcurrentHashMap<String, Boolean> cache = 
    new ConcurrentHashMap<>();
2.3 异步处理
复制代码
@Async
public CompletableFuture<Void> asyncBroadcast() {
    broadcastUpdate();
    return CompletableFuture.completedFuture(null);
}

项目亮点

1. 用户体验优化

  • 流畅动画:所有动画使用 CSS3 transition/animation,60fps 流畅度

  • 实时反馈:报名、抽奖结果即时显示,无需刷新

  • 视觉设计:金色主题、渐变背景、阴影效果营造年会氛围

  • 交互友好:悬停效果、点击反馈、加载状态一应俱全

2. 系统稳定性

  • 高并发支持:信号量限流 + 队列缓冲,支持百人同时操作

  • 数据一致性:读写锁 + 原子操作,避免数据损坏

  • 容错机制:异常捕获、默认值处理、自动重连

  • 防重复提交:多重校验机制,防止刷票

3. 代码质量

  • 模块化设计:前后端分离,职责清晰

  • 注释完善:关键逻辑都有详细注释

  • 日志记录:操作日志、错误日志便于排查问题

  • 可扩展性:易于添加新奖品、修改抽奖规则

4. 技术创新

  • WebSocket推送:实现真正的实时数据同步

  • 批量队列写入:提升高并发场景性能

  • 原子化文件操作:保证数据不丢失

  • 图片卡片展示:创新的中奖记录展示方式


部署说明

1. 环境要求

  • JDK 17+

  • Maven 3.6+

  • 浏览器支持 ES6+

2. 配置修改

修改 application.yml

复制代码
server:
  port: 8250
​
spring:
  application:
    name: lottery-system

修改前端 IP 地址(index.js):

复制代码
// 修改为实际服务器IP
const API_BASE_URL = 'http://YOUR_SERVER_IP:8250';

3. 启动步骤

复制代码
# 1. 编译项目
mvn clean compile
​
# 2. 运行项目
mvn spring-boot:run
​
# 3. 访问系统
浏览器打开: http://localhost:8250/
报名页面: http://localhost:8250/froms

4. 数据准备

data/prizes.json 中配置奖品:

复制代码
{
  "prizes": [
    {
      "text": "一等奖",
      "icon": "/img/img/prize1.png",
      "count": 1,
      "initialCount": 1
    }
  ]
}

未来优化方向

  1. 数据库支持:从文件存储迁移到 MySQL/PostgreSQL

  2. 管理后台:添加奖品管理、人员管理、数据导出功能

  3. 多轮抽奖:支持分轮次抽奖,每轮独立配置

  4. 中奖概率:支持设置不同奖品的中奖概率

  5. 移动端优化:独立开发移动端抽奖界面

  6. 分布式部署:支持多实例部署,Redis 共享会话


总结

这个年会抽奖系统是一个完整的前后端项目,涵盖了从需求分析、架构设计、编码实现到性能优化的完整流程。通过这个项目,可以学习到:

  • Spring Boot 构建 RESTful API 和 WebSocket 服务

  • Vue.js 实现响应式用户界面

  • 高并发处理 的多种解决方案

  • 实时通信 技术的应用

  • CSS 动画 和用户体验设计

  • 系统架构 和模块化设计思想

项目代码结构清晰,注释详细,适合作为学习案例或二次开发的基础。

效果图



相关推荐
尘似鹤17 小时前
设计一个状态机
学习·状态模式·嵌入式软件
Knight_AL2 天前
大文件分片上传:简单案例(前端切割与后端合并)
前端·状态模式
阿珊和她的猫4 天前
深入理解与手写发布订阅模式
开发语言·前端·javascript·vue.js·ecmascript·状态模式
Mr_WangAndy5 天前
C++设计模式_行为型模式_状态模式State
c++·设计模式·状态模式
zl9798996 天前
SpringBoot-依赖管理和自动配置
spring boot·后端·状态模式
Meteors.6 天前
23种设计模式——状态模式(State Pattern)
java·设计模式·状态模式
mit6.8249 天前
[Backstage] 插件架构 | 软件目录 | 实体的生命周期
架构·状态模式
笨手笨脚の12 天前
设计模式-状态模式
设计模式·状态模式·行为型设计模式
Hello.Reader12 天前
Flink 状态模式演进(State Schema Evolution)从原理到落地的一站式指南
python·flink·状态模式