年会抽奖系统 - 完整技术实现
一个基于 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
}
]
}
未来优化方向
-
数据库支持:从文件存储迁移到 MySQL/PostgreSQL
-
管理后台:添加奖品管理、人员管理、数据导出功能
-
多轮抽奖:支持分轮次抽奖,每轮独立配置
-
中奖概率:支持设置不同奖品的中奖概率
-
移动端优化:独立开发移动端抽奖界面
-
分布式部署:支持多实例部署,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
}
]
}
未来优化方向
-
数据库支持:从文件存储迁移到 MySQL/PostgreSQL
-
管理后台:添加奖品管理、人员管理、数据导出功能
-
多轮抽奖:支持分轮次抽奖,每轮独立配置
-
中奖概率:支持设置不同奖品的中奖概率
-
移动端优化:独立开发移动端抽奖界面
-
分布式部署:支持多实例部署,Redis 共享会话
总结
这个年会抽奖系统是一个完整的前后端项目,涵盖了从需求分析、架构设计、编码实现到性能优化的完整流程。通过这个项目,可以学习到:
-
Spring Boot 构建 RESTful API 和 WebSocket 服务
-
Vue.js 实现响应式用户界面
-
高并发处理 的多种解决方案
-
实时通信 技术的应用
-
CSS 动画 和用户体验设计
-
系统架构 和模块化设计思想
项目代码结构清晰,注释详细,适合作为学习案例或二次开发的基础。
效果图
