网络投票系统作为现代校园活动管理的重要工具,正在逐步取代传统的纸质投票方式。我选择这个课题源于对校园实际需求的观察——每年学校都会组织各类评选活动,从优秀学生干部选举到文艺汇演节目评比,传统方式不仅消耗大量纸张,统计环节也容易出现人为错误。基于Web的投票系统能够实现投票流程的数字化管理,显著提升组织效率和结果可信度。
这个毕业设计项目旨在开发一个轻量级的校园网络投票平台,主要服务于中小型评选活动(参与人数在500人以内)。系统采用B/S架构,区分管理员和普通用户两种角色。管理员负责投票活动的创建、管理和结果统计;普通用户则通过浏览器完成注册、登录、投票和评论等操作。整个系统设计遵循"功能完备但不过度复杂"的原则,确保在毕业设计周期内可完成核心功能的实现。
系统采用经典的RBAC(基于角色的访问控制)模型,将用户分为两个层级:
普通用户权限:
管理员权限:
权限控制通过Spring Security实现,在Controller层进行注解式权限校验。例如管理员专属的投票管理接口会添加@PreAuthorize("hasRole('ADMIN')")注解,确保未授权用户无法访问。
作为系统的核心功能,投票管理包含以下关键子功能:
活动创建:
活动进行中管理:
活动结束后处理:
技术实现上,采用Vue.js前端配合Spring Boot后端提供的RESTful API。前端通过axios发起异步请求,后端使用MyBatis-Plus操作MySQL数据库。投票数据变更时会通过WebSocket推送到前端,实现统计结果的实时更新。
经过对校园实际环境和技术成熟度的评估,最终确定的技术方案如下:
前端技术栈:
后端技术栈:
数据库:
开发工具:
选择这套技术栈主要基于以下考虑:
系统共设计12张核心表,这里重点说明几个关键表结构:
用户表(user):
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '加密后的密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`role` enum('USER','ADMIN') NOT NULL DEFAULT 'USER' COMMENT '用户角色',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
投票活动表(vote_activity):
sql复制CREATE TABLE `vote_activity` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '活动标题',
`description` text COMMENT '活动描述',
`cover_image` varchar(255) DEFAULT NULL COMMENT '封面图URL',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`max_choices` int NOT NULL DEFAULT '1' COMMENT '最多可选数',
`anonymous` tinyint NOT NULL DEFAULT '0' COMMENT '是否匿名投票',
`status` enum('NOT_STARTED','ONGOING','ENDED') NOT NULL DEFAULT 'NOT_STARTED' COMMENT '活动状态',
`creator_id` bigint NOT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_creator` (`creator_id`),
KEY `idx_time` (`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
投票记录表(vote_record):
sql复制CREATE TABLE `vote_record` (
`id` bigint NOT NULL AUTO_INCREMENT,
`activity_id` bigint NOT NULL COMMENT '活动ID',
`user_id` bigint NOT NULL COMMENT '投票用户ID',
`option_id` bigint NOT NULL COMMENT '投票选项ID',
`vote_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '投票时间',
`ip_address` varchar(50) DEFAULT NULL COMMENT 'IP地址',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_activity` (`user_id`,`activity_id`) COMMENT '用户-活动唯一索引',
KEY `idx_activity` (`activity_id`),
KEY `idx_option` (`option_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
为防止重复投票,在vote_record表上建立了(user_id, activity_id)的联合唯一索引,这样当同一用户对同一活动重复提交时,数据库会直接抛出唯一键冲突异常。前端也会在用户投票后立即禁用投票按钮,实现双重防护。
除了数据库层的唯一索引约束,系统还实现了以下防护措施:
IP频率限制:
@RateLimiter注解实现方法级限流验证码机制:
Cookie指纹:
核心限流代码如下:
java复制@RateLimiter(value = 5, key = "#ipAddress")
@PostMapping("/vote")
public Result vote(@RequestBody VoteDTO voteDTO,
@RequestHeader("X-Real-IP") String ipAddress) {
// 验证投票有效性
if(voteService.hasVoted(voteDTO.getUserId(), voteDTO.getActivityId())) {
throw new BusinessException("您已经参与过本次投票");
}
// 记录投票
return voteService.addVote(voteDTO, ipAddress);
}
考虑到校园活动可能在短时间内有大量用户集中投票,系统做了以下性能优化:
缓存策略:
数据库优化:
异步处理:
@Async实现异步方法投票统计的缓存实现示例:
java复制public VoteStatsDTO getVoteStats(Long activityId) {
String cacheKey = "vote:stats:" + activityId;
// 先查缓存
VoteStatsDTO stats = (VoteStatsDTO)redisTemplate.opsForValue().get(cacheKey);
if(stats != null) {
return stats;
}
// 缓存未命中,查数据库
stats = voteMapper.selectStatsByActivityId(activityId);
// 写入缓存,过期时间动态设置为活动结束剩余时间
long ttl = voteMapper.selectEndTime(activityId) - System.currentTimeMillis();
if(ttl > 0) {
redisTemplate.opsForValue().set(
cacheKey,
stats,
ttl,
TimeUnit.MILLISECONDS
);
}
return stats;
}
前端采用Vue 3的组合式API开发,主要组件包括:
Auth组件:
ActivityList组件:
VoteDetail组件:
AdminConsole组件:
使用Pinia进行状态管理,典型store定义如下:
javascript复制// stores/activity.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import api from '@/api'
export const useActivityStore = defineStore('activity', () => {
const currentActivity = ref(null)
const stats = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchActivity = async (id) => {
try {
loading.value = true
const response = await api.get(`/activities/${id}`)
currentActivity.value = response.data
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
const fetchStats = async (id) => {
const response = await api.get(`/activities/${id}/stats`)
stats.value = response.data
}
const isActive = computed(() => {
return currentActivity.value?.status === 'ONGOING'
})
return {
currentActivity,
stats,
loading,
error,
fetchActivity,
fetchStats,
isActive
}
})
通过WebSocket实现投票结果的实时更新:
javascript复制// utils/socket.js
import { io } from 'socket.io-client'
const socket = io(import.meta.env.VITE_WS_URL, {
autoConnect: false,
reconnectionAttempts: 3,
transports: ['websocket']
})
export const connectSocket = (token) => {
socket.auth = { token }
socket.connect()
}
export const subscribeToActivity = (activityId, callback) => {
socket.on(`activity:${activityId}`, callback)
socket.emit('subscribe', `activity:${activityId}`)
}
export const unsubscribeFromActivity = (activityId) => {
socket.off(`activity:${activityId}`)
socket.emit('unsubscribe', `activity:${activityId}`)
}
export default socket
javascript复制import { onMounted, onUnmounted } from 'vue'
import { subscribeToActivity, unsubscribeFromActivity } from '@/utils/socket'
import { useActivityStore } from '@/stores/activity'
const store = useActivityStore()
onMounted(() => {
subscribeToActivity(props.activityId, (data) => {
store.stats = data
})
})
onUnmounted(() => {
unsubscribeFromActivity(props.activityId)
})
根据多年指导经验,网络投票系统类毕设答辩中,评委常关注以下问题:
典型问题:
应对建议:
典型问题:
应对建议:
典型问题:
应对建议:
在实际开发过程中,我总结了以下值得注意的经验点:
时间管理建议:
技术难点预警:
常见错误排查:
markdown复制| 现象 | 可能原因 | 解决方案 |
|----------------------|--------------------------|----------------------------|
| 投票结果不更新 | WebSocket未正确连接 | 检查socket.io服务端状态 |
| 管理员无法创建活动 | JWT角色声明未正确传递 | 检查Spring Security配置 |
| 分页查询性能差 | 缺少复合索引 | 为查询条件添加适当索引 |
| 移动端布局错乱 | 未使用响应式布局 | 引入Flex/Grid布局系统 |
测试要点:
在数据库设计阶段,我最初没有充分考虑投票记录表的分表需求,导致测试数据量较大时查询性能明显下降。后来通过按活动ID哈希分片解决了这个问题,这也提醒我在设计之初就应该评估数据增长规模。另一个教训是关于事务处理 - 最初没有对投票操作添加事务隔离,在高压测试时出现了票数统计不准确的情况,通过添加@Transactional注解和乐观锁机制最终解决了这个问题。