投票系统是常见的业务功能,可用于活动评选、意见收集等场景,使用Golang开发可以借助其原生并发特性支撑高并发投票请求。本文将以Gin框架为基础,结合MySQL存储数据、Redis做缓存和防刷控制,完整实现一个可用的投票系统。

一、需求分析与技术选型
投票系统核心需求包括创建投票活动、用户参与投票、查看投票结果、防止重复投票。技术选型如下:
- 开发语言:Golang 1.20+
- Web框架:Gin,轻量高性能
- 数据库:MySQL 8.0,存储活动和投票数据
- 缓存:Redis,存储投票状态、防刷标识
- 依赖管理:Go Modules
二、数据库表设计
需要设计两张核心表,分别是投票活动表和投票记录表:
1. 投票活动表 vote_activity
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 活动ID,主键自增 |
| title | varchar(255) | 活动标题 |
| start_time | datetime | 活动开始时间 |
| end_time | datetime | 活动结束时间 |
| status | tinyint | 活动状态,1正常 0下线 |
2. 投票记录表 vote_record
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 记录ID,主键自增 |
| activity_id | bigint | 关联的活动ID |
| option_id | bigint | 投票选项ID |
| user_id | bigint | 投票用户ID |
| vote_time | datetime | 投票时间 |
三、项目初始化与依赖配置
首先创建项目目录,初始化Go模块,安装所需依赖:
# 初始化项目 mkdir vote-system && cd vote-system go mod init vote-system # 安装依赖 go get github.com/gin-gonic/gin go get gorm.io/gorm go get gorm.io/driver/mysql go get github.com/go-redis/redis/v8
四、核心功能实现
1. 初始化数据库连接
分别初始化MySQL和Redis连接:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
var RedisClient *redis.Client
var Ctx = context.Background()
// 初始化MySQL
func initMySQL() {
dsn := "root:password@tcp(127.0.0.1:3306)/vote_db?charset=utf8mb4&parseTime=True"
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("MySQL连接失败: %v", err)
}
// 自动迁移表结构
DB.AutoMigrate(&VoteActivity{}, &VoteRecord{})
}
// 初始化Redis
func initRedis() {
RedisClient = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
_, err := RedisClient.Ping(Ctx).Result()
if err != nil {
log.Fatalf("Redis连接失败: %v", err)
}
}
2. 定义数据模型
创建对应的Golang结构体:
// 投票活动模型
type VoteActivity struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Title string `gorm:"type:varchar(255)" json:"title"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Status int8 `gorm:"type:tinyint" json:"status"`
}
// 投票记录模型
type VoteRecord struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ActivityID int64 `gorm:"index" json:"activity_id"`
OptionID int64 `json:"option_id"`
UserID int64 `gorm:"index" json:"user_id"`
VoteTime time.Time `json:"vote_time"`
}
3. 投票接口实现
核心投票逻辑需要校验活动状态、用户投票资格,同时用Redis防止重复投票:
func voteHandler(c *gin.Context) {
var req struct {
ActivityID int64 `json:"activity_id" binding:"required"`
OptionID int64 `json:"option_id" binding:"required"`
UserID int64 `json:"user_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"code": 400, "msg": "参数错误"})
return
}
// 1. 校验活动是否存在且处于有效状态
var activity VoteActivity
if err := DB.First(&activity, req.ActivityID).Error; err != nil {
c.JSON(404, gin.H{"code": 404, "msg": "投票活动不存在"})
return
}
now := time.Now()
if activity.Status != 1 || now.Before(activity.StartTime) || now.After(activity.EndTime) {
c.JSON(400, gin.H{"code": 400, "msg": "活动未开启或已结束"})
return
}
// 2. 用Redis校验用户是否已投票,key格式:vote:activity_id:user_id
voteKey := fmt.Sprintf("vote:%d:%d", req.ActivityID, req.UserID)
exists, err := RedisClient.Exists(Ctx, voteKey).Result()
if err != nil {
c.JSON(500, gin.H{"code": 500, "msg": "系统错误"})
return
}
if exists > 0 {
c.JSON(400, gin.H{"code": 400, "msg": "您已参与过本次投票"})
return
}
// 3. 开启事务写入投票记录
tx := DB.Begin()
record := VoteRecord{
ActivityID: req.ActivityID,
OptionID: req.OptionID,
UserID: req.UserID,
VoteTime: now,
}
if err := tx.Create(&record).Error; err != nil {
tx.Rollback()
c.JSON(500, gin.H{"code": 500, "msg": "投票失败"})
return
}
tx.Commit()
// 4. Redis设置投票标识,过期时间设为活动结束时间
expire := activity.EndTime.Sub(now)
RedisClient.Set(Ctx, voteKey, 1, expire)
c.JSON(200, gin.H{"code": 200, "msg": "投票成功"})
}
4. 查询投票结果接口
统计每个选项的投票数:
func getVoteResult(c *gin.Context) {
activityID := c.Query("activity_id")
if activityID == "" {
c.JSON(400, gin.H{"code": 400, "msg": "缺少活动ID"})
return
}
// 统计各选项投票数
type Result struct {
OptionID int64 `json:"option_id"`
Count int64 `json:"count"`
}
var results []Result
DB.Model(&VoteRecord{}).
Select("option_id, count(*) as count").
Where("activity_id = ?", activityID).
Group("option_id").
Scan(&results)
c.JSON(200, gin.H{"code": 200, "msg": "查询成功", "data": results})
}
5. 路由注册
将接口注册到Gin路由:
func main() {
// 初始化连接
initMySQL()
initRedis()
// 初始化路由
r := gin.Default()
api := r.Group("/api/vote")
{
api.POST("/submit", voteHandler)
api.GET("/result", getVoteResult)
}
// 启动服务
if err := r.Run(":8080"); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
五、优化与注意事项
- 防刷优化:除了用户ID校验,还可以增加IP维度的投票限制,避免同一IP批量刷票
- 性能优化:投票结果可以定时缓存到Redis,减少MySQL查询压力
- 数据一致性:高并发场景下可以用Redis分布式锁保证投票记录的写入一致性
- 参数校验:实际项目中需要增加更完善的参数校验,比如选项ID是否属于当前活动
六、总结
本文完整实现了基于Golang的投票系统核心功能,涵盖了从设计到开发的全部流程。开发者可以根据实际业务需求扩展功能,比如增加投票选项管理、活动权限控制、投票结果导出等模块。Golang的高并发特性可以很好地支撑投票系统的高流量场景,搭配Gin、MySQL、Redis的组合可以兼顾开发效率和系统稳定性。