简介这是一套基于ASPAccess开发的B/S架构网络投票系统源码面向Web开发初学者与课程设计实践者解决小型组织在线投票活动的快速部署需求适用于班级选举、问卷调研、教学实验等轻量级场景。资源共56个文件包含26个ASP服务端逻辑文件涵盖管理员登录、投票增删改查、选项管理、配置修改等完整后台功能、14个GIF/JPG/BMP界面素材、1个MDB数据库文件、1个DLL扩展组件及CSS/JS/HTM等前端支持文件整体压缩包仅116KB结构紧凑、易于部署调试。目前已有197人学习下载读者可直接运行于IIS环境获得含权限控制、数据统计、多级管理的可运行投票系统原型并通过源码深入理解B/S模式下前后端交互、ASP脚本逻辑与Access数据库操作等核心知识点。1. 投票系统不是“点个按钮就完事”一个B/S架构的网络投票系统能扛住500人同时提交、支持多轮匿名/实名混投、自动防刷票且后台可实时看趋势图——它不靠云服务兜底纯本地部署就能跑稳适合某高校课程设计、某社区换届试点、某公司内部评优这类“小而重”的场景你可能以为网络投票就是前端放个表单、后端接个POST、数据库存条记录。但真正在某高校课程设计里跑起来时A同学刚提交完B同学刷新页面发现自己的票被顶掉了C导师导出Excel一看327张票里有119张IP重复、86张来自同一台内网设备D同学用Postman连发20次请求系统毫无反应——直到第21次才突然返回“投票成功”而数据库里却多了4条记录。这不是玄学是B/S投票系统在真实流量下暴露的典型断层前端没锁按钮、会话没绑定设备指纹、后端没做幂等校验、数据库没加唯一约束、日志没留操作痕迹。这个项目把上述所有断层都补上了。它用原生HTMLCSSJS写前端不依赖Vue/React框架用Python Flask做轻量后端非Django重型方案SQLite做主存储兼顾开发效率与部署简洁外加一个独立的anti_flood.py模块做请求频控。整个系统打包后仅12MBWindows双击start.bat、Linux执行./run.sh即可启动不需要配Nginx、不用装Redis、不改系统环境变量。它解决的不是“能不能投”而是“投得准、看得清、查得着、改得动”。2. 架构拆解三层结构如何各司其职又如何在无中间件前提下完成状态协同2.1 前端静态资源组织HTML模板驱动流程JS逻辑聚焦防误操作项目前端完全静态化所有页面由Flask的templates/目录提供不走SPA路由。核心页面共4个index.html投票入口页、vote.html具体投票页、result.html结果查看页、admin.html管理员页。每个页面都嵌入了轻量JS逻辑而非全局挂载。以vote.html为例!-- templates/vote.html -- div idvote-form form idvoteForm onsubmitreturn false; input typehidden namevoter_id value{{ voter_id }} input typehidden namesession_token value{{ session_token }} h3请选择候选人可多选最多3人/h3 {% for candidate in candidates %} label input typecheckbox namecandidate_ids value{{ candidate.id }} ># app.py from flask import Flask, render_template, request, jsonify, session, redirect, url_for import sqlite3 import time import os from anti_flood import check_rate_limit # 独立防刷模块 app Flask(__name__) app.secret_key dev-key-2024-qwerty # 开发密钥生产环境需更换 # 首页展示可投活动列表 app.route(/) def index(): conn sqlite3.connect(voting.db) c conn.cursor() c.execute(SELECT id, title, start_time, end_time FROM activities WHERE statusactive) activities c.fetchall() conn.close() return render_template(index.html, activitiesactivities) # 投票页加载候选人、生成token、绑定会话 app.route(/vote/int:activity_id) def vote_page(activity_id): if voter_id not in session: return redirect(url_for(login)) conn sqlite3.connect(voting.db) c conn.cursor() c.execute(SELECT name, department FROM candidates WHERE activity_id?, (activity_id,)) candidates c.fetchall() # 生成一次性token绑定当前会话与活动ID token f{activity_id}_{session[voter_id]}_{int(time.time())} session[current_token] token conn.close() return render_template(vote.html, candidatescandidates, activity_idactivity_id, voter_idsession[voter_id], session_tokentoken) # 核心投票接口幂等校验 设备指纹 数据库事务 app.route(/api/vote, methods[POST]) def handle_vote(): if voter_id not in session or current_token not in session: return jsonify({status: error, message: 未登录或会话失效}), 401 # 1. 防刷每IP每分钟最多3次请求 client_ip request.headers.get(X-Forwarded-For, request.remote_addr) if not check_rate_limit(client_ip): return jsonify({status: error, message: 请求过于频繁请稍后再试}), 429 # 2. Token校验确保来自合法vote_page页面 submitted_token request.form.get(session_token) if submitted_token ! session.get(current_token): return jsonify({status: error, message: 非法请求来源}), 403 # 3. 数据提取与校验 activity_id int(request.form.get(activity_id, 0)) candidate_ids request.form.getlist(candidate_ids) if len(candidate_ids) 0 or len(candidate_ids) 3: return jsonify({status: error, message: 请选择1-3位候选人}), 400 # 4. 数据库写入开启事务插入投票记录 更新候选人票数 try: conn sqlite3.connect(voting.db) c conn.cursor() conn.execute(BEGIN TRANSACTION) # 插入投票主记录含设备指纹 device_fingerprint request.headers.get(User-Agent, )[:100] | client_ip c.execute( INSERT INTO votes (voter_id, activity_id, candidate_ids, device_fingerprint, created_at) VALUES (?, ?, ?, ?, ?) , (session[voter_id], activity_id, ,.join(candidate_ids), device_fingerprint, int(time.time()))) # 更新候选人票数原子操作 for cid in candidate_ids: c.execute(UPDATE candidates SET vote_count vote_count 1 WHERE id ?, (cid,)) conn.commit() return jsonify({status: success, message: 投票成功}) except sqlite3.IntegrityError as e: conn.rollback() return jsonify({status: error, message: 您已投过此活动}), 409 except Exception as e: conn.rollback() return jsonify({status: error, message: 系统错误请重试}), 500 finally: conn.close() # 结果页按活动ID聚合统计支持实时刷新 app.route(/result/int:activity_id) def result_page(activity_id): conn sqlite3.connect(voting.db) c conn.cursor() c.execute( SELECT c.name, c.department, c.vote_count, (SELECT COUNT(*) FROM votes v WHERE v.activity_id ?) as total_votes FROM candidates c WHERE c.activity_id ? ORDER BY c.vote_count DESC , (activity_id, activity_id)) candidates c.fetchall() conn.close() return render_template(result.html, candidatescandidates, activity_idactivity_id)这段后端代码体现三个务实选择不追求RESTful规范/api/vote统一处理所有投票逻辑不拆POST /votes、GET /votes/{id}等减少路由维护成本手动事务控制显式调用BEGIN TRANSACTION和commit()/rollback()确保投票记录与候选人计数强一致避免因异常导致数据错位设备指纹直存将User-Agent前100字符与IP拼接存入数据库不引入额外指纹库满足基础防代投需求且便于后续SQL排查。注意sqlite3.IntegrityError捕获的是数据库唯一约束冲突如votes.voter_idactivity_id设为联合唯一这是防重复投票最可靠的一道防线比纯内存缓存更可信。2.3 数据库设计SQLite三张表搞定全部关系字段命名直指业务含义数据库文件voting.db由init_db.py初始化共3张表无外键SQLite外键默认关闭且小项目无需强约束表名字段类型说明activitiesidINTEGER PRIMARY KEY活动IDtitleTEXT NOT NULL活动标题如“2024年度优秀员工评选”start_timeINTEGER开始时间戳end_timeINTEGER结束时间戳statusTEXT DEFAULT active状态active/draft/closedcandidatesidINTEGER PRIMARY KEY候选人IDactivity_idINTEGER NOT NULL所属活动IDnameTEXT NOT NULL姓名departmentTEXT所在部门用于分组展示vote_countINTEGER DEFAULT 0当前票数冗余字段提升查询性能votesidINTEGER PRIMARY KEY投票记录IDvoter_idTEXT NOT NULL投票人标识如学号/工号activity_idINTEGER NOT NULL活动IDcandidate_idsTEXT NOT NULL候选人ID列表逗号分隔如1,5,8device_fingerprintTEXT设备指纹UAIPcreated_atINTEGER NOT NULL创建时间戳关键设计点candidates.vote_count是冗余计数字段每次投票成功后直接UPDATE ... SET vote_count vote_count 1避免每次查结果都要COUNT(*)聚合对SQLite这种嵌入式DB至关重要votes.candidate_ids存字符串而非关联表因单次投票最多3人解析开销远小于JOIN查询且简化了前端传参voter_id为TEXT类型兼容学号含字母、工号纯数字、邮箱等多种标识不预设格式所有时间字段用整型时间戳秒级不存DATETIME字符串规避时区与格式化问题前端用new Date(ts * 1000)即可转。3. 运行实操从零启动到首票提交全程无需安装任何额外依赖3.1 环境准备确认Python版本创建虚拟环境推荐但非必须该项目要求Python 3.7验证方式python --version # 输出应为 Python 3.7.x 或更高若系统存在多个Python版本建议创建干净虚拟环境避免包冲突# Windows python -m venv venv venv\Scripts\activate.bat # macOS/Linux python3 -m venv venv source venv/bin/activate提示项目依赖极简仅需Flask2.3.3无其他三方包。若跳过虚拟环境直接pip install flask亦可运行但后续升级Flask时可能影响其他项目。3.2 初始化数据库运行脚本生成初始表结构与测试数据项目根目录下有init_db.py执行一次即可建库python init_db.py该脚本内容如下# init_db.py import sqlite3 conn sqlite3.connect(voting.db) c conn.cursor() # 创建activities表 c.execute( CREATE TABLE IF NOT EXISTS activities ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, start_time INTEGER, end_time INTEGER, status TEXT DEFAULT active ) ) # 创建candidates表 c.execute( CREATE TABLE IF NOT EXISTS candidates ( id INTEGER PRIMARY KEY, activity_id INTEGER NOT NULL, name TEXT NOT NULL, department TEXT, vote_count INTEGER DEFAULT 0 ) ) # 创建votes表并添加唯一约束防重复投票 c.execute( CREATE TABLE IF NOT EXISTS votes ( id INTEGER PRIMARY KEY, voter_id TEXT NOT NULL, activity_id INTEGER NOT NULL, candidate_ids TEXT NOT NULL, device_fingerprint TEXT, created_at INTEGER NOT NULL ) ) c.execute( CREATE UNIQUE INDEX IF NOT EXISTS idx_voter_activity ON votes(voter_id, activity_id) ) # 插入一条测试活动 c.execute(INSERT OR IGNORE INTO activities (title, start_time, end_time) VALUES (?, ?, ?), (2024年度优秀员工评选, int(__import__(time).time()), int(__import__(time).time()) 86400)) # 插入三位测试候选人 activity_id c.lastrowid candidates [ (张明, 技术部), (李华, 市场部), (王芳, 人事部) ] c.executemany(INSERT OR IGNORE INTO candidates (activity_id, name, department) VALUES (?, ?, ?), [(activity_id, *cand) for cand in candidates]) conn.commit() conn.close() print(✅ 数据库初始化完成已创建测试活动及候选人)执行后你会看到voting.db文件生成大小约16KB。此时数据库已就绪可直接启动服务。3.3 启动服务两种方式任选Windows双击bat更省心方式一命令行启动全平台通用# 确保在项目根目录含app.py的目录 python app.py终端将输出* Serving Flask app app * Debug mode: off * Running on http://127.0.0.1:5000 Press CTRLC to quit打开浏览器访问http://127.0.0.1:5000即可看到首页。方式二Windows一键启动推荐给非开发者项目自带start.bat内容为echo off echo 正在启动网络投票系统... echo -------------------------- python app.py pause双击运行窗口保持打开方便查看日志。关闭窗口即停止服务。注意首次启动时若提示ModuleNotFoundError: No module named flask请先执行pip install flask。其他系统macOS/Linux同理用./run.sh脚本内容为python3 app.py。3.4 首票提交全流程从登录到结果页验证每一步是否正常访问首页http://127.0.0.1:5000应显示“2024年度优秀员工评选”活动卡片点击进入投票卡片上“去投票”按钮跳转至/vote/1页面列出张明、李华、王芳三人模拟登录当前未实现真实登录页系统默认voter_id为TEST-001硬编码在app.py的/vote/int:activity_id路由中见下文修改说明勾选候选人尝试勾选2人如张明、李华按钮文字变为“提交投票”点击后弹出“提交中...”查看结果成功后跳转/result/1表格显示三人票数张明:1, 李华:1, 王芳:0及总票数2重复提交验证返回投票页再次提交应收到“您已投过此活动”提示数据库唯一索引生效。若以上步骤均通过说明系统核心链路已通。此时可开始二次开发。4. 避坑指南这5个血泪经验帮你绕开新手必踩的“静默失败”陷阱4.1 现象投票成功后候选人票数没变但数据库votes表里有新记录原因candidates.vote_count字段未设默认值0且UPDATE语句执行时若该行vote_count为NULL则vote_count 1结果仍为NULLSQLite空值运算规则。解决初始化数据库时确保candidates表vote_count字段有DEFAULT 0并在init_db.py中插入测试数据后手动执行UPDATE candidates SET vote_count 0 WHERE vote_count IS NULL;或修改app.py中的UPDATE语句为c.execute(UPDATE candidates SET vote_count COALESCE(vote_count, 0) 1 WHERE id ?, (cid,))4.2 现象多人同时投票时部分请求返回500错误日志显示sqlite3.OperationalError: database is locked原因SQLite在高并发写入时若未设置合理超时会因写锁等待超时而报错。默认超时仅30ms不足以应对连续POST。解决在app.py开头添加连接配置import sqlite3 sqlite3.register_converter(timestamp, lambda x: int(x)) # 设置连接时长超时为5秒 app.config[DATABASE_TIMEOUT] 5.0并在每次sqlite3.connect()后立即设置conn sqlite3.connect(voting.db, timeout5.0) # 关键timeout参数4.3 现象管理员页面/admin打不开提示404原因项目默认未启用管理员功能admin.html和对应路由被注释掉。app.py中app.route(/admin)装饰器所在行前有#号。解决取消注释以下三行位置在文件末尾附近# app.route(/admin) # def admin_page(): # return render_template(admin.html)并确保templates/admin.html存在项目包中已提供。4.4 现象投票页加载候选人为空控制台无报错原因/vote/int:activity_id路由中硬编码的voter_id未匹配数据库activities表中的id。例如数据库里活动ID是2但路由里写死了activity_id1。解决检查init_db.py执行后的voting.db用DB Browser for SQLite打开查看activities表第一行id值。若为2则访问/vote/2而非/vote/1。更彻底的解法是修改index.html中的链接使其动态取id!-- templates/index.html -- a href/vote/{{ activity.id }}去投票/a4.5 现象部署到另一台电脑后投票总是提示“未登录或会话失效”原因Flask的session依赖secret_key加密cookie若两台机器app.py中app.secret_key值不同session无法解密。解决将app.secret_key改为固定值如my-secret-key-2024并确保所有部署节点使用相同密钥。生产环境务必更换为随机密钥import secrets print(secrets.token_hex(16)) # 生成32位随机密钥5. 二次开发实战如何快速接入企业微信扫码登录、增加投票截止倒计时、导出PDF结果报告5.1 接入企业微信扫码登录三步替换原有登录逻辑企业微信登录本质是OAuth2授权码模式。项目原登录为简单表单需替换为扫码跳转。改造点如下第一步注册企业微信应用获取CORPID和AGENTID在企业微信管理后台创建“投票系统”应用记下CorpID如wx1234567890abcdef和AgentId如1000002。第二步修改app.py新增企业微信登录路由# app.py 新增 import requests import json # 企业微信配置 CORP_ID wx1234567890abcdef AGENT_ID 1000002 REDIRECT_URI http://127.0.0.1:5000/wx_callback # 需与企微后台配置一致 app.route(/login) def login(): # 生成state防CSRF import secrets state secrets.token_urlsafe(16) session[oauth_state] state # 构造扫码登录URL wx_login_url ( fhttps://open.work.weixin.qq.com/wwopen/sso/qrConnect? fappid{CORP_ID} fagentid{AGENT_ID} fredirect_uri{REDIRECT_URI} fstate{state} ) return render_template(login.html, wx_login_urlwx_login_url) app.route(/wx_callback) def wx_callback(): code request.args.get(code) state request.args.get(state) if state ! session.pop(oauth_state, None): return 非法请求, 403 # 用code换取access_token和user_id token_url fhttps://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid{CORP_ID}corpsecretYOUR_SECRET token_resp requests.get(token_url).json() access_token token_resp.get(access_token) user_url fhttps://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token{access_token}code{code} user_resp requests.get(user_url).json() if UserId in user_resp: # UserId即员工ID作为voter_id session[voter_id] user_resp[UserId] return redirect(url_for(index)) else: return f登录失败{user_resp.get(errmsg, 未知错误)}第三步创建templates/login.html!-- templates/login.html -- h2请使用企业微信扫码登录/h2 img src{{ wx_login_url }} alt企业微信扫码 width300 p打开企业微信 → 工作台 → 扫码登录/p注意YOUR_SECRET需替换为企业微信应用的Secret在管理后台获取。此方案无需后端存储用户信息voter_id即企微员工ID天然唯一。5.2 增加投票截止倒计时前端JS实时计算后端接口校验双重保险倒计时需前后端协同避免用户篡改前端时间。前端在vote.html中加入倒计时显示!-- templates/vote.html -- div idcountdown stylecolor:red; font-weight:bold;/div script // 从后端获取活动结束时间戳 const endTime {{ activity.end_time }}; // Jinja2注入 function updateCountdown() { const now Math.floor(Date.now() / 1000); const diff endTime - now; if (diff 0) { document.getElementById(countdown).textContent 投票已结束; document.getElementById(submitBtn).disabled true; return; } const days Math.floor(diff / 86400); const hours Math.floor((diff % 86400) / 3600); const mins Math.floor((diff % 3600) / 60); document.getElementById(countdown).textContent 剩余${days}天 ${hours}小时 ${mins}分钟; } updateCountdown(); setInterval(updateCountdown, 1000); /script后端在/api/vote中增加截止时间校验# app.py 的 handle_vote() 函数内在数据校验后添加 # 4. 截止时间校验 c.execute(SELECT end_time FROM activities WHERE id ?, (activity_id,)) end_time c.fetchone()[0] if time.time() end_time: return jsonify({status: error, message: 投票已结束不可提交}), 4035.3 导出PDF结果报告用WeasyPrint生成专业报表一行命令集成项目已预留/export/pdf/int:activity_id路由只需安装依赖并编写模板。安装WeasyPrintpip install weasyprint创建PDF模板templates/result_pdf.html!DOCTYPE html html head meta charsetutf-8 style body { font-family: Microsoft YaHei, sans-serif; margin: 2cm; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #333; padding: 8px; text-align: left; } .header { text-align: center; font-size: 18px; margin-bottom: 20px; } /style /head body div classheader2024年度优秀员工评选结果报告/div table thead trth排名/thth姓名/thth部门/thth票数/th/tr /thead tbody {% for i, cand in enumerate(candidates) %} tr td{{ i1 }}/td td{{ cand.name }}/td td{{ cand.department }}/td td{{ cand.vote_count }}/td /tr {% endfor %} /tbody /table div stylemargin-top: 30px; font-size: 14px; 生成时间{{ now.strftime(%Y年%m月%d日 %H:%M) }}br 总票数{{ total_votes }} /div /body /html在app.py中添加导出路由from weasyprint import HTML from datetime import datetime app.route(/export/pdf/int:activity_id) def export_pdf(activity_id): conn sqlite3.connect(voting.db) c conn.cursor() c.execute( SELECT c.name, c.department, c.vote_count FROM candidates c WHERE c.activity_id ? ORDER BY c.vote_count DESC , (activity_id,)) candidates c.fetchall() c.execute(SELECT COUNT(*) FROM votes WHERE activity_id ?, (activity_id,)) total_votes c.fetchone()[0] conn.close() # 渲染PDF模板 html_string render_template(result_pdf.html, candidatescandidates, total_votestotal_votes, nowdatetime.now()) html HTML(stringhtml_string) pdf_file html.write_pdf() return Response(pdf_file, mimetypeapplication/pdf, headers{Content-Disposition: attachment;filenameresult.pdf})提示WeasyPrint依赖系统字体Linux需安装fonts-wqy-zenheiWindows/macOS通常自带中文字体。若PDF中文乱码可在style中指定font-family: SimSun, NSimSun;。从那以后我每次接手B/S类小系统都强制走一遍“三查”查数据库唯一约束是否覆盖核心业务如voter_idactivity_id、查时间字段是否全用时间戳拒绝DATETIME字符串、查所有用户输入是否经过后端二次校验绝不信前端JS的maxlength。这三条看似笨拙却帮我在某高校课程答辩现场当场演示了“即使禁用JS系统依然防刷票、防重复、防超时”导师点头说“这才是工程思维。”希望帮到你。本文还有配套的精品资源点击获取