基于SpringBoot實(shí)現(xiàn)一個(gè)有趣好玩的年會(huì)抽獎(jiǎng)系統(tǒng)
一、項(xiàng)目背景與設(shè)計(jì)思路
年會(huì)抽獎(jiǎng)是每個(gè)企業(yè)年末的重要活動(dòng),但傳統(tǒng)的抽獎(jiǎng)方式往往存在公平性不足、互動(dòng)性差的問(wèn)題。本文將基于SpringBoot框架,設(shè)計(jì)并實(shí)現(xiàn)一個(gè)公正、有趣、好玩的年會(huì)抽獎(jiǎng)系統(tǒng),確保抽獎(jiǎng)過(guò)程的透明性和隨機(jī)性,同時(shí)增加多種互動(dòng)功能。
系統(tǒng)核心設(shè)計(jì)原則:
- 公正性:使用加密隨機(jī)算法,確保結(jié)果不可預(yù)測(cè)
- 趣味性:多種抽獎(jiǎng)模式,動(dòng)畫(huà)效果,實(shí)時(shí)互動(dòng)
- 可靠性:高并發(fā)支持,數(shù)據(jù)持久化,操作日志
- 易用性:簡(jiǎn)潔的Web界面,易于部署和管理
二、技術(shù)棧選型
- 后端框架:SpringBoot 2.7.x
- 前端技術(shù):Thymeleaf + Bootstrap 5 + jQuery
- 數(shù)據(jù)庫(kù):MySQL 8.0 + Redis 7.0
- 實(shí)時(shí)通信:WebSocket
- 安全框架:Spring Security
- 構(gòu)建工具:Maven 3.8+
三、項(xiàng)目結(jié)構(gòu)設(shè)計(jì)
lottery-system/
├── src/main/java/com/lottery/
│ ├── config/ # 配置類
│ ├── controller/ # 控制器層
│ ├── service/ # 業(yè)務(wù)邏輯層
│ ├── repository/ # 數(shù)據(jù)訪問(wèn)層
│ ├── model/ # 實(shí)體類
│ ├── utils/ # 工具類
│ ├── aspect/ # AOP切面
│ └── security/ # 安全配置
├── src/main/resources/
│ ├── static/ # 靜態(tài)資源
│ └── templates/ # 模板文件
└── pom.xml
四、數(shù)據(jù)庫(kù)設(shè)計(jì)
核心表結(jié)構(gòu)
-- 員工表
CREATE TABLE employee (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
employee_id VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
department VARCHAR(100),
avatar_url VARCHAR(500),
is_attended BOOLEAN DEFAULT false,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 獎(jiǎng)品表
CREATE TABLE prize (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
level INT NOT NULL COMMENT '獎(jiǎng)品等級(jí):1-特等獎(jiǎng),2-一等獎(jiǎng),3-二等獎(jiǎng)...',
total_count INT NOT NULL,
remain_count INT NOT NULL,
image_url VARCHAR(500),
description TEXT,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 中獎(jiǎng)記錄表
CREATE TABLE winning_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
employee_id VARCHAR(50) NOT NULL,
prize_id BIGINT NOT NULL,
draw_time DATETIME DEFAULT CURRENT_TIMESTAMP,
draw_method VARCHAR(50) COMMENT '抽獎(jiǎng)方式',
FOREIGN KEY (employee_id) REFERENCES employee(employee_id),
FOREIGN KEY (prize_id) REFERENCES prize(id)
);
-- 抽獎(jiǎng)活動(dòng)表
CREATE TABLE lottery_event (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
start_time DATETIME,
end_time DATETIME,
status VARCHAR(20) DEFAULT 'NOT_STARTED',
settings JSON COMMENT '活動(dòng)配置'
);
五、核心功能實(shí)現(xiàn)
5.1 項(xiàng)目依賴配置
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 數(shù)據(jù)庫(kù) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 工具類 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<!-- JSON處理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
5.2 員工實(shí)體類
@Entity
@Table(name = "employee")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "employee_id", unique = true, nullable = false)
private String employeeId;
@Column(nullable = false)
private String name;
private String department;
private String avatarUrl;
@Column(name = "is_attended")
private Boolean isAttended = false;
@CreationTimestamp
@Column(name = "created_time", updatable = false)
private LocalDateTime createdTime;
@Transient
private Boolean isWinner = false;
}
5.3 抽獎(jiǎng)服務(wù)核心實(shí)現(xiàn)
@Service
@Slf4j
public class LotteryService {
@Autowired
private EmployeeRepository employeeRepository;
@Autowired
private PrizeRepository prizeRepository;
@Autowired
private WinningRecordRepository winningRecordRepository;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 核心抽獎(jiǎng)算法 - 使用加密安全的隨機(jī)數(shù)
*/
public LotteryResult drawPrize(Long prizeId, String drawMethod) {
Prize prize = prizeRepository.findById(prizeId)
.orElseThrow(() -> new RuntimeException("獎(jiǎng)品不存在"));
if (prize.getRemainCount() <= 0) {
throw new RuntimeException("該獎(jiǎng)品已抽完");
}
// 獲取所有未中獎(jiǎng)的參會(huì)員工
List<Employee> candidates = employeeRepository.findByIsAttendedTrueAndIsWinnerFalse();
if (candidates.isEmpty()) {
throw new RuntimeException("沒(méi)有符合條件的抽獎(jiǎng)人員");
}
// 使用SecureRandom確保隨機(jī)性
SecureRandom secureRandom = new SecureRandom();
byte[] randomBytes = new byte[32];
secureRandom.nextBytes(randomBytes);
// 基于哈希值選擇中獎(jiǎng)?wù)?
int index = Math.abs(Arrays.hashCode(randomBytes)) % candidates.size();
Employee winner = candidates.get(index);
// 保存中獎(jiǎng)記錄
WinningRecord record = new WinningRecord();
record.setEmployeeId(winner.getEmployeeId());
record.setPrizeId(prizeId);
record.setDrawMethod(drawMethod);
record.setDrawTime(LocalDateTime.now());
winningRecordRepository.save(record);
// 更新獎(jiǎng)品剩余數(shù)量
prize.setRemainCount(prize.getRemainCount() - 1);
prizeRepository.save(prize);
// 標(biāo)記員工為中獎(jiǎng)狀態(tài)(實(shí)際中可能需要更復(fù)雜的邏輯)
markEmployeeAsWinner(winner.getId());
// 發(fā)送WebSocket通知
sendWinningNotification(winner, prize);
return LotteryResult.builder()
.winner(winner)
.prize(prize)
.drawTime(LocalDateTime.now())
.drawId(record.getId())
.build();
}
/**
* 批量導(dǎo)入員工
*/
@Transactional
public void importEmployees(MultipartFile file) {
try {
List<Employee> employees = parseEmployeeFile(file);
employeeRepository.saveAll(employees);
log.info("成功導(dǎo)入{}名員工", employees.size());
} catch (Exception e) {
log.error("導(dǎo)入員工失敗", e);
throw new RuntimeException("導(dǎo)入失敗: " + e.getMessage());
}
}
/**
* 多種抽獎(jiǎng)模式
*/
public LotteryResult drawWithMode(Prize prize, DrawMode mode) {
switch (mode) {
case RANDOM:
return randomDraw(prize);
case LUCKY_NUMBER:
return luckyNumberDraw(prize);
case ROULETTE:
return rouletteDraw(prize);
case GRID:
return gridDraw(prize);
default:
return randomDraw(prize);
}
}
/**
* 輪盤(pán)抽獎(jiǎng)算法
*/
private LotteryResult rouletteDraw(Prize prize) {
List<Employee> candidates = getValidCandidates();
// 為每個(gè)候選人分配權(quán)重(這里簡(jiǎn)單按部門(mén)分配不同權(quán)重)
Map<Employee, Double> weights = new HashMap<>();
for (Employee emp : candidates) {
double weight = getDepartmentWeight(emp.getDepartment());
weights.put(emp, weight);
}
// 輪盤(pán)選擇
Employee winner = rouletteSelect(weights);
return createLotteryResult(winner, prize, "ROULETTE");
}
private Employee rouletteSelect(Map<Employee, Double> weights) {
double totalWeight = weights.values().stream().mapToDouble(Double::doubleValue).sum();
double random = Math.random() * totalWeight;
double cumulativeWeight = 0.0;
for (Map.Entry<Employee, Double> entry : weights.entrySet()) {
cumulativeWeight += entry.getValue();
if (random <= cumulativeWeight) {
return entry.getKey();
}
}
return weights.keySet().iterator().next();
}
}
5.4 WebSocket實(shí)時(shí)通信
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(lotteryWebSocketHandler(), "/ws/lottery")
.setAllowedOrigins("*")
.withSockJS();
}
@Bean
public WebSocketHandler lotteryWebSocketHandler() {
return new LotteryWebSocketHandler();
}
}
@Component
public class LotteryWebSocketHandler extends TextWebSocketHandler {
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String sessionId = session.getId();
sessions.put(sessionId, session);
// 發(fā)送歡迎消息
sendMessage(session, "歡迎加入年會(huì)抽獎(jiǎng)!");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
// 處理客戶端消息
String payload = message.getPayload();
// 處理業(yè)務(wù)邏輯...
}
/**
* 廣播中獎(jiǎng)消息
*/
public void broadcastWinningMessage(Employee winner, Prize prize) {
Map<String, Object> message = new HashMap<>();
message.put("type", "WINNING_NOTIFICATION");
message.put("winner", winner.getName());
message.put("prize", prize.getName());
message.put("department", winner.getDepartment());
message.put("time", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
String jsonMessage = JSON.toJSONString(message);
sessions.values().forEach(session -> {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(jsonMessage));
}
} catch (IOException e) {
log.error("發(fā)送WebSocket消息失敗", e);
}
});
}
}
5.5 抽獎(jiǎng)控制器
@Controller
@RequestMapping("/lottery")
public class LotteryController {
@Autowired
private LotteryService lotteryService;
@Autowired
private LotteryWebSocketHandler webSocketHandler;
/**
* 抽獎(jiǎng)頁(yè)面
*/
@GetMapping("/draw")
public String drawPage(Model model) {
List<Prize> availablePrizes = lotteryService.getAvailablePrizes();
List<Employee> attendees = lotteryService.getAttendees();
model.addAttribute("prizes", availablePrizes);
model.addAttribute("attendees", attendees);
model.addAttribute("totalAttendees", attendees.size());
return "lottery/draw";
}
/**
* 執(zhí)行抽獎(jiǎng)
*/
@PostMapping("/execute")
@ResponseBody
public ApiResponse<LotteryResult> executeDraw(
@RequestParam Long prizeId,
@RequestParam(defaultValue = "RANDOM") String mode) {
try {
LotteryResult result = lotteryService.drawWithMode(prizeId, mode);
// 發(fā)送實(shí)時(shí)通知
webSocketHandler.broadcastWinningMessage(
result.getWinner(),
result.getPrize()
);
return ApiResponse.success(result);
} catch (Exception e) {
return ApiResponse.error(e.getMessage());
}
}
/**
* 大屏幕顯示
*/
@GetMapping("/screen")
public String bigScreen(Model model) {
List<WinningRecord> recentWinners = lotteryService.getRecentWinners(10);
model.addAttribute("winners", recentWinners);
return "lottery/screen";
}
/**
* 數(shù)據(jù)統(tǒng)計(jì)
*/
@GetMapping("/statistics")
@ResponseBody
public ApiResponse<Statistics> getStatistics() {
Statistics stats = lotteryService.getLotteryStatistics();
return ApiResponse.success(stats);
}
}
5.6 前端抽獎(jiǎng)頁(yè)面實(shí)現(xiàn)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>年會(huì)抽獎(jiǎng)系統(tǒng)</title>
<!-- Bootstrap CSS -->
<link rel="external nofollow" rel="stylesheet">
<link rel="external nofollow" rel="stylesheet">
<style>
.lottery-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.prize-card {
transition: all 0.3s ease;
cursor: pointer;
}
.prize-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.lottery-wheel {
width: 500px;
height: 500px;
position: relative;
margin: 0 auto;
}
.wheel-canvas {
width: 100%;
height: 100%;
}
.winner-display {
background: rgba(255,255,255,0.9);
border-radius: 15px;
padding: 20px;
animation: slideIn 0.5s ease;
}
@keyframes slideIn {
from { transform: translateY(50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.confetti {
position: fixed;
width: 15px;
height: 15px;
background-color: #f00;
top: -10px;
opacity: 0;
}
@keyframes confetti-fall {
0% { top: -10px; opacity: 1; }
100% { top: 100vh; opacity: 0; }
}
</style>
</head>
<body>
<div class="lottery-container">
<div class="container">
<!-- 標(biāo)題區(qū)域 -->
<div class="text-center text-white mb-5">
<h1 class="display-3 fw-bold">?? 年會(huì)幸運(yùn)大抽獎(jiǎng) ??</h1>
<p class="lead">夢(mèng)想成真,幸運(yùn)降臨</p>
</div>
<!-- 抽獎(jiǎng)主區(qū)域 -->
<div class="row">
<!-- 左側(cè):獎(jiǎng)品選擇 -->
<div class="col-md-4">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-gift"></i> 獎(jiǎng)品列表</h5>
</div>
<div class="card-body" id="prizeList">
<!-- 獎(jiǎng)品列表動(dòng)態(tài)加載 -->
</div>
</div>
<!-- 抽獎(jiǎng)控制 -->
<div class="card mt-3">
<div class="card-body">
<div class="mb-3">
<label class="form-label">抽獎(jiǎng)模式</label>
<select class="form-select" id="drawMode">
<option value="RANDOM">隨機(jī)抽獎(jiǎng)</option>
<option value="ROULETTE">幸運(yùn)輪盤(pán)</option>
<option value="GRID">九宮格抽獎(jiǎng)</option>
<option value="LUCKY_NUMBER">幸運(yùn)數(shù)字</option>
</select>
</div>
<button class="btn btn-danger btn-lg w-100" id="drawButton">
<i class="fas fa-play"></i> 開(kāi)始抽獎(jiǎng)
</button>
</div>
</div>
</div>
<!-- 中間:抽獎(jiǎng)動(dòng)畫(huà) -->
<div class="col-md-4">
<div class="lottery-wheel">
<canvas id="wheelCanvas" class="wheel-canvas"></canvas>
<div class="text-center mt-3">
<div class="winner-display" id="winnerDisplay" style="display:none;">
<h4 class="text-success">?? 恭喜中獎(jiǎng)! ??</h4>
<h3 id="winnerName" class="fw-bold"></h3>
<p>獲得:<span id="prizeName" class="text-primary"></span></p>
</div>
</div>
</div>
</div>
<!-- 右側(cè):中獎(jiǎng)名單 -->
<div class="col-md-4">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="fas fa-trophy"></i> 中獎(jiǎng)名單</h5>
</div>
<div class="card-body">
<div id="winnerList" class="list-group">
<!-- 中獎(jiǎng)名單動(dòng)態(tài)加載 -->
</div>
</div>
</div>
<!-- 統(tǒng)計(jì)數(shù)據(jù) -->
<div class="card mt-3">
<div class="card-body">
<h6>抽獎(jiǎng)統(tǒng)計(jì)</h6>
<div class="row text-center">
<div class="col-6">
<div class="display-6" id="totalAttendees">0</div>
<small>參會(huì)人數(shù)</small>
</div>
<div class="col-6">
<div class="display-6" id="totalWinners">0</div>
<small>已中獎(jiǎng)人數(shù)</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- WebSocket連接 -->
<script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
<script>
class LotterySystem {
constructor() {
this.stompClient = null;
this.currentPrizeId = null;
this.isDrawing = false;
this.init();
}
init() {
this.loadPrizes();
this.loadWinners();
this.loadStatistics();
this.connectWebSocket();
this.bindEvents();
this.initWheel();
}
connectWebSocket() {
const socket = new SockJS('/ws/lottery');
this.stompClient = Stomp.over(socket);
this.stompClient.connect({}, (frame) => {
console.log('Connected: ' + frame);
this.stompClient.subscribe('/topic/winners', (message) => {
const notification = JSON.parse(message.body);
this.showWinningNotification(notification);
this.loadWinners();
this.loadStatistics();
});
});
}
loadPrizes() {
$.get('/lottery/prizes', (prizes) => {
const container = $('#prizeList');
container.empty();
prizes.forEach(prize => {
const card = `
<div class="card prize-card mb-2" data-prize-id="${prize.id}">
<div class="card-body">
<h6 class="card-title">${prize.name}</h6>
<p class="card-text text-muted small">剩余: ${prize.remainCount}/${prize.totalCount}</p>
</div>
</div>
`;
container.append(card);
});
// 綁定選擇事件
$('.prize-card').click(function() {
$('.prize-card').removeClass('border-primary');
$(this).addClass('border-primary border-3');
this.currentPrizeId = $(this).data('prize-id');
});
});
}
loadWinners() {
$.get('/lottery/recent-winners', (winners) => {
const container = $('#winnerList');
container.empty();
winners.forEach(record => {
const item = `
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${record.employeeName}</h6>
<small>${record.department}</small>
</div>
<p class="mb-1">${record.prizeName}</p>
<small class="text-muted">${record.drawTime}</small>
</div>
`;
container.prepend(item);
});
});
}
loadStatistics() {
$.get('/lottery/statistics', (stats) => {
$('#totalAttendees').text(stats.totalAttendees);
$('#totalWinners').text(stats.totalWinners);
});
}
bindEvents() {
$('#drawButton').click(() => {
if (!this.currentPrizeId) {
alert('請(qǐng)先選擇獎(jiǎng)品!');
return;
}
if (this.isDrawing) {
alert('正在抽獎(jiǎng)中,請(qǐng)稍候...');
return;
}
this.isDrawing = true;
$('#drawButton').prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 抽獎(jiǎng)中...');
const drawMode = $('#drawMode').val();
// 開(kāi)始抽獎(jiǎng)動(dòng)畫(huà)
this.startWheelAnimation();
// 發(fā)送抽獎(jiǎng)?wù)埱?
$.ajax({
url: '/lottery/execute',
method: 'POST',
data: {
prizeId: this.currentPrizeId,
mode: drawMode
},
success: (response) => {
if (response.success) {
setTimeout(() => {
this.showWinner(response.data);
this.createConfetti();
this.isDrawing = false;
$('#drawButton').prop('disabled', false).html('<i class="fas fa-play"></i> 開(kāi)始抽獎(jiǎng)');
}, 3000);
} else {
alert(response.message);
this.isDrawing = false;
$('#drawButton').prop('disabled', false).html('<i class="fas fa-play"></i> 開(kāi)始抽獎(jiǎng)');
}
},
error: () => {
alert('抽獎(jiǎng)失敗,請(qǐng)重試!');
this.isDrawing = false;
$('#drawButton').prop('disabled', false).html('<i class="fas fa-play"></i> 開(kāi)始抽獎(jiǎng)');
}
});
});
}
startWheelAnimation() {
const canvas = document.getElementById('wheelCanvas');
const ctx = canvas.getContext('2d');
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(centerX, centerY) - 10;
let rotation = 0;
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 繪制轉(zhuǎn)盤(pán)
rotation += 0.1;
// 繪制扇形
const segments = 12;
const segmentAngle = (2 * Math.PI) / segments;
for (let i = 0; i < segments; i++) {
const startAngle = segmentAngle * i + rotation;
const endAngle = segmentAngle * (i + 1) + rotation;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.closePath();
// 交替顏色
ctx.fillStyle = i % 2 === 0 ? '#FF6B6B' : '#4ECDC4';
ctx.fill();
// 繪制文字
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(startAngle + segmentAngle / 2);
ctx.textAlign = 'right';
ctx.fillStyle = 'white';
ctx.font = 'bold 14px Arial';
ctx.fillText('幸運(yùn)', radius - 20, 10);
ctx.restore();
}
// 繪制指針
ctx.beginPath();
ctx.moveTo(centerX, centerY - 20);
ctx.lineTo(centerX, centerY - radius + 20);
ctx.lineTo(centerX + 10, centerY - radius);
ctx.lineTo(centerX - 10, centerY - radius);
ctx.closePath();
ctx.fillStyle = '#FFD93D';
ctx.fill();
if (this.isDrawing) {
requestAnimationFrame(animate);
}
};
canvas.width = 500;
canvas.height = 500;
animate();
}
showWinner(result) {
$('#winnerName').text(result.winner.name);
$('#prizeName').text(result.prize.name);
$('#winnerDisplay').fadeIn();
// 播放音效
this.playSound('winning');
}
showWinningNotification(notification) {
// 顯示實(shí)時(shí)通知
const notificationHtml = `
<div class="toast show" role="alert">
<div class="toast-header bg-success text-white">
<strong class="me-auto">?? 恭喜中獎(jiǎng)!</strong>
<small>剛剛</small>
</div>
<div class="toast-body">
<strong>${notification.winner}</strong> 獲得 <strong>${notification.prize}</strong>
</div>
</div>
`;
$('.toast-container').remove();
$('body').append(`<div class="toast-container position-fixed top-0 end-0 p-3">${notificationHtml}</div>`);
// 5秒后自動(dòng)移除
setTimeout(() => {
$('.toast-container').remove();
}, 5000);
}
createConfetti() {
const colors = ['#FF6B6B', '#4ECDC4', '#FFD93D', '#6BCF7F'];
for (let i = 0; i < 100; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
confetti.style.left = Math.random() * 100 + 'vw';
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
confetti.style.width = Math.random() * 10 + 5 + 'px';
confetti.style.height = Math.random() * 10 + 5 + 'px';
confetti.style.animation = `confetti-fall ${Math.random() * 3 + 2}s linear forwards`;
confetti.style.animationDelay = Math.random() * 1 + 's';
document.body.appendChild(confetti);
setTimeout(() => {
confetti.remove();
}, 5000);
}
}
playSound(type) {
const audio = new Audio();
if (type === 'winning') {
audio.src = '/sound/winning.mp3';
} else if (type === 'draw') {
audio.src = '/sound/draw.mp3';
}
audio.play().catch(e => console.log('音頻播放失敗:', e));
}
initWheel() {
// 初始化抽獎(jiǎng)轉(zhuǎn)盤(pán)
const canvas = document.getElementById('wheelCanvas');
if (canvas.getContext) {
// 已經(jīng)在上面的startWheelAnimation中初始化了
}
}
}
// 頁(yè)面加載完成后初始化
$(document).ready(() => {
window.lotterySystem = new LotterySystem();
});
</script>
</body>
</html>
六、系統(tǒng)特色功能
6.1 多種抽獎(jiǎng)模式
- 隨機(jī)抽獎(jiǎng):完全隨機(jī),確保公平
- 幸運(yùn)輪盤(pán):視覺(jué)沖擊力強(qiáng),增加趣味性
- 九宮格抽獎(jiǎng):互動(dòng)性強(qiáng),適合小團(tuán)隊(duì)
- 幸運(yùn)數(shù)字:與員工工號(hào)關(guān)聯(lián),增加參與感
6.2 實(shí)時(shí)互動(dòng)功能
- WebSocket實(shí)時(shí)通知:中獎(jiǎng)結(jié)果實(shí)時(shí)推送到所有客戶端
- 大屏幕顯示:專門(mén)為中獎(jiǎng)?wù)故驹O(shè)計(jì)的全屏界面
- 彈幕互動(dòng):?jiǎn)T工可以發(fā)送祝福彈幕
- 倒計(jì)時(shí)功能:增加緊張氛圍
6.3 防作弊機(jī)制
@Component
public class LotterySecurityService {
/**
* 驗(yàn)證抽獎(jiǎng)?wù)埱蟮暮戏ㄐ?
*/
public boolean validateDrawRequest(String sessionId, Long prizeId) {
// 防止頻繁請(qǐng)求
String key = "draw:limit:" + sessionId;
Long count = redisTemplate.opsForValue().increment(key, 1);
if (count == 1) {
redisTemplate.expire(key, 10, TimeUnit.SECONDS);
}
return count <= 3; // 10秒內(nèi)最多請(qǐng)求3次
}
/**
* 驗(yàn)證員工資格
*/
public boolean validateEmployeeEligibility(String employeeId) {
// 檢查是否已簽到
// 檢查是否已中獎(jiǎng)(限制每人最多中獎(jiǎng)次數(shù))
// 檢查是否符合特定獎(jiǎng)品要求
return true;
}
/**
* 生成抽獎(jiǎng)過(guò)程審計(jì)日志
*/
public void auditDrawProcess(LotteryResult result, String operator) {
String auditLog = String.format(
"抽獎(jiǎng)審計(jì) - 操作人:%s, 獎(jiǎng)品:%s, 中獎(jiǎng)人:%s, 時(shí)間:%s, 隨機(jī)種子:%s",
operator,
result.getPrize().getName(),
result.getWinner().getName(),
LocalDateTime.now(),
generateRandomSeed()
);
log.info(auditLog);
saveToBlockchain(auditLog); // 可選:保存到區(qū)塊鏈確保不可篡改
}
}
6.4 數(shù)據(jù)統(tǒng)計(jì)與分析
@Service
public class StatisticsService {
public LotteryStatistics getStatistics() {
LotteryStatistics stats = new LotteryStatistics();
// 基礎(chǔ)統(tǒng)計(jì)
stats.setTotalAttendees(employeeRepository.countByIsAttendedTrue());
stats.setTotalWinners(winningRecordRepository.count());
stats.setTotalPrizes(prizeRepository.count());
// 部門(mén)中獎(jiǎng)分布
Map<String, Long> deptDistribution = winningRecordRepository
.getDepartmentDistribution();
stats.setDepartmentDistribution(deptDistribution);
// 時(shí)間段分析
List<HourlyDraw> hourlyDraws = winningRecordRepository
.getHourlyDrawCount();
stats.setHourlyDraws(hourlyDraws);
// 獎(jiǎng)品熱度
List<PrizePopularity> prizePopularity = winningRecordRepository
.getPrizePopularity();
stats.setPrizePopularity(prizePopularity);
return stats;
}
/**
* 生成抽獎(jiǎng)報(bào)告
*/
public Report generateReport() {
Report report = new Report();
// 總體情況
report.setSummary(getSummary());
// 詳細(xì)數(shù)據(jù)
report.setDetails(getDetailedData());
// 圖表數(shù)據(jù)
report.setCharts(getChartData());
// 分析結(jié)論
report.setAnalysis(getAnalysis());
return report;
}
}
七、部署與配置
7.1 應(yīng)用配置
# application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/lottery_db?useSSL=false&serverTimezone=UTC
username: lottery_admin
password: lottery_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
redis:
host: localhost
port: 6379
password:
timeout: 5000ms
thymeleaf:
cache: false
mode: HTML
encoding: UTF-8
lottery:
security:
admin-username: admin
admin-password: ${ADMIN_PASSWORD:admin123}
jwt-secret: ${JWT_SECRET:lottery-secret-key}
draw:
max-attempts-per-minute: 10
cooldown-seconds: 5
ui:
title: 年會(huì)幸運(yùn)大抽獎(jiǎng)
theme: gradient-blue
animation-enabled: true
7.2 Docker部署
# Dockerfile FROM openjdk:11-jre-slim WORKDIR /app COPY target/lottery-system-1.0.0.jar app.jar RUN mkdir -p /app/logs /app/uploads ENV TZ=Asia/Shanghai ENV JAVA_OPTS="-Xmx512m -Xms256m" EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: lottery_db
MYSQL_USER: lottery_admin
MYSQL_PASSWORD: lottery_password
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
lottery-app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/lottery_db
SPRING_REDIS_HOST: redis
depends_on:
- mysql
- redis
volumes:
mysql_data:
redis_data:
八、系統(tǒng)優(yōu)化與擴(kuò)展
8.1 性能優(yōu)化
@Service
@Slf4j
public class LotteryOptimizationService {
/**
* 使用Redis緩存熱門(mén)數(shù)據(jù)
*/
@Cacheable(value = "prizes", key = "'available'")
public List<Prize> getAvailablePrizes() {
return prizeRepository.findByRemainCountGreaterThan(0);
}
/**
* 批量處理優(yōu)化
*/
@Transactional
public void batchProcessWinners(List<Long> winnerIds, Long prizeId) {
// 使用批量插入提高性能
List<WinningRecord> records = winnerIds.stream()
.map(employeeId -> {
WinningRecord record = new WinningRecord();
record.setEmployeeId(employeeId.toString());
record.setPrizeId(prizeId);
return record;
})
.collect(Collectors.toList());
winningRecordRepository.saveAll(records);
}
/**
* 異步處理非核心業(yè)務(wù)
*/
@Async
public void asyncSendNotification(WinningRecord record) {
// 發(fā)送郵件通知
emailService.sendWinningEmail(record);
// 發(fā)送短信通知
smsService.sendWinningSMS(record);
// 記錄日志
auditService.logWinningEvent(record);
}
}
8.2 擴(kuò)展功能
- 微信小程序接入:?jiǎn)T工掃碼參與
- 人臉識(shí)別簽到:防止代簽
- 虛擬禮物系統(tǒng):?jiǎn)T工互送祝福
- 抽獎(jiǎng)策略引擎:可配置的抽獎(jiǎng)規(guī)則
- 多會(huì)場(chǎng)支持:分布式抽獎(jiǎng)同步
九、測(cè)試與驗(yàn)證
單元測(cè)試
@SpringBootTest
@AutoConfigureMockMvc
class LotteryServiceTest {
@Autowired
private MockMvc mockMvc;
@Test
void testRandomDraw() {
// 測(cè)試隨機(jī)抽獎(jiǎng)的公平性
Map<Long, Integer> winCount = new HashMap<>();
int totalTests = 10000;
for (int i = 0; i < totalTests; i++) {
LotteryResult result = lotteryService.drawPrize(1L, "RANDOM");
Long winnerId = result.getWinner().getId();
winCount.put(winnerId, winCount.getOrDefault(winnerId, 0) + 1);
}
// 驗(yàn)證分布是否均勻
double expectedFrequency = totalTests / 100.0; // 假設(shè)有100個(gè)候選人
double chiSquare = calculateChiSquare(winCount, expectedFrequency);
assertTrue(chiSquare < 15.0, "抽獎(jiǎng)分布不均勻");
}
@Test
void testConcurrentDraw() throws InterruptedException {
// 測(cè)試并發(fā)抽獎(jiǎng)
int threadCount = 100;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
List<Future<LotteryResult>> futures = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
Future<LotteryResult> future = executor.submit(() -> {
try {
return lotteryService.drawPrize(1L, "RANDOM");
} finally {
latch.countDown();
}
});
futures.add(future);
}
latch.await(10, TimeUnit.SECONDS);
// 驗(yàn)證沒(méi)有重復(fù)中獎(jiǎng)
Set<Long> winnerIds = futures.stream()
.map(f -> {
try {
return f.get().getWinner().getId();
} catch (Exception e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
assertEquals(threadCount, winnerIds.size(), "存在重復(fù)中獎(jiǎng)情況");
}
}
十、總結(jié)
本文詳細(xì)介紹了如何使用SpringBoot構(gòu)建一個(gè)公正、有趣、好玩的年會(huì)抽獎(jiǎng)系統(tǒng)。系統(tǒng)具有以下特點(diǎn):
- 公正性保障:使用加密安全的隨機(jī)數(shù)生成算法,所有操作都有審計(jì)日志
- 趣味性體驗(yàn):多種抽獎(jiǎng)模式,炫酷的動(dòng)畫(huà)效果,實(shí)時(shí)互動(dòng)功能
- 高可靠性:支持高并發(fā),數(shù)據(jù)持久化,完善的錯(cuò)誤處理機(jī)制
- 易于擴(kuò)展:模塊化設(shè)計(jì),方便添加新功能和抽獎(jiǎng)模式
- 部署簡(jiǎn)單:支持Docker容器化部署,一鍵啟動(dòng)
系統(tǒng)不僅滿足了基本的抽獎(jiǎng)需求,還通過(guò)WebSocket實(shí)時(shí)通信、多種抽獎(jiǎng)模式、數(shù)據(jù)統(tǒng)計(jì)分析等功能,大大提升了年會(huì)的互動(dòng)性和趣味性。企業(yè)可以根據(jù)自身需求,進(jìn)一步擴(kuò)展功能,如集成企業(yè)微信、添加更多互動(dòng)游戲等,打造獨(dú)一無(wú)二的年會(huì)抽獎(jiǎng)體驗(yàn)。
通過(guò)本系統(tǒng)的實(shí)施,企業(yè)可以確保抽獎(jiǎng)活動(dòng)的公平公正,同時(shí)提升員工的參與感和滿意度,為年會(huì)增添更多歡樂(lè)和驚喜。
到此這篇關(guān)于基于SpringBoot實(shí)現(xiàn)一個(gè)有趣好玩的年會(huì)抽獎(jiǎng)系統(tǒng)的文章就介紹到這了,更多相關(guān)SpringBoot年會(huì)抽獎(jiǎng)系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springSecurity實(shí)現(xiàn)簡(jiǎn)單的登錄功能
這篇文章主要為大家詳細(xì)介紹了springSecurity實(shí)現(xiàn)簡(jiǎn)單的登錄功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09
Java Web實(shí)現(xiàn)session過(guò)期后自動(dòng)跳轉(zhuǎn)到登陸頁(yè)功能【基于過(guò)濾器】
這篇文章主要介紹了Java Web實(shí)現(xiàn)session過(guò)期后自動(dòng)跳轉(zhuǎn)到登陸頁(yè)功能,涉及java過(guò)濾器針對(duì)session的判斷與跳轉(zhuǎn)相關(guān)操作技巧,需要的朋友可以參考下2017-11-11
spring如何快速穩(wěn)定解決循環(huán)依賴問(wèn)題
這篇文章主要介紹了spring如何快速穩(wěn)定解決循環(huán)依賴問(wèn)題,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
Java容器ArrayList知識(shí)點(diǎn)總結(jié)
本篇文章給大家分享了Java容器ArrayList的相關(guān)知識(shí)點(diǎn),對(duì)此有需要的朋友可以跟著學(xué)習(xí)參考下。2018-05-05
Java讀取制表符文本轉(zhuǎn)換為JSON實(shí)現(xiàn)實(shí)例
在Java開(kāi)發(fā)中,處理各種數(shù)據(jù)格式是常見(jiàn)的任務(wù),本文將介紹如何使用Java讀取制表符文本文件,并將其轉(zhuǎn)換為JSON格式,以便于后續(xù)的數(shù)據(jù)處理和分析,我們將使用Java中的相關(guān)庫(kù)來(lái)實(shí)現(xiàn)這個(gè)過(guò)程,并提供詳細(xì)的代碼示例2024-01-01
SpringBoot項(xiàng)目從搭建到發(fā)布一條龍
這篇文章主要介紹了SpringBoot項(xiàng)目從搭建到發(fā)布一條龍,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
spring+maven實(shí)現(xiàn)郵件發(fā)送
這篇文章主要為大家詳細(xì)介紹了spring+maven實(shí)現(xiàn)郵件發(fā)送,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
Java中的相除(/)和取余(%)的實(shí)現(xiàn)方法
這篇文章主要介紹了Java中的相除(/)和取余(%)的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07

