1. 项目概述
在线投票系统是解决传统线下投票效率低、统计繁琐、地域限制等问题的轻量化解决方案。作为一名长期从事企业级应用开发的工程师,我发现很多组织仍然在使用纸质投票或简单的Excel统计方式,这不仅耗时耗力,还容易出现人为错误。基于Spring Boot和MyBatis的在线投票系统正是为了解决这些痛点而设计的。
这个系统特别适合以下场景:
- 企业内部员工满意度调查
- 学校社团活动投票
- 社区公共事务决策
- 小型商业机构的产品调研
提示:在设计投票系统时,最关键的是要平衡功能完整性和系统复杂度。我们不需要一开始就实现所有高级功能,而是应该先构建一个可用的核心版本,再根据实际需求逐步扩展。
2. 系统设计与技术选型
2.1 架构设计
系统采用经典的三层架构设计:
code复制┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Controller层 │ ←→ │ Service层 │ ←→ │ DAO层 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
↑ ↓
┌─────────────────┐ ┌─────────────────┐
│ 前端界面 │ │ MySQL数据库 │
└─────────────────┘ └─────────────────┘
这种分层设计的主要优势在于:
- 职责分离:每层只关注自己的核心职责,便于维护和扩展
- 可测试性:可以单独测试每一层的功能
- 灵活性:可以替换某一层的实现而不影响其他层
2.2 技术选型解析
Spring Boot 是我们的基础框架选择。相比传统的Spring MVC,Spring Boot有以下几个显著优势:
- 自动配置:减少了大量的XML配置
- 内嵌服务器:可以直接打包成可执行的JAR文件
- 丰富的starter依赖:简化了常见功能的集成
MyBatis 作为ORM框架,特别适合我们这个项目,因为:
- 投票系统有很多复杂的查询需求(如多条件筛选、关联查询)
- 我们需要对SQL有完全的控制权,以便进行性能优化
- 动态SQL功能可以简化条件查询的实现
MySQL 是关系型数据库的可靠选择,特别是对于投票系统这种需要严格保证数据一致性的场景。我们使用InnoDB引擎,它支持事务和行级锁,这对并发投票场景非常重要。
3. 核心功能实现
3.1 数据库设计
我们的数据库包含以下几个核心表:
用户表(users)
sql复制CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
email VARCHAR(100),
role ENUM('USER','ADMIN') DEFAULT 'USER',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
投票表(votes)
sql复制CREATE TABLE votes (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
description TEXT,
creator_id BIGINT NOT NULL,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP NOT NULL,
status ENUM('DRAFT','ACTIVE','ENDED') DEFAULT 'DRAFT',
FOREIGN KEY (creator_id) REFERENCES users(id)
);
选项表(options)
sql复制CREATE TABLE options (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
vote_id BIGINT NOT NULL,
content VARCHAR(200) NOT NULL,
vote_count INT DEFAULT 0,
FOREIGN KEY (vote_id) REFERENCES votes(id)
);
投票记录表(vote_records)
sql复制CREATE TABLE vote_records (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
vote_id BIGINT NOT NULL,
option_id BIGINT NOT NULL,
voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(50),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (vote_id) REFERENCES votes(id),
FOREIGN KEY (option_id) REFERENCES options(id),
UNIQUE KEY (user_id, vote_id)
);
注意:vote_records表中的唯一约束(user_id, vote_id)确保了每个用户对每个投票只能投一次,这是防止重复投票的基础机制。
3.2 用户管理实现
用户认证采用Spring Security框架,核心配置如下:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
用户注册的核心逻辑:
java复制@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public User register(RegisterRequest request) {
if(userRepository.existsByUsername(request.getUsername())) {
throw new RuntimeException("用户名已存在");
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(request.getEmail());
user.setRole("USER");
return userRepository.save(user);
}
}
3.3 投票管理实现
投票创建服务的核心代码:
java复制@Service
public class VoteServiceImpl implements VoteService {
@Autowired
private VoteRepository voteRepository;
@Autowired
private OptionRepository optionRepository;
@Override
@Transactional
public Vote createVote(VoteCreateRequest request, Long creatorId) {
// 验证结束时间是否合理
if(request.getEndTime().isBefore(LocalDateTime.now())) {
throw new IllegalArgumentException("结束时间不能早于当前时间");
}
// 创建投票主体
Vote vote = new Vote();
vote.setTitle(request.getTitle());
vote.setDescription(request.getDescription());
vote.setCreatorId(creatorId);
vote.setStartTime(LocalDateTime.now());
vote.setEndTime(request.getEndTime());
vote.setStatus(VoteStatus.ACTIVE);
vote = voteRepository.save(vote);
// 添加投票选项
for (String optionContent : request.getOptions()) {
Option option = new Option();
option.setVoteId(vote.getId());
option.setContent(optionContent);
option.setVoteCount(0);
optionRepository.save(option);
}
return vote;
}
}
动态查询投票列表的MyBatis实现:
xml复制<!-- VoteMapper.xml -->
<select id="findVotes" resultType="com.example.votesystem.model.Vote">
SELECT v.*, u.username as creator_name
FROM votes v
JOIN users u ON v.creator_id = u.id
<where>
<if test="creatorId != null">
AND v.creator_id = #{creatorId}
</if>
<if test="status != null">
AND v.status = #{status}
</if>
<if test="keyword != null and keyword != ''">
AND (v.title LIKE CONCAT('%', #{keyword}, '%')
OR v.description LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
ORDER BY v.created_at DESC
</select>
4. 系统安全与性能优化
4.1 防刷票机制
为了防止恶意刷票,我们实现了多重防护措施:
-
基础防护:
- 用户认证:必须登录才能投票
- 唯一性约束:一个用户对一个投票只能投一次
-
IP限制:
java复制@Service
public class VoteServiceImpl implements VoteService {
@Override
public void recordVote(Long voteId, Long optionId, Long userId, String ip) {
// 检查同一IP在最近1小时内的投票次数
int votesFromSameIp = voteRecordRepository.countByIpAndTimeAfter(
ip, LocalDateTime.now().minusHours(1));
if(votesFromSameIp > 10) {
throw new RuntimeException("投票过于频繁,请稍后再试");
}
// 记录投票...
}
}
- 验证码:在投票前要求用户输入图形验证码
4.2 性能优化
对于高并发的投票场景,我们采用以下优化策略:
- 缓存热门投票结果:
java复制@Service
public class VoteServiceImpl implements VoteService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String VOTE_RESULT_KEY = "vote:result:%d";
@Override
public VoteResult getVoteResult(Long voteId) {
String key = String.format(VOTE_RESULT_KEY, voteId);
VoteResult result = (VoteResult) redisTemplate.opsForValue().get(key);
if(result == null) {
result = calculateVoteResult(voteId);
redisTemplate.opsForValue().set(key, result, 5, TimeUnit.MINUTES);
}
return result;
}
}
- 数据库优化:
- 为常用查询字段添加索引
- 使用读写分离架构
- 定期归档历史投票数据
5. 测试与部署
5.1 测试策略
我们采用分层测试策略:
- 单元测试:使用JUnit + Mockito测试Service层
java复制@ExtendWith(MockitoExtension.class)
class VoteServiceTest {
@Mock
private VoteRepository voteRepository;
@Mock
private OptionRepository optionRepository;
@InjectMocks
private VoteServiceImpl voteService;
@Test
void createVote_WithValidRequest_ShouldSuccess() {
VoteCreateRequest request = new VoteCreateRequest();
request.setTitle("Test Vote");
request.setDescription("Test Description");
request.setEndTime(LocalDateTime.now().plusDays(1));
request.setOptions(Arrays.asList("Option1", "Option2"));
when(voteRepository.save(any())).thenReturn(new Vote());
Vote result = voteService.createVote(request, 1L);
assertNotNull(result);
verify(optionRepository, times(2)).save(any());
}
}
- 集成测试:使用Testcontainers测试数据库交互
- API测试:使用Postman进行端到端测试
5.2 部署方案
推荐使用Docker进行容器化部署:
dockerfile复制# Dockerfile
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/vote-system-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
使用docker-compose编排服务:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/vote_system
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=password
depends_on:
- db
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=vote_system
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
6. 扩展方向
6.1 多端适配
-
微信小程序:
- 使用uni-app框架开发跨平台前端
- 复用现有后端API
- 添加微信登录支持
-
移动端优化:
- 响应式设计
- 触摸友好的交互
- 离线投票功能
6.2 实时交互
使用WebSocket实现实时投票结果更新:
java复制@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*");
}
}
@Controller
public class VoteResultController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@PostMapping("/api/votes/{voteId}/options/{optionId}/vote")
public ResponseEntity<?> vote(@PathVariable Long voteId,
@PathVariable Long optionId,
@AuthenticationPrincipal User user) {
// 处理投票逻辑...
// 广播投票结果更新
VoteResult result = voteService.getVoteResult(voteId);
messagingTemplate.convertAndSend("/topic/vote/" + voteId, result);
return ResponseEntity.ok().build();
}
}
6.3 数据分析
使用ELK栈实现投票数据分析:
- 数据采集:记录用户投票行为日志
- 存储:使用Elasticsearch存储日志数据
- 可视化:使用Kibana创建数据分析仪表盘
核心指标包括:
- 投票参与率
- 选项分布
- 用户投票时间分布
- 异常投票行为检测
在实际开发过程中,我发现有几个关键点需要特别注意:
- 投票时间的处理要特别小心,必须考虑时区问题,建议在数据库中统一存储UTC时间
- 对于大型投票活动,要考虑分页查询和懒加载策略,避免一次性加载过多数据
- 防刷票机制需要根据实际场景调整参数,过于严格会影响正常用户体验