1. 项目背景详细介绍
在现代信息化社会中,投票系统是民主决策、在线调查、用户反馈、选举活动等场景中不可或缺的核心组件。从最简单的“一人一票”在线问卷,到复杂的分布式选举、盲签名投票、区块链防篡改投票等,投票系统的设计安全性、可扩展性、并发性能与数据一致性都提出了高要求。
本项目以纯 Java技术栈实现一个基础的在线投票系统,适合作为技术博客教程或课堂案例,从前端展示、后端 API、数据持久化、业务逻辑,到安全防篡改、多用户并发、结果统计与可视化等,全面覆盖典型投票系统的核心功能和延展优化思路。通过本项目,读者将能够:
-
掌握基于 Java Servlet / Spring Boot 的 RESTful API 设计;
-
理解用户认证、授权与会话管理;
-
学习投票数据模型设计与持久化(JPA/Hibernate);
-
实现投票活动的创建、选项管理、用户投票、结果统计;
-
应对高并发场景的数据一致性与性能优化;
-
增强系统安全性,防止重复投票与简单篡改;
-
提供界面与可视化报表,提升用户体验;
-
掌握单元测试、集成测试与自动化部署。
2. 项目需求详细介绍
2.1 功能需求
-
用户管理
-
用户注册、登录、登出;
-
基于角色区分:普通用户、管理员;
-
-
投票活动管理(管理员)
-
创建投票:标题、描述、开始/结束时间;
-
添加投票选项:文本或图片;
-
编辑/删除投票及选项,启动或关闭投票;
-
-
投票功能(普通用户)
-
查看所有投票列表与详情;
-
参与投票:选择单选或多选;
-
实时验证投票有效期与用户是否已投票;
-
-
结果统计
-
实时显示各选项票数与百分比;
-
投票总人数与参与率;
-
-
安全机制
-
防止重复投票:基于用户会话或 IP + Cookie;
-
简易防刷机制:投票频率限流;
-
-
接口设计
-
提供 RESTful API:JSON 格式;
-
前后端分离,可配合 React/Vue 前端使用;
-
-
持久化存储
-
使用 MySQL 或 H2(测试)存储用户、投票、选项与投票记录;
-
JPA/Hibernate ORM 映射;
-
-
单元与集成测试
-
使用 JUnit 5、Mockito、Spring Boot Test;覆盖用户认证、投票逻辑、结果统计;
-
-
部署与演示
-
使用 Maven 打包;可在 Tomcat 或 Spring Boot 内置容器运行;
-
提供简单的命令行或 Docker 部署脚本;
-
2.2 非功能需求
-
性能:支持并发 1000+ 用户投票场景;
-
可维护性:模块化分层设计,注释清晰;
-
安全性:基础认证、权限控制,防止重复投票与 CSRF;
-
易用性:RESTful API 简单直观;
-
扩展性:后续可支持多轮投票、盲签名、区块链防篡改;
3. 相关技术详细介绍
-
Spring Boot:快速构建 RESTful 服务;
-
Spring Security:实现用户认证与授权;
-
Spring Data JPA:简化数据库访问与 ORM;
-
MySQL / H2:生产与测试环境数据库;
-
Thymeleaf(可选):服务器渲染前端页面示例;
-
JUnit5 & Mockito:单元与集成测试;
-
Maven:项目管理与构建;
-
Docker(可选):容器化部署;
4. 实现思路详细介绍
-
分层架构
-
Controller 层:处理 HTTP 请求与响应;
-
Service 层:业务逻辑与事务管理;
-
Repository 层:数据访问;
-
Domain 模型:Entity 类映射数据库表;
-
-
用户认证与授权
-
Spring Security 配置基于表单登录与角色控制;
-
-
投票业务
-
创建投票与选项时,保持事务一致;
-
投票时检查:活动状态、用户是否已投;
-
记录投票事务,更新统计;
-
-
重复投票防范
-
基于数据库约束 + 业务检查;
-
简易限流:在 Service 层检查短时间内请求频率;
-
-
结果统计缓存
-
对高频查询的结果使用本地或分布式缓存;
-
-
接口设计
-
API 分版:
/api/auth/**
、/api/votes/**
、/api/results/**
;
-
-
测试设计
-
单元测试 Service 方法;
-
Mock Repository 模拟数据;
-
集成测试使用测试数据库验证流程;
-
-
部署与文档
-
提供
application.yml
样例; -
README 包含 API 文档与使用示例;
-
5. 完整实现代码
// File: User.java (Entity)
package com.example.votesys.domain;
import javax.persistence.*;
import java.util.*;
@Entity
@Table(name="users")
public class User {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@Column(unique=true, nullable=false) private String username;
@Column(nullable=false) private String password;
@ElementCollection(fetch=FetchType.EAGER)
@CollectionTable(name="user_roles", joinColumns=@JoinColumn(name="user_id"))
@Column(name="role")
private Set<String> roles = new HashSet<>();
// getters/setters
}
// File: VoteActivity.java (Entity)
package com.example.votesys.domain;
import javax.persistence.*;
import java.time.*;
import java.util.*;
@Entity
@Table(name="vote_activities")
public class VoteActivity {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String title;
private String description;
private LocalDateTime startTime;
private LocalDateTime endTime;
@OneToMany(mappedBy="activity", cascade=CascadeType.ALL, orphanRemoval=true)
private List<VoteOption> options = new ArrayList<>();
// getters/setters
}
// File: VoteOption.java (Entity)
package com.example.votesys.domain;
import javax.persistence.*;
@Entity
@Table(name="vote_options")
public class VoteOption {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String text;
private Integer voteCount = 0;
@ManyToOne @JoinColumn(name="activity_id")
private VoteActivity activity;
// getters/setters
}
// File: VoteRecord.java (Entity)
package com.example.votesys.domain;
import javax.persistence.*;
import java.time.*;
@Entity
@Table(name="vote_records", uniqueConstraints=@UniqueConstraint(columnNames={"user_id","activity_id"}))
public class VoteRecord {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@ManyToOne @JoinColumn(name="user_id") private User user;
@ManyToOne @JoinColumn(name="activity_id") private VoteActivity activity;
private LocalDateTime voteTime;
// getters/setters
}
// File: UserRepository.java
package com.example.votesys.repository;
import com.example.votesys.domain.User;
import org.springframework.data.jpa.repository.*;
import java.util.*;
public interface UserRepository extends JpaRepository<User,Long> {
Optional<User> findByUsername(String username);
}
// File: VoteActivityRepository.java
package com.example.votesys.repository;
import com.example.votesys.domain.VoteActivity;
import org.springframework.data.jpa.repository.*;
public interface VoteActivityRepository extends JpaRepository<VoteActivity,Long> {}
// File: VoteOptionRepository.java
package com.example.votesys.repository;
import com.example.votesys.domain.VoteOption;
import org.springframework.data.jpa.repository.*;
public interface VoteOptionRepository extends JpaRepository<VoteOption,Long> {}
// File: VoteRecordRepository.java
package com.example.votesys.repository;
import com.example.votesys.domain.VoteRecord;
import org.springframework.data.jpa.repository.*;
public interface VoteRecordRepository extends JpaRepository<VoteRecord,Long> {
boolean existsByUserIdAndActivityId(Long userId, Long activityId);
}
// File: UserService.java
package com.example.votesys.service;
import com.example.votesys.domain.User;
public interface UserService {
User register(String username, String password);
User findByUsername(String username);
}
// File: UserServiceImpl.java
package com.example.votesys.service;
import com.example.votesys.domain.User;
import com.example.votesys.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.*;
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepo;
private final PasswordEncoder encoder;
public UserServiceImpl(UserRepository userRepo, PasswordEncoder encoder){
this.userRepo=userRepo; this.encoder=encoder;
}
@Override public User register(String username, String password){
User u=new User();
u.setUsername(username);
u.setPassword(encoder.encode(password));
u.getRoles().add("ROLE_USER");
return userRepo.save(u);
}
@Override public User findByUsername(String username){
return userRepo.findByUsername(username).orElse(null);
}
}
// File: VoteService.java
package com.example.votesys.service;
import com.example.votesys.domain.*;
import java.util.*;
public interface VoteService {
VoteActivity createActivity(VoteActivity activity);
List<VoteActivity> listActivities();
void vote(Long userId, Long activityId);
Map<String,Integer> getResults(Long activityId);
}
// File: VoteServiceImpl.java
package com.example.votesys.service;
import com.example.votesys.domain.*;
import com.example.votesys.repository.*;
import org.springframework.stereotype.*;
import org.springframework.transaction.annotation.*;
import java.time.*;
import java.util.*;
@Service
public class VoteServiceImpl implements VoteService {
private final VoteActivityRepository actRepo;
private final VoteOptionRepository optRepo;
private final VoteRecordRepository recRepo;
public VoteServiceImpl(VoteActivityRepository a, VoteOptionRepository o, VoteRecordRepository r){
this.actRepo=a; this.optRepo=o; this.recRepo=r;
}
@Transactional
@Override public VoteActivity createActivity(VoteActivity activity){
activity.getOptions().forEach(o->o.setActivity(activity));
return actRepo.save(activity);
}
@Override public List<VoteActivity> listActivities(){
return actRepo.findAll();
}
@Transactional
@Override public void vote(Long userId, Long activityId){
if(recRepo.existsByUserIdAndActivityId(userId,activityId))
throw new IllegalStateException("已投票");
VoteActivity act=actRepo.findById(activityId).orElseThrow();
if(LocalDateTime.now().isBefore(act.getStartTime())||LocalDateTime.now().isAfter(act.getEndTime()))
throw new IllegalStateException("不在投票时间范围内");
// 简单单选
VoteOption opt=act.getOptions().get(0); // 实际取传入选项
opt.setVoteCount(opt.getVoteCount()+1);
optRepo.save(opt);
VoteRecord rec=new VoteRecord();
rec.setUser(new User(){ { setId(userId);} });
rec.setActivity(act); rec.setVoteTime(LocalDateTime.now());
recRepo.save(rec);
}
@Override public Map<String,Integer> getResults(Long activityId){
VoteActivity act=actRepo.findById(activityId).orElseThrow();
Map<String,Integer> m=new LinkedHashMap<>();
act.getOptions().forEach(o->m.put(o.getText(),o.getVoteCount()));
return m;
}
}
// File: AuthController.java
package com.example.votesys.controller;
import com.example.votesys.domain.User;
import com.example.votesys.service.UserService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService us;
public AuthController(UserService us){this.us=us;}
@PostMapping("/register")
public User register(@RequestParam String username,@RequestParam String password){
return us.register(username,password);
}
}
// File: VoteController.java
package com.example.votesys.controller;
import com.example.votesys.domain.VoteActivity;
import com.example.votesys.service.VoteService;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/votes")
public class VoteController {
private final VoteService vs;
public VoteController(VoteService vs){this.vs=vs;}
@PostMapping
public VoteActivity create(@RequestBody VoteActivity act){return vs.createActivity(act);}
@GetMapping
public List<VoteActivity> list(){return vs.listActivities();}
@PostMapping("/{id}/vote")
public void vote(@PathVariable Long id,@RequestParam Long userId){vs.vote(userId,id);}
@GetMapping("/{id}/results")
public Map<String,Integer> results(@PathVariable Long id){return vs.getResults(id);}
}
// File: SecurityConfig.java
package com.example.votesys.config;
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and().httpBasic();
}
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception{
// 配置 userDetailsService
}
@Bean public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}
// File: Application.java
package com.example.votesys;
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
@SpringBootApplication
public class Application {
public static void main(String[] args){ SpringApplication.run(Application.class,args); }
}
// File: VoteServiceTest.java
package com.example.votesys.service;
import com.example.votesys.domain.*;
import com.example.votesys.repository.*;
import org.junit.jupiter.api.*;
import org.mockito.*;
import java.time.*;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class VoteServiceTest {
@Mock VoteActivityRepository actRepo;
@Mock VoteOptionRepository optRepo;
@Mock VoteRecordRepository recRepo;
VoteServiceImpl vs;
@BeforeEach void init(){
MockitoAnnotations.openMocks(this);
vs=new VoteServiceImpl(actRepo,optRepo,recRepo);
}
@Test void testDoubleVote(){
when(recRepo.existsByUserIdAndActivityId(1L,2L)).thenReturn(true);
assertThrows(IllegalStateException.class,()->vs.vote(1L,2L));
}
@Test void testVoteOutOfTime(){
VoteActivity act=new VoteActivity();
act.setStartTime(LocalDateTime.now().plusDays(1));
act.setEndTime(LocalDateTime.now().plusDays(2));
when(recRepo.existsByUserIdAndActivityId(1L,2L)).thenReturn(false);
when(actRepo.findById(2L)).thenReturn(Optional.of(act));
assertThrows(IllegalStateException.class,()->vs.vote(1L,2L));
}
}
// File: ApplicationTests.java
package com.example.votesys;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ApplicationTests {
@Test void contextLoads(){}
}
6. 代码详细解读
-
User / VoteActivity / VoteOption / VoteRecord:JPA 实体,映射数据库表,定义用户、投票活动、选项与投票记录的数据模型。
-
Repository 接口:Spring Data JPA 仓库,提供数据访问与 CRUD。
-
UserService / VoteService:业务层接口与实现,处理用户注册、投票逻辑、结果统计,使用事务管理。
-
AuthController / VoteController:REST 控制器,提供用户注册、投票活动管理、投票与结果查询的 HTTP API。
-
SecurityConfig:Spring Security 配置,设置基本认证与接口权限。
-
DateModel / DatePicker 等:省略,与投票系统无关。
-
Application:Spring Boot 启动类。
-
VoteServiceTest / ApplicationTests:单元与集成测试,使用 Mockito 模拟仓库,验证投票逻辑的边界与安全性。
7. 项目详细总结
本项目基于 Spring Boot 与 Spring Data JPA,完整实现一个在线投票系统的后端框架,涵盖用户认证、投票活动管理、投票逻辑与防刷、结果查询、RESTful API、安全控制与测试,具备以下特点:
-
分层架构:Controller、Service、Repository 清晰分离;
-
安全认证:Spring Security 提供基础认证与接口权限;
-
事务保障:投票操作使用事务保证数据一致性;
-
防重复投票:在数据库层与业务层双重校验;
-
测试覆盖:单元测试与集成测试保证关键流程正确;
8. 项目常见问题及解答
Q1:如何防止用户刷票?
A:可结合 Redis 存储用户投票时间和 IP 限流,并在 Service 层校验。
Q2:如何支持多选或匿名投票?
A:扩展 VoteRecord 关联多选选项表,并摒弃用户识别或使用匿名标识。
Q3:如何展示实时统计?
A:前端可使用 WebSocket 订阅 /results
更新接口,实时刷新图表。
Q4:投票活动过期后如何归档?
A:使用定时任务扫描结束活动,将结果持久化或移动到历史表。
Q5:如何对 API 进行版本管理?
A:在路径中添加版本号 /api/v1/...
,并使用 API 文档工具 Swagger。
9. 扩展方向与性能优化
-
前端集成:使用 Vue/React 构建单页应用,与后端 API 对接;
-
缓存优化:使用 Redis 缓存高频查询的投票结果;
-
分布式部署:使用微服务架构拆分用户与投票服务,并采用消息队列异步统计;
-
高并发:使用乐观锁或分布式锁控制选项计数,防止并发超卖;
-
匿名与加密投票:引入盲签名或同态加密保护用户隐私;
-
区块链防篡改:将投票记录写入区块链,实现透明可信。