1. 项目概述
在线投票系统是现代组织活动中不可或缺的工具,无论是企业内部决策、学校选举还是社区活动,都需要一个安全可靠的投票平台。这个基于Spring Boot + MyBatis的解决方案,正是针对这类需求而设计的轻量级实现方案。
我去年为本地社区开发过一个类似的系统,处理了超过5000人同时在线投票的场景。这套技术栈的选择并非偶然 - Spring Boot提供了快速开发的便利,MyBatis则保证了数据操作的灵活性,两者结合既满足了性能需求,又保持了代码的可维护性。
2. 技术选型解析
2.1 为什么选择Spring Boot
Spring Boot的自动配置特性让我们可以快速搭建起一个完整的Web应用。我特别看重的是它内嵌的Tomcat服务器,这省去了单独部署Web容器的麻烦。在实际开发中,通过简单的@SpringBootApplication注解就能启动整个应用,开发效率提升明显。
另一个关键因素是Spring Security的集成。投票系统对安全性要求极高,Spring Security提供了开箱即用的认证授权机制。我在项目中配置了基于角色的访问控制(RBAC),确保只有授权用户才能创建和参与投票。
2.2 MyBatis的优势考量
相比Hibernate等全自动ORM框架,MyBatis的半自动化特性更适合这个项目。投票系统中有不少复杂的统计查询,需要精细控制SQL语句。MyBatis的XML映射文件方式让这些复杂查询变得清晰可维护。
我特别喜欢MyBatis的动态SQL功能。比如在实现按条件筛选投票记录时,使用 标签可以灵活构建查询条件,避免了拼接SQL字符串的安全风险。下面是一个典型示例:
<select id="findVotesByCondition" resultType="Vote"> SELECT * FROM votes <where> <if test="title != null"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="status != null"> AND status = #{status} </if> </where> </select>3. 核心功能实现
3.1 投票创建与管理模块
投票创建是整个系统的核心功能之一。我设计了多层次的校验机制:
- 前端使用Vue.js进行基础校验(如截止时间必须晚于当前时间)
- 后端使用Spring Validation进行二次校验
- 数据库层面设置约束条件(如非空字段)
投票选项采用了动态设计,创建者可以自由添加任意数量的选项。数据库设计上,我使用了投票主表和选项从表的一对多关系:
@Entity public class Vote { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String description; private LocalDateTime endTime; @OneToMany(mappedBy = "vote", cascade = CascadeType.ALL) private List<VoteOption> options = new ArrayList<>(); }3.2 投票参与与防刷机制
防止重复投票是系统的关键需求。我实现了基于用户ID+投票ID的唯一约束,确保每个用户对每个投票只能参与一次。同时加入了IP限制和Cookie验证作为辅助防护。
在性能优化方面,我使用了Redis缓存热门投票的当前结果,减轻数据库压力。投票提交时采用异步处理,通过Spring的@Async注解实现:
@Async @Transactional public void submitVote(Long voteId, Long optionId, Long userId) { // 验证投票有效性 // 记录投票结果 // 更新统计缓存 }4. 数据库设计与优化
4.1 核心表结构设计
经过多次迭代,最终确定的数据库主要包含以下表:
- 用户表(users):存储用户基本信息
- 投票表(votes):投票主题、描述、时间等
- 选项表(vote_options):投票的具体选项
- 投票记录表(vote_records):记录用户投票选择
特别需要注意的是投票记录表的设计,它包含了防止重复投票的关键约束:
CREATE TABLE vote_records ( id BIGINT PRIMARY KEY AUTO_INCREMENT, vote_id BIGINT NOT NULL, user_id BIGINT NOT NULL, option_id BIGINT NOT NULL, vote_time DATETIME NOT NULL, UNIQUE KEY uk_vote_user (vote_id, user_id), FOREIGN KEY (vote_id) REFERENCES votes(id), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (option_id) REFERENCES vote_options(id) );4.2 查询性能优化
投票系统最频繁的操作是查询投票结果。为提高性能,我采取了以下措施:
- 为常用查询字段添加索引
- 使用MyBatis的二级缓存
- 对统计结果进行预计算和缓存
- 实现分页查询避免大数据量传输
一个典型的优化案例是投票结果统计查询。原始方案是每次实时计算,后来改为定时任务预计算+事件触发更新:
@Scheduled(cron = "0 */5 * * * ?") public void updateVoteStats() { List<Vote> activeVotes = voteRepository.findActiveVotes(); activeVotes.forEach(vote -> { Map<Long, Long> stats = voteRecordRepository.countByOption(vote.getId()); redisTemplate.opsForHash().putAll("vote:stats:" + vote.getId(), stats); }); }5. 安全防护措施
5.1 认证与授权
系统采用JWT进行无状态认证,结合Spring Security实现完整的权限控制。我特别注意了以下几点:
- 密码存储使用BCrypt强哈希
- JWT设置合理的过期时间(通常2小时)
- 实现token刷新机制
- 敏感操作要求二次验证
Spring Security配置核心如下:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/votes/create").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager())); } }5.2 防攻击策略
针对常见的Web攻击,我实施了多重防护:
- XSS防护:对用户输入进行过滤和转义
- CSRF防护:虽然API采用无状态设计,但仍对表单操作添加token验证
- SQL注入防护:坚持使用MyBatis的参数化查询
- DDoS防护:实现API限流(使用Guava RateLimiter)
一个具体的防护实现是投票提交的限流控制:
private final RateLimiter rateLimiter = RateLimiter.create(10.0); // 每秒10次 @PostMapping("/vote") public ResponseEntity<?> submitVote(@RequestBody VoteRequest request) { if (!rateLimiter.tryAcquire()) { throw new RateLimitExceededException(); } // 处理投票逻辑 }6. 部署与监控
6.1 生产环境部署
系统支持多种部署方式:
- 传统WAR包部署到外部Tomcat
- 使用内嵌容器直接运行JAR
- Docker容器化部署
我推荐使用Docker方式,因为它的环境一致性最好。Dockerfile配置示例:
FROM openjdk:11-jre-slim VOLUME /tmp COPY target/vote-system.jar app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]配合docker-compose可以轻松启动包含MySQL和Redis的完整环境:
version: '3' services: app: build: . ports: - "8080:8080" depends_on: - redis - mysql redis: image: redis:alpine ports: - "6379:6379" mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: votedb MYSQL_USER: voteuser MYSQL_PASSWORD: votepass ports: - "3306:3306"6.2 系统监控
为确保系统稳定运行,我集成了以下监控措施:
- Spring Boot Actuator提供健康检查
- Prometheus + Grafana监控系统指标
- ELK日志收集系统
- 自定义业务指标监控
一个有用的实践是监控投票活动的异常情况:
@RestController @RequestMapping("/api/admin") public class AdminController { @Autowired private MeterRegistry meterRegistry; @PostMapping("/vote") public ResponseEntity<?> createVote(@Valid @RequestBody VoteCreateDTO dto) { try { Vote vote = voteService.createVote(dto); meterRegistry.counter("vote.created").increment(); return ResponseEntity.ok(vote); } catch (Exception e) { meterRegistry.counter("vote.create.failed").increment(); throw e; } } }7. 测试策略
7.1 自动化测试体系
完善的测试是保证系统质量的关键。我建立了多层次的测试防护网:
- 单元测试:使用JUnit5 + Mockito测试业务逻辑
- 集成测试:使用SpringBootTest测试完整调用链
- API测试:使用TestRestTemplate测试HTTP接口
- 前端测试:使用Jest测试Vue组件
一个典型的服务层测试示例:
@ExtendWith(MockitoExtension.class) class VoteServiceTest { @Mock private VoteRepository voteRepository; @InjectMocks private VoteService voteService; @Test void shouldCreateVoteSuccessfully() { VoteCreateDTO dto = new VoteCreateDTO("Test Vote", "Description", LocalDateTime.now().plusDays(1)); when(voteRepository.save(any(Vote.class))) .thenAnswer(invocation -> invocation.getArgument(0)); Vote result = voteService.createVote(dto); assertNotNull(result); assertEquals("Test Vote", result.getTitle()); verify(voteRepository).save(any(Vote.class)); } }7.2 性能测试
使用JMeter对系统进行了压力测试,重点关注:
- 并发创建投票的性能
- 高并发投票时的响应时间
- 长时间运行的稳定性
测试发现的一个关键问题是投票结果统计查询在高并发下会成为瓶颈。通过引入Redis缓存和预计算机制,我们将95%的请求响应时间从1200ms降低到了150ms。
8. 扩展与改进方向
虽然当前系统已经满足基本需求,但仍有改进空间:
- 增加多语言支持:使用Spring的MessageSource实现国际化
- 改进用户体验:添加实时结果更新(WebSocket)
- 增强分析功能:集成大数据分析投票模式
- 移动端优化:开发专门的PWA应用
一个值得尝试的扩展是实时结果展示:
@Controller public class VoteResultController { @Autowired private SimpMessagingTemplate messagingTemplate; @PostMapping("/vote") public ResponseEntity<?> submitVote(@RequestBody VoteRequest request) { // 处理投票 VoteStats stats = voteService.getVoteStats(request.getVoteId()); messagingTemplate.convertAndSend("/topic/vote/" + request.getVoteId(), stats); return ResponseEntity.ok().build(); } }在前端,使用StompJS订阅更新:
const socket = new SockJS('/ws'); const client = Stomp.over(socket); client.connect({}, () => { client.subscribe(`/topic/vote/${voteId}`, (message) => { const stats = JSON.parse(message.body); updateResultChart(stats); }); });这套系统从设计到实现历时两个月,期间遇到了不少挑战,特别是高并发场景下的数据一致性问题。最终通过合理的架构设计和多种技术手段的组合应用,成功构建了一个稳定可靠的在线投票平台。对于想要学习Spring Boot和MyBatis实际应用的开发者来说,这个项目涵盖了许多典型场景和解决方案,值得深入研究和实践。