目录
前言:
开发环境:SQL Server 2022,Qt Creator 4.11.1 (Community)
源码在文章末尾
结果预览:
屏幕录制 2025-05-20 183909
一,系统需求分析
1,需求概述
(1)应用系统背景
随着互联网技术的发展,传统线下投票方式(如纸质投票、会议举手表决等)逐渐暴露出效率低、成本高、统计复杂、安全性差等问题。网上投票管理系统旨在通过数字化手段实现投票流程的自动化,支持多场景投票需求(如企业决策、校园活动、社会调研等),提供高效、安全、透明的投票服务。
系统目标:
提高投票效率和准确性,减少人工统计错误;
支持匿名/实名投票、单选/多选、限时投票等多种模式;
保障数据安全性,防止刷票、篡改等恶意行为;
提供友好的用户界面和实时结果可视化功能。
(2)组织结构情况
普通用户:参与投票、查看投票结果。
管理员:创建/管理投票活动、审核用户身份、设置投票规则。
审核员:对敏感投票内容进行人工审核。
系统管理员:维护系统安全与数据库。
(3)需求分析过程
1,需求收集方法:
问卷调查:面向企业、学校等潜在用户,收集投票场景需求。
竞品分析:研究现有投票系统(如腾讯投票、问卷星)的功能优缺点。
专家访谈:与信息安全专家讨论防刷票、数据加密方案。
2,核心需求整理
功能性需求:
用户注册/登录(支持手机号、邮箱、第三方登录);
投票创建(设置标题、选项、截止时间、投票规则);
投票参与(匿名/实名模式、防重复提交);
结果统计(实时图表展示、数据导出为Excel/PDF);
审核机制(敏感词过滤、人工审核流程)。
非功能性需求:
性能:支持1000人同时在线投票,响应时间<2秒;
安全性:HTTPS传输、数据库加密、IP限制防刷票;
兼容性:适配PC、手机浏览器及微信小程序。
2,系统功能图
3,业务流程图
业务流程分析
1,用户注册与身份验证流程
- 新用户通过邮箱/手机号注册,系统验证唯一性后创建账户。
- 管理员可对用户信息进行审核。
2,投票创建与发布流程
- 管理员填写投票基本信息(标题,选项,截止时间等等)
- 审核通过后 ,投票进入“已发布”状态,用户可见。
3,用户参与投票流程
- 用户选择投票活动,根据规则(实名/匿名)提交选择。
- 系统校验用户权限。
- 投票实时记录,并写入数据库。
4,投票处理流程
- 投票时间到后,系统自动关闭并锁定数据。
- 支持查看投票结果。
业务流程图


4,数据流程图

5,数据字典
1,用户表(User)
表说明:
存储系统用户信息,支持角色权限管理。用户名和邮箱需唯一,密码加密存储。
字段名 | 数据类型 | 长度 | 是否允许为空 | 默认值 | 键/索引 | 说明 |
UserID | INT | 11 | 否 | 无 | 主键 | 用户唯一标识,自增 |
Username | NVARCHAR | 50 | 否 | 无 | 唯一索引 | 用户名 |
Password | NVARCHAR | 128 | 否 | 无 | 无 | 密码 |
NVARCHAR | 100 | 否 | 无 | 无 | 用户邮箱 | |
Role | NVARCHAR | 10 | 否 | 'user' | 索引 | 用户角色 |
CreatedAt | DATETIME | - | 否 | GETDATE() | 无 | 用户注册时间 |
2,投票主表(Vote)
表说明:
存储投票活动的基本信息,通过外键关联创建者。状态字段控制投票生命周期。
字段名 | 数据类型 | 长度 | 是否允许为空 | 默认值 | 键/索引 | 说明 |
VoteID | INT | 11 | 否 | 无 | 主键 | 投票唯一标识,自增 |
Title | NAVRCHAR | 100 | 否 | 无 | 无 | 投票标题 |
CreatorID | INT | 11 | 否 | 无 | 外键User | 投票创建者 ID |
IsAnonymous | BIT | 否 | 0 | 无 | 是否匿名投票:0(实名),1(匿名) | |
MaxChoices | INT | 11 | 否 | 1 | 无 | 用户最多可选选项数(单选/多选) |
EndTime | DaATETIME | 否 | 索引 | 投票结束时间 | ||
Status | NVARCHAR | 10 | 否 | 'draft' | 联合索引 | 状态:draft(草稿),published(已发布),closed(已关闭) |
3,投票选项表(VoteOption)
表说明:
存储投票的选项信息,每个投票可包含多个选项,通过外键关联投票主表。
字段名 | 数据类型 | 长度 | 是否允许为空 | 默认值 | 键/索引 | 说明 |
OptionID | INT | 11 | 否 | 主键 | 选项唯一标识,自增 | |
VoteID | INT | 11 | 否 | 外键->Vote | 关联的投票ID | |
OptionText | NVARCHAR | 200 | 否 | 选项文本内容 |
4,投票记录表(VoteRecord)
表说明:
记录用户投票行为,匿名投票时UserID为NULL。通过唯一约束(UserID+VoteID)防止重复投票。
字段名 | 数据类型 | 长度 | 是否允许为空 | 默认值 | 键/索引 | 说明 |
RecordID | INT | 11 | 否 | 主键 | 投票记录唯一标识,自增 | |
VoteID | INT | 11 | 否 | 外键->Vote | 关联投票ID | |
UserID | INT | 11 | 是 | NULL | 外键->User | 投票用户ID,匿名时为NULL |
OptionID | INT | 11 | 否 | 外键->VoteOption | 用户选择的选项ID | |
VoteTime | DATETIME | 否 | GETDATE() | 索引 | 投票提交时间 | |
IPAddress | NVARCHAR | 45 | 否 | 用户投票时的投票IP |
5,审核日志表 (AuditLog)
表说明:
记录投票审核的操作日志,确保投票内容的合规性。
字段名 | 数据类型 | 长度 | 是否允许为空 | 默认值 | 键/索引 | 说明 |
LogID | INT | 11 | 否 | 主键 | 唯一标识 ,自增 | |
VoteID | INT | 11 | 否 | 外键->Vote | 被审核的投票ID | |
AuditorLog | INT | 11 | 否 | 外键->User | 审核员用户ID | |
Action | NVARCHAR | 10 | 否 | 审核操作:通过,驳回 | ||
Comment | NVARCHAR(MAX) | 是 | NULL | 审核意见(文本) | ||
AuditTime | DATETIME | 否 | GETTIME() | 审核时间 |
二,概念结构设计
数据库概念结构设计是指将系统需求转化为一个抽象的信息结构,即概念模型。
1,实体分析
实体识别,核心实体:
User(用户),Vote(投票),VoteOpston(投票选项),VoteRecord(投票记录),AyditLog(审核日志)
2,属性分析
实体特征:
为每个实体定义唯一标识符(主键)和描述性属性。
为每个实体定义唯一标识(主键约束),然后根据业务需求确立其他字段。(如非空、唯一性、默认值)。
User 实体:
UserID
(主键)、Username
、Password
、Role
。Vote 实体:
VoteID
(主键)、Title
、EndTime
、Status
。
3,联系分析
根据业务流程分析实体间交互(如用户创建投票、用户选择选项)。
确定关系基数(1:1、1:N、M:N)。
实体与实体之间的关系:
确定关系基数,用户:投票=1:n(一个用户可以创建多个投票活动)
投票:选项=1:n(一个投票包含多个选项)等等。
实体关系模型:E-R图如下
4,概念模型分析(CDM图)
三,逻辑结构设计
数据库的逻辑设计的任务是把概念设计阶段设计好的基本E-R图转换为与选用的具体机器上的DBMS所支持的数据模型相符合的逻辑结构。根据ER图 写出关系模式,并标出主键外键。
1,实体到表的转换
每个实体映射为一张表,属性转换为表的字段。关系模型的基本映射规则(实体-表,属性-列)。示例:User实体->User表;Vote实体->Vote表。
2,关系处理
1:n关系:通过外键在“多”端表中引用“一”端表中的主键。
3,主键与唯一键约束
主键(Primary Key):每个表定义唯一标识符。如自增(auto increment)
唯一性约束:防止数据重复,如用户名唯一。
逻辑结构模型
1,用户表(User)
字段名 | 数据类型 | 约束 | 说明 |
UserID | INT | PRIMARY KEY IDENTITY | 用户唯一标识 |
Username | NVARCHAR(50) | UNIQUE NOT NULL | 用户名(唯一) |
Password | NVARCHAR(128) | NOT NULL | 密码(加密) |
NVARCHAR(100) | NOT NULL | 用户邮箱 | |
Role | NVARCHAR(10) | CHECK (Role IN ('user','admin','auditor')) DEFAULT 'user' | 用户角色 |
CreatorAt | DATETIME | DEFAULT GETDATE() | 注册时间 |
2,投票主表(Vote)
字段名 | 数据类型 | 约束 | 说明 |
VoteID | INT | PRIMARY KEY IDENTITY | 投票唯一 |
Title | NVARCHAR(100) | NOT NULL | 投票标题 |
CreatorID | INT | FOREIGN KEY REFERENCES User(UserID) | 创建者ID |
IsAnonymous | BIT | DEFAULT 0 | 0实名, 1匿名 |
MaxChoices | INT | DEFAULT 1 | 最多可选数 |
Endtime | DATETIME | NOT NULL | 截止时间 |
Status | NVARCHAR(10) | CHECK (Status IN ('draft','published','closed')) DEFAULT 'draft' | 投票状态 |
3,投票选项表(VoteOption)
字段名 | 数据类型 | 约束 | 说明 |
OptionID | INT | PRIMARY KEY IDENTITY | 选项唯一标识 |
VoteID | INT | FOREIGN KEY REFERENCES Vote(VoteID) | 所属投票ID |
OptionText | NVARCHAR(200) | NOT NULL | 选项文本内容 |
4,投票记录表(OptionRecord)
字段名 | 数据类型 | 约束 | 说明 |
RecordID | INT | PRIMARY KEY INENTITY | 记录唯一标识 |
VoteID | INT | FOREIGN KEY REFERENCES Vote(VoteID) | 关联投票ID |
UserID | INT | FOREIGN KEY REFERENCES User(UserID) | 投票用户ID |
OptionID | INT | FOREIGN KEY REFERENCES VoteOption(OptionID) | 用户选择的选项ID |
VoteTime | DATETIME | DEFAULT GETDATE() | 投票时间 |
IPAddress | NVARCHAR(45) | NOT NULL | 用户IP地址 |
5,审核日志表 (AuditLog)
字段名 | 数据类型 | 约束 | 说明 |
LogID | INT | PRIMARY KEY IDENTITY | 日志唯一标识 |
VoteID | INT | FOREIGN KEY REFERENCES Vote(VoteID) | 被审核的投票ID |
AuditorID | INT | FOREIGN KEY REFERENCES User(UserID) | 审核员ID |
Action | NVARCHAR(10) | CHECK (Action IN ('approve','reject')) | 审核操作 |
Comment | NVARCHAR(200) | NULL | 审核意见 |
AuditTime | DATETIME | DEFAULT GETDATE() | 审核时间 |
逻辑结构图 (LDM)
四,物理结构设计
1,表设计
存储优化:
使用NVARCHAR存储多语言文本(如中文,符号等等)。
自增主键IEDNTITY(1,1)替身插入性能。
完整性约束:
级联删除:删除用户时自动清理其生成的投票(ON DELETE CASCADE)
索引策略:
高频查询字段:Vote表中的Status和EndTime联合索引,加速查询。
时间范围查询:VoteRecord表中的VoteTime创建索引。
安全设计:
密码字段使用SHA2_256(哈希算法)。
匿名投票时,强制IPAddress非空。
2,建表sql语句
注:下面 的建表语句中,有的表的名称 加上了[ ],目的是避免你创建的表,或查询与一些SQL SERVER自带的关键字发生冲突。
比如下面创建了一个的User表,而系统中也包含这一张表,所以需要加上[ ],避免冲突。
-- 1. 用户表
CREATE TABLE [User](
UserID INT IDENTITY(1,1) PRIMARY KEY,
Username NVARCHAR(50) NOT NULL UNIQUE,
Password NVARCHAR(128) NOT NULL,
Email NVARCHAR(100),
Role NVARCHAR(10)
CHECK (Role IN ('user', 'admin', 'auditor')) DEFAULT 'user',
CreatedAt DATETIME NOT NULL DEFAULT GETDATE()
);
GO
-- 2. 投票主表
CREATE TABLE Vote (
VoteID INT IDENTITY(1,1) PRIMARY KEY,
Title NVARCHAR(100) NOT NULL,
CreatorID INT NOT NULL
FOREIGN KEY REFERENCES [User](UserID) ON DELETE CASCADE,
IsAnonymous BIT NOT NULL DEFAULT 0,
MaxChoices INT NOT NULL DEFAULT 1
CHECK (MaxChoices > 0),
EndTime DATETIME NOT NULL,
);
GO
-- 3. 投票选项表
CREATE TABLE VoteOption (
OptionID INT IDENTITY(1,1) PRIMARY KEY,
VoteID INT NOT NULL
FOREIGN KEY REFERENCES Vote(VoteID) ON DELETE CASCADE,
OptionText NVARCHAR(200) NOT NULL
);
GO
-- 4. 投票记录表
CREATE TABLE VoteRecord (
RecordID INT IDENTITY(1,1) PRIMARY KEY,
VoteID INT NOT NULL
FOREIGN KEY REFERENCES Vote(VoteID) ON DELETE NO ACTION ,
UserID INT NULL
FOREIGN KEY REFERENCES [User](UserID) ON DELETE SET NULL,
OptionID INT NOT NULL
FOREIGN KEY REFERENCES VoteOption(OptionID) ON DELETE NO ACTION ,
VoteTime DATETIME NOT NULL DEFAULT GETDATE(),
IPAddress NVARCHAR(45) NOT NULL,
-- 唯一约束防止重复投票(匿名投票需程序补充IP校验)
CONSTRAINT UK_User_Vote UNIQUE (UserID, VoteID)
);
GO
-- 5. 审核日志表
CREATE TABLE AuditLog (
LogID INT IDENTITY(1,1) PRIMARY KEY,
VoteID INT NOT NULL
FOREIGN KEY REFERENCES Vote(VoteID) ON DELETE CASCADE,
AuditorID INT NOT NULL
FOREIGN KEY REFERENCES [User](UserID),
Action NVARCHAR(10) NOT NULL
CHECK (Action IN ('approve', 'reject')),
Comment NVARCHAR(MAX) NULL,
AuditTime DATETIME NOT NULL DEFAULT GETDATE()
);
GO
-- 6. 索引设计
CREATE INDEX idx_User_Role ON [User] (Role);
CREATE INDEX idx_Vote_Status_EndTime ON Vote (Status, EndTime);
CREATE INDEX idx_VoteRecord_VoteTime ON VoteRecord (VoteTime);
CREATE INDEX idx_AuditLog_AuditTime ON AuditLog (AuditTime);
3,物理结构(PDM图)
五,数据库功能实现及界面展示
1,确定数据库需要创建的对象
视图
视图(views):简化复杂查询,如统计投票结果,显示待审核的投票。
投票结果视图:展示已发布投票的每个选项得票数
CREATE VIEW vw_VoteResults AS
SELECT
v.VoteID,
v.Title,
vo.OptionID,
vo.OptionText,
COUNT(vr.OptionID) AS VoteCount
FROM Vote v
JOIN VoteOption vo ON v.VoteID = vo.VoteID
LEFT JOIN VoteRecord vr ON vo.OptionID = vr.OptionID
GROUP BY v.VoteID, v.Title, vo.OptionID, vo.OptionText;
待审核投票视图:列出所有状态为"草稿"的 视图,包含创建者及截止时间。
CREATE VIEW vw_PendingAudits AS
SELECT
v.VoteID,
v.Title,
u.Username AS Creator,
v.CreatedAt,
v.EndTime
FROM Vote v
JOIN [User] u ON v.CreatorID = u.UserID
WHERE v.Status = 'draft';
用户投票记录视图:仅显示实名投票记录,包含用户,投票及选项信息。
CREATE VIEW vw_UserVoteHistory AS
SELECT
u.UserID,
u.Username,
v.VoteID,
v.Title,
vo.OptionText AS VotedOption,
vr.VoteTime
FROM VoteRecord vr
JOIN [User] u ON vr.UserID = u.UserID
JOIN Vote v ON vr.VoteID = v.VoteID
JOIN VoteOption vo ON vr.OptionID = vo.OptionID
WHERE v.IsAnonymous = 0;
删除投票视图
--CREATE PROCEDURE sp_DeleteVote
-- @VoteID INT,
-- @RequesterUserID INT
--AS
--BEGIN
-- BEGIN TRY
-- BEGIN TRANSACTION
-- -- 验证权限:仅管理员或创建者可删除
-- IF NOT EXISTS (
-- SELECT 1
-- FROM Vote
-- WHERE VoteID = @VoteID
-- AND (
-- CreatorID = @RequesterUserID
-- OR EXISTS (
-- SELECT 1
-- FROM [User]
-- WHERE UserID = @RequesterUserID
-- AND Role = 'admin'
-- )
-- )
-- )
-- BEGIN
-- RAISERROR('无权删除该投票', 16, 1)
-- RETURN
-- END
-- -- 执行删除(触发器处理级联删除)
-- DELETE FROM Vote
-- WHERE VoteID = @VoteID
-- COMMIT TRANSACTION
-- END TRY
-- BEGIN CATCH
-- IF @@TRANCOUNT > 0
-- ROLLBACK TRANSACTION;
-- THROW; -- 抛出原始错误
-- END CATCH
--END
自定义函数
自定义函数::用于业务逻辑,如检查用户是否已投票、计算选项得票数。
检查用户是否已投票(防止重复投票)
CREATE FUNCTION dbo.fn_HasUserVoted (
@UserID INT,
@VoteID INT
)
RETURNS BIT
AS
BEGIN
DECLARE @Result BIT = 0;
IF EXISTS (
SELECT 1
FROM VoteRecord
WHERE UserID = @UserID AND VoteID = @VoteID
)
SET @Result = 1;
RETURN @Result;
END;
存储过程
存储过程:封装业务操作,如创建投票、提交投票、审核投票。
创建投票:
CREATE PROCEDURE sp_CreateVote
@Title NVARCHAR(100),
@CreatorID INT,
@IsAnonymous BIT,
@MaxChoices INT,
@EndTime DATETIME,
@Options NVARCHAR(MAX) -- JSON格式: ["Option1", "Option2"]
AS
BEGIN
BEGIN TRANSACTION;
-- 插入投票主表
INSERT INTO Vote (Title, CreatorID, IsAnonymous, MaxChoices, EndTime)
VALUES (@Title, @CreatorID, @IsAnonymous, @MaxChoices, @EndTime);
DECLARE @VoteID INT = SCOPE_IDENTITY();
-- 解析并插入选项
INSERT INTO VoteOption (VoteID, OptionText)
SELECT @VoteID, [value]
FROM OPENJSON(@Options);
-- 自动触发敏感词检测
IF EXISTS (
SELECT 1
FROM VoteOption
WHERE VoteID = @VoteID
AND dbo.fn_CheckSensitiveWords(OptionText) = 1
)
BEGIN
-- 存在敏感词,状态设为待审核
UPDATE Vote SET Status = 'draft' WHERE VoteID = @VoteID;
END
ELSE
BEGIN
-- 无敏感词,直接发布
UPDATE Vote SET Status = 'published' WHERE VoteID = @VoteID;
END
COMMIT TRANSACTION;
END;
用户提交投票:
CREATE PROCEDURE sp_SubmitVote
@VoteID INT,
@UserID INT, -- 匿名时为NULL
@OptionIDs NVARCHAR(MAX), -- JSON格式: [1, 2]
@IPAddress NVARCHAR(45)
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
-- 检查投票是否有效
IF NOT EXISTS (
SELECT 1
FROM Vote
WHERE VoteID = @VoteID
AND Status = 'published'
AND EndTime > GETDATE()
)
RAISERROR('投票不存在或已截止!', 16, 1);
-- 检查选项数量
DECLARE @OptionCount INT = (
SELECT COUNT(*)
FROM OPENJSON(@OptionIDs)
);
IF @OptionCount > (SELECT MaxChoices FROM Vote WHERE VoteID = @VoteID)
RAISERROR('超出最大可选数量!', 16, 1);
-- 实名投票检查重复
IF @UserID IS NOT NULL AND dbo.fn_HasUserVoted(@UserID, @VoteID) = 1
RAISERROR('您已参与过本次投票!', 16, 1);
-- 匿名投票检查IP限制
IF @UserID IS NULL AND EXISTS (
SELECT 1
FROM VoteRecord
WHERE VoteID = @VoteID
AND IPAddress = @IPAddress
AND VoteTime >= DATEADD(HOUR, -24, GETDATE())
)
RAISERROR('同一IP24小时内仅能投票一次!', 16, 1);
-- 插入投票记录
INSERT INTO VoteRecord (VoteID, UserID, OptionID, IPAddress)
SELECT @VoteID, @UserID, [value], @IPAddress
FROM OPENJSON(@OptionIDs);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
THROW;
END CATCH
END;
触发器
触发器:触发器是一种特殊的存储过程,它会在特定的数据库事件(如插入,删除,更新等等)发生时自动执行。触发器与表直接关联,用于在数据变动时,完成某些业务逻辑,从而保证数据的一致性,完整性和自动化处理。
自动更新选项的票数:每次插入投票记录时,对应选项的 VoteCount字段自动+1
CREATE TRIGGER trg_UpdateVoteCount
ON VoteRecord
AFTER INSERT
AS
BEGIN
UPDATE vo
SET vo.VoteCount = vo.VoteCount + 1
FROM VoteOption vo
JOIN inserted i ON vo.OptionID = i.OptionID;
END;
防止修改已结束的投票:拦截对已截止投票的修改操作,确保数据不可变性。
CREATE TRIGGER trg_PreventModifyClosedVote
ON Vote
INSTEAD OF UPDATE
AS
BEGIN
IF UPDATE(EndTime) OR UPDATE(Status)
BEGIN
IF EXISTS (
SELECT 1
FROM inserted i
JOIN deleted d ON i.VoteID = d.VoteID
WHERE d.EndTime < GETDATE()
)
RAISERROR('投票已截止,禁止修改!', 16, 1);
ELSE
UPDATE v
SET
Title = i.Title,
EndTime = i.EndTime,
Status = i.Status
FROM Vote v
JOIN inserted i ON v.VoteID = i.VoteID;
END
END;
2,界面布局及实现
程序开始界面
登录/注册界面
主页面
遇到的问题:
在进行新建投票的操作过程中,出现userID冲突的过程。找了半天是在用户登录注册代码部分,获取userID时,由于通过map查找userID时,字符串大小写搞错了,一直获取的是同一个userID。
还有就是对于选项表,起初的想法是按照数组的特点,每个选项对应一个下标,运行后才发现这样做使得OptionID发生冲突,也就是发生主键冲突。解决办法是将每个选项的文本和它的选项ID拼接在一起,获取选项ID的时候,直接取选项的第一个字符然后转为整数(先这么做,这里看到会存在问题的)。
源码:
网上投票系统: 这是一个数据库课程设计,关于网上投票管理系统的,使用到的技术QT,使用的数据库SQL-Server - Gitee.com