简介这是一套采用浏览器/服务器架构的网络投票系统源码基于ASP动态页面技术实现面向Web开发学习者、毕业生以及需要快速搭建内部投票活动的小型团队。系统后台功能完整覆盖投票主题创建与编辑、选项增删改、配置参数调整等管理链路并提供登录与密码验证的用户认证机制能够区分管理员与普通用户的操作权限有效保障投票数据的安全与流程规范。压缩包共56个文件以26个asp核心页面为功能主体配合gif、jpg等界面素材、txt说明文档以及数据库文件和安装脚本整体仅116KB属于轻量级、可快速部署的完整工程。已有197人学习下载适合作为学习浏览器/服务器架构、ASP后端开发及后台管理系统设计的入门实例也方便修改后直接用于课程设计或内部投票场景。1. B/S方式网络投票系统浏览器就是投票终端B/S方式网络投票系统说穿了就是一套跑在服务器上的网站用户拿浏览器打开链接或者拿手机扫一下二维码就能看到候选人列表、完成投票、查看结果。终端不需要装任何软件服务器端改一处逻辑所有用户下一次访问立刻生效。真正做起来才发现这类系统最容易翻车的地方不在页面而在“怎么保证一个人只能投一次”。前端按钮禁用、刷新限制这些手段在一个愿意写几行脚本的人面前基本等于没有。B/S网络投票擅长解决的是企业内部评优、满意度调查、课程评教、集中表决这类场景参与者会在短时间内集中涌入投票时段有明确的开始和结束结果要可核对又要能挡住明显的刷票行为。适合的读者是后端或全栈工程师打算在有限时间内交付一个可靠的投票功能或者正在几个方案之间做选型判断。2. B/S架构选型与数据模型投票要禁得住并发先得立住表结构2.1 为什么B/S成了网络投票的默认形态早期投票系统常做成C/S结构用户需要安装客户端服务器升级一次所有终端都要跟着更新。放在内部办公网还能忍受放到公网场景几乎是灾难。后来出现的另一种方案是做成小程序或App体验不错但要过平台审核、要发版本投票活动往往就开放几个小时等审核通过活动都结束了。B/S方式把逻辑和界面全部集中在服务器端浏览器只负责渲染和提交。参与者的门槛被降到最低有浏览器就能投不需要安装不需要考虑操作系统是Windows还是macOS手机、平板、电脑都一样。活动方的收益也很直接投票时间、候选选项、统计口径都在自己手里想改随时改不用等发版。对短时效、高瞬时并发的“活动型系统”来说这种集中控制的价值比什么都重要。代价同样明显服务器要扛住所有并发且必须自己面对刷票问题。因为浏览器不像原生App那样能拿到强设备标识服务器光靠IP、Cookie这类信息很难完美区分“一个真人”和“一个脚本”。所以这类系统的核心功夫不在前端而在后端的数据约束和频控策略上。2.2 核心数据模型场次、候选人与投票记录三张核心表我刚接触这类需求时第一版只建了两张表一张存候选人一张存票数。结果发现活动一旦分时段、分场次立刻改表想查谁投了谁完全无据可查。后来形成的稳定做法是拆成三张表场次表、候选人表、投票记录表。表名用途关键约束说明vote_session一次投票活动start_time / end_time一个活动就是一个场次控制开放时间candidate场次下的候选人或选项session_id 外键一场活动可有多个候选人vote_record用户每次投票的真实记录(session_id, voter_key) 唯一索引防重票的核心也是后续对账的依据这里面最容易被忽略的是 vote_record 的组合唯一索引。如果业务定义是“一个用户在一个场次里只能投一票”那唯一键就应该建在 session_id 和 voter_key 上。voter_key 可以是登录用户的ID也可以是匿名用户生成的Token。如果业务改成“一个人可选多名候选人”那唯一键要变成 (session_id, voter_key, candidate_id)意思是每个人对每个候选人最多一票。这个决定必须在建表时定下来后期靠代码去查重是不靠谱的。2.3 技术栈与身份识别先决定“一票”怎么定义技术栈选择上我用过前后端分离的方案也用过服务端渲染的方案。小规模投票系统完全没必要上前后端分离和跨域配置选一个后端框架加模板渲染把页面直接交付部署一个进程就能跑维护成本最低。常见做法是Java生态里的Spring Boot配Thymeleaf数据库用MySQL再加一个缓存中间件做频控。没有缓存中间件也能跑只是同一IP刷票时只能靠查数据库来限制压力会大不少。身份识别粒度是整个系统的地基建议在动手前先回答清楚三个问题。第一投票是记名还是匿名记名直接拿用户ID当voter_key匿名则需要生成随机Token放在Cookie里。第二活动有没有登录要求没登录的话voter_key要足够随机避免被人猜出来伪造。第三允许一个人投几票这直接决定唯一索引怎么建。这三个问题想清楚了表结构基本就定了后面改起来成本很高。3. 从零手写投票核心链路初始化库表、投票接口与结果页3.1 初始化数据库建表SQL与唯一索引这一节先落地数据库。以下SQL是投票系统的最小可用结构包含场次、候选人和投票记录三张表并建好了防重票的组合唯一索引。CREATE DATABASE vote_demo DEFAULT CHARACTER SET utf8mb4; USE vote_demo; CREATE TABLE vote_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 投票场次ID, title VARCHAR(200) NOT NULL COMMENT 场次名称, start_time DATETIME NOT NULL COMMENT 开始时间, end_time DATETIME NOT NULL COMMENT 结束时间, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用 ) COMMENT投票场次; CREATE TABLE candidate ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL COMMENT 所属场次, name VARCHAR(100) NOT NULL COMMENT 候选人/选项名称, sort_order INT DEFAULT 0 COMMENT 展示顺序, vote_count INT NOT NULL DEFAULT 0 COMMENT 冗余计数字段 ) COMMENT候选选项; CREATE INDEX idx_candidate_session ON candidate(session_id); CREATE TABLE vote_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL COMMENT 场次ID, candidate_id BIGINT NOT NULL COMMENT 投给哪个候选人, voter_key VARCHAR(64) NOT NULL COMMENT 投票人标识用户ID或匿名Token, voter_ip VARCHAR(45) DEFAULT NULL COMMENT 来源IP仅供参考, vote_time DATETIME NOT NULL COMMENT 投票时间, UNIQUE KEY uk_session_voter (session_id, voter_key) ) COMMENT投票记录;这个SQL里的几个细节值得说明。首先字符集用utf8mb4而不是utf8因为候选人姓名、场次标题里都可能出现特殊字符或表情符号utf8mb4才能完整存下。其次voter_key设计成VARCHAR(64)无论放用户ID还是UUID都足够。第三vote_count字段是冗余计数它存在的原因是为了结果页快速展示而不是每次都用COUNT去遍历记录表但这个字段的高并发更新策略很重要下一节会专门讲。唯一索引uk_session_voter是整个表的灵魂。它保证了同一场次下同一个voter_key只有一条记录。即使后端代码写漏了校验数据库也会在最后一道防线拦下重复插入。后面所有防重逻辑本质上都是围绕这条约束展开的。3.2 投票接口事务、唯一索引与重复投递的捕获投票接口是系统的核心路径。常见做法是Controller接收参数Service里完成校验和写入。下面这段Java代码是Service层的核心方法我按最稳妥的方式来组织。Transactional public VoteResult vote(Long sessionId, Long candidateId, String voterKey, String ip) { VoteSession session sessionMapper.selectById(sessionId); if (session null) { return VoteResult.error(投票场次不存在); } LocalDateTime now LocalDateTime.now(); if (now.isBefore(session.getStartTime()) || now.isAfter(session.getEndTime())) { return VoteResult.error(不在投票时间段内); } Candidate candidate candidateMapper.selectById(candidateId); if (candidate null || !candidate.getSessionId().equals(sessionId)) { return VoteResult.error(候选选项不存在); } VoteRecord record new VoteRecord(); record.setSessionId(sessionId); record.setCandidateId(candidateId); record.setVoterKey(voterKey); record.setVoterIp(ip); record.setVoteTime(now); try { insertRecordInNewTransaction(record); } catch (DuplicateKeyException e) { return VoteResult.error(您已经投过票不能重复投票); } candidateMapper.increaseCount(candidateId); return VoteResult.ok(); } Transactional(propagation Propagation.REQUIRES_NEW) public void insertRecordInNewTransaction(VoteRecord record) { voteRecordMapper.insert(record); }这段代码的逻辑顺序是先校验场次是否存在以及是否在投票时间内再校验候选人是否属于当前场次然后插入投票记录插入失败则捕获唯一索引冲突。时间校验用的是服务器本地时间LocalDateTime.now()不是前端传上来的时间。前端展示的倒计时只是提示最终裁决权必须在后端。insertRecordInNewTransaction方法单独开了REQUIRES_NEW事务。这样做的原因是如果直接在当前事务里执行insert并捕获DuplicateKeyExceptionSpring会把当前事务标记为rollback-only即使代码catch住了异常外层事务提交时仍会抛出UnexpectedRollbackException用户看到的会是500错误而不是“您已经投过票”。把插入放到独立事务里重复键冲突的影响范围就被限制在插入这一步外层事务可以正常继续或干净地返回业务错误。candidateMapper.increaseCount对应SQL是UPDATE candidate SET vote_count vote_count 1 WHERE id ?。这里必须用数据库原子的自增语句不能先查出vote_count再赋值回去否则并发时会出现丢失更新。插入记录和更新计数放在同一个外层事务里要么都成功要么都回滚保证数据一致。3.3 前端投票页与结果页提交与展示分离前端页面不需要多复杂一个候选人列表加一个提交按钮就够了。真正的要点是提交时把身份Token一起交给后端并在提交成功后及时刷新结果区域。下面给出一个最小可运行的示例骨架。script async function submitVote(candidateId) { const resp await fetch(/vote/submit, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({sessionId: sessionId, candidateId: candidateId}), credentials: same-origin }); const result await resp.json(); if (result.code 0) { document.getElementById(tips).innerText 投票成功; document.getElementById(submitBtn).disabled true; loadResult(); } else { document.getElementById(tips).innerText result.msg; } } async function loadResult() { const resp await fetch(/vote/result?sessionId sessionId); const data await resp.json(); // 用一个开源图表库渲染饼图或柱状图 renderChart(data); } /script这里有个容易忽略的配置fetch请求必须带上credentials: same-origin否则匿名场景下浏览器不会把用于识别身份的Cookie发送给后端后端每次拿到的都是新用户。按钮置灰只是用户体验层面的反馈防刷靠的还是后端唯一的约束和频控逻辑。接口返回统一用code和msg的JSON结构好处是前端判断逻辑非常简单也方便后续接日志和告警。3.4 从单选扩展多选和加权改一处唯一索引和统计口径很多投票活动不是简单的单选比如优秀员工可以选三人或者允许给每个候选人打分。这类扩展不需要推翻表结构只需要调整约束和统计口径。多选场景下唯一索引从(session_id, voter_key)改成(session_id, voter_key, candidate_id)含义就变成“每个用户对每个候选人最多一票”。统计结果时直接对vote_record做GROUP BY candidate_id取COUNT。加权的场景稍微复杂一点。常规做法是给candidate表增加一个weight字段比如讲师评分权重1.0专家评分权重2.0。统计时会用如下的SQLSELECT candidate_id, SUM(weight) AS total_score FROM vote_record r JOIN candidate c ON c.id r.candidate_id WHERE r.session_id ? GROUP BY r.candidate_id ORDER BY total_score DESC;要注意weight不能存在vote_record里合并统计。因为权重要跟着候选人走而不是跟着某张票走把weight冗余进投票记录的做法一旦活动中途调整权重历史数据全要重算很容易出事。4. 上线遇坑与排查并发、重复票与打不开的服务器4.1 前端按钮禁用拦不住刷票现象投票页面提交后按钮置灰看起来不能再投了。但有人直接对着后端接口发POST请求一次提交几十张票全部成功。原因前端所有限制都是给正常用户看的。脚本绕过页面后唯一能依赖的就是后端的校验和数据约束。解决第一vote_record上必须有组合唯一索引这是最后一道闸第二后端必须自己校验voter_key是否已存在不能把“已投票”状态只存在内存或前端变量里第三配合缓存中间件做IP和账号维度的频控。三者叠加才是可靠方案缺任何一个都存在漏洞窗口。4.2 高并发下的丢失更新票数会悄悄变少现象用压测工具模拟2000人同时投票结果页显示的总票数只有1700多但数据库里vote_record实际插入了2000条记录。原因计数更新写成了先SELECT vote_count再在内存里加1最后UPDATE。两个人同时读到了同一个旧值分别加1写回最终只增加了一次。这是典型的丢失更新问题。解决计数更新必须用数据库原子的自增语句UPDATE candidate SET vote_count vote_count 1。同时要以vote_record的聚合结果作为核对依据定期跑对账SQLSELECT candidate_id, COUNT(*) FROM vote_record WHERE session_id ? GROUP BY candidate_id;把聚合结果和candidate表的冗余计数做比较。出现不一致时以vote_record的聚合结果为准去修复count字段。4.3 Session漂移、本机时间绕过与端口不通这三个问题几乎每个投票系统上线都会遇到一次逐个说。Session漂移的现象是用户第一次请求分到了服务器A登录状态写进了A的内存第二次请求被负载均衡转发到了服务器BB没有这份Session用户又被要求重新登录。在多个实例部署时几乎是必然发生。原因就是默认Session存在本机内存。解决思路有两条要么把Session统一放到集中式缓存中所有实例共享要么放弃Session把身份信息做成签名的Token由后端验签彻底绕开“状态保存在服务器”这个问题。投票系统这种请求频率不高的场景用签名Token更干净。本机时间绕过的现象是投票场次还没到开始时间有人把自己电脑系统时间改到开始时间之后页面上的倒计时变成了已开始居然真的投成功了。原因很简单前端用的是本机时间判断。解决方法是后端所有时间判断一律使用服务器时间前端倒计时只做展示提交接口里重新校验start_time和end_time。数据库记录投票时间时也用数据库的CURRENT_TIMESTAMP保证时间源的统一。端口不通的现象是本地用localhost访问一切正常部署到服务器后用公网IP访问却一直超时。原因通常是服务绑定在了127.0.0.1上只监听本机回环地址或者是服务器防火墙没有放行对应端口。解决方法是启动时显式指定监听地址为0.0.0.0例如Spring Boot里加--server.address0.0.0.0然后检查云服务商安全组和本机防火墙的入站规则。排查顺序一般是先curl http://localhost:8080确认进程活着再curl http://服务器内网IP:8080确认监听地址正确最后用公网IP确认安全组放行。5. 进阶验证与提升并发压测、实时刷新与匿名Token投票功能写完后我习惯先用ab工具做一次小型压测再决定是否投入真实使用。ab可以模拟指定数量的请求和并发度。压测命令大致是这样ab -n 200 -c 20 -T application/json -p vote.json http://your-server/vote/submit-n是总请求数-c是并发数-p指定请求体文件。注意如果测试数据里所有请求都用同一个voter_key压测验证的是“重复投票拦截是否可靠”数据库里应该只有一条投票记录。如果想测系统吞吐量就得生成几百个不同的voter_key轮换使用。压测完成后用对账SQL核对vote_record和candidate.vote_count是否一致这是上线前最后一道检查。结果页实时更新这块小型系统直接用轮询就够了每5秒请求一次结果接口实现简单且没有额外基础设施。实时性要求高的话可以改成SSE推送服务器主动往浏览器推票数变化。匿名投票的安全性还可以再进一步生成Token时做签名用密钥对voter_key加时间戳做HMAC防止用户自己篡改Cookie伪造身份。我做这类系统的习惯是先拿一个小范围的真实投票试跑一轮哪怕只有五十人参加。试跑能暴露出时间配置、统计展示、移动端适配这些测试数据发现不了的问题。没有一次绕过这个步骤能直接上大场次投票系统上线容易出了事想给参与者和活动方解释清楚很难。希望帮到你。本文还有配套的精品资源点击获取