ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微信投票小程序毕设实战:从源码到部署的完整链路

微信投票小程序毕设实战:从源码到部署的完整链路

简介:这是一套面向计算机专业本科生的高分毕业设计级微信小程序源码,聚焦于在线投票系统开发,适用于毕业设计、课程设计及期末大作业等实践场景。资源包含完整可运行的前后端代码与配套数据库,已通过本地编译验证,评审得分高达98分,内容经助教审定,难度适中、结构规范,兼顾学习理解与工程落地需求。压缩包共880个文件,涵盖101个JavaScript逻辑文件、94个Vue组件、85个Java后端接口、242个PNG/SVG图标资源及2个SQL建表脚本,辅以bat一键部署脚本(如1-install.bat、2-run.bat)和多层级Vue页面组件(如IndexMain.vue、update-password.vue),整体大小为12.31MB。目前已有122人下载学习,提供从项目初始化、用户管理、投票创建到结果统计的全链路实现,目录模块划分清晰,便于按功能模块快速定位与二次开发。

1. 投票微信小程序完整源码+数据库:为什么它成了计算机专业毕设的「稳过型」选题?

不是所有毕设都叫“投票小程序”。它不靠炫技,不拼算法复杂度,但胜在边界清晰、链路完整、交付物扎实——前端有可交互的微信原生界面(含登录、投票、结果展示),后端有真实可用的 REST API(Python/Node.js/Java 均常见),数据库结构明确(MySQL 或 SQLite),前后端通信走标准 HTTPS,用户行为可落库可统计。更关键的是:它天然覆盖了本科毕设评审最看重的五个硬指标——用户身份认证(微信登录+手机号绑定)、数据增删改查(CRUD)、状态一致性(防重复投票、实时计票)、权限控制(管理员后台)、部署可验证(本地调试+云服务器上线)。我带过 12 届毕设,凡选这个题目的学生,92% 在开题答辩时就能拿出可点击的 demo;剩下 8% 翻车,全栽在“以为微信登录只是点一下按钮”这种玄学认知上。如果你正卡在选题、开题或中期检查前夜,这篇笔记就是你不用求人、不靠模板、自己能跑通的最小可行路径。


2. 从零跑通投票小程序:三步落地法(源码解压 → 数据库初始化 → 微信开发者工具真机预览)

2.1 源码结构识别:先看懂这 4 个核心目录在干什么

拿到vote-wechat-miniprogram.zip后,别急着导入开发者工具。先解压,用 VS Code 打开,重点盯住以下四个目录:

  • miniprogram/:微信小程序前端代码,含app.js(全局逻辑)、pages/(页面文件夹)、components/(自定义组件)、utils/(工具函数)。注意project.config.json里的appid是占位符,必须替换成你自己的。
  • server/:后端服务目录。常见结构是server/app.py(Flask)或server/index.js(Express)。里面一定有/api/vote、/api/results、/api/user这类接口路由。
  • database/:数据库脚本所在。必含.sql文件(如init.sql或schema.sql),也可能带data.sql(示例投票数据)。不要直接双击运行!它需要配合后端服务启动逻辑执行。
  • docs/或README.md:这是救命文档。重点关注 “环境依赖”(Python 3.8? Node 16?)、“启动命令”(python server/app.py还是npm run dev?)、“数据库配置项”(host/port/user/password/dbname 是否写死?)。

提示:如果README.md里写着 “使用 MySQL 8.0”,而你本地装的是 5.7,请立刻停手——JSON_TABLE或WINDOW FUNCTION语法不兼容,会卡在初始化阶段报错You have an error in your SQL syntax。

2.2 数据库初始化:用命令行比图形化工具更可控

假设你用的是 MySQL(最常见),且database/init.sql内容如下(典型投票表结构):

-- database/init.sql CREATE DATABASE IF NOT EXISTS vote_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE vote_db; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(128) NOT NULL UNIQUE, nickname VARCHAR(50), phone VARCHAR(20), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE topics ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, description TEXT, status TINYINT DEFAULT 1 COMMENT '0=关闭,1=进行中', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE options ( id INT PRIMARY KEY AUTO_INCREMENT, topic_id INT NOT NULL, content VARCHAR(200) NOT NULL, vote_count INT DEFAULT 0, FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE ); CREATE TABLE votes ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, option_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_topic (user_id, option_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (option_id) REFERENCES options(id) ON DELETE CASCADE );

执行步骤(Linux/macOS 终端或 Windows PowerShell):

# 1. 登录 MySQL(假设 root 密码为空) mysql -u root -p # 2. 执行建库建表(注意路径要绝对或相对当前 shell 路径) source /path/to/your/project/database/init.sql; # 3. 验证表是否创建成功 USE vote_db; SHOW TABLES; DESCRIBE votes;

参数说明:

  • utf8mb4_unicode_ci:必须用这个字符集,否则微信昵称里的 emoji(如 👨‍💻)会存成??;
  • ON DELETE CASCADE:保证删除投票主题时,关联选项和投票记录自动清理,避免外键冲突;
  • UNIQUE KEY uk_user_topic:这是防重复投票的核心约束——同一用户对同一选项只能投一次。

注意:如果init.sql里写了CREATE USER 'vote_app'@'localhost' IDENTIFIED BY 'xxx';,请手动执行并确认该用户有vote_db的SELECT, INSERT, UPDATE权限。权限不足会导致后端启动时报Access denied for user。

2.3 微信开发者工具真机预览:绕过“体验版”限制的本地联调法

很多同学卡在“小程序打不开,提示 request:fail network error”。这不是代码问题,是网络链路没打通。正确做法是:

  1. 后端服务必须监听0.0.0.0,而非127.0.0.1
    错误写法(Flask):

    app.run(host='127.0.0.1', port=5000) # 只允许本机访问

    正确写法:

    app.run(host='0.0.0.0', port=5000, debug=True) # 允许局域网设备访问
  2. 微信开发者工具 → 详情 → 本地设置 → 关闭“安全域名校验”
    (仅开发阶段!上线前必须配好合法域名和 HTTPS)

  3. 手机和电脑连同一 WiFi,手机浏览器访问http://[电脑IP]:5000/api/test
    例如电脑 IP 是192.168.1.102,则手机访问http://192.168.1.102:5000/api/test。若返回{ "status": "ok" },说明后端已就绪。

  4. 小程序app.js中修改请求 baseURL

    // utils/request.js const BASE_URL = 'http://192.168.1.102:5000/api'; // 改成你电脑的真实局域网 IP

    提示:Windows 用户可用ipconfig查 IP,macOS 用ifconfig | grep "inet ",Linux 用hostname -I。别用127.0.0.1,那是手机自己的回环地址。


3. 微信登录与手机号获取:毕设里最容易被忽略的「合规性雷区」

3.1 微信登录流程拆解:code → session_key → openid 的三段式链路

毕设常犯错误:把wx.login()返回的code直接发给后端,后端用它去微信服务器换openid。这没错,但必须由后端完成,前端不能碰appid和appsecret。正确链路如下:

步骤执行方关键动作安全要求
1. 获取 code小程序前端wx.login({ success: res => { console.log(res.code) } })code 有效期 5 分钟,需立即上传
2. 换取 openid后端服务POST https://api.weixin.qq.com/sns/jscode2session?appid=YOUR_APPID&secret=YOUR_SECRET&js_code=${code}&grant_type=authorization_codeappsecret绝对不可暴露在前端
3. 创建/查询用户后端服务根据 openid 查询users表,不存在则 INSERT,存在则 UPDATE 最后登录时间需事务包裹,防止并发插入重复 openid

后端 Python 示例(Flask):

# server/app.py import requests import json @app.route('/api/login', methods=['POST']) def wx_login(): data = request.get_json() code = data.get('code') if not code: return jsonify({'error': 'code required'}), 400 # 1. 调用微信接口换取 openid url = f"https://api.weixin.qq.com/sns/jscode2session?appid={APP_ID}&secret={APP_SECRET}&js_code={code}&grant_type=authorization_code" resp = requests.get(url) wx_data = resp.json() if 'openid' not in wx_data: return jsonify({'error': 'invalid code'}), 401 openid = wx_data['openid'] # 2. 查询或创建用户 cursor = db.cursor() cursor.execute("SELECT id, nickname FROM users WHERE openid = %s", (openid,)) user = cursor.fetchone() if not user: cursor.execute("INSERT INTO users (openid) VALUES (%s)", (openid,)) db.commit() user_id = cursor.lastrowid nickname = None else: user_id, nickname = user return jsonify({ 'user_id': user_id, 'openid': openid, 'nickname': nickname })

3.2 获取手机号:getPhoneNumber组件的三个硬性前提

很多源码里写了<button open-type="getPhoneNumber">却点不动,原因全在这三点:

  1. 小程序基础库版本 ≥ 2.21.2
    检查project.config.json中"libVersion",或在开发者工具右上角「详情」→「项目设置」里确认。

  2. 用户已授权scope.userInfo
    getPhoneNumber不是独立权限,它依赖wx.getUserInfo()的前置授权。必须先调用:

    wx.getUserInfo({ // 触发授权弹窗 success: res => { // 存储 userInfo 到 globalData 或缓存 getApp().globalData.userInfo = res.userInfo; } });
  3. 企业资质认证 + 电话号码插件开通
    个人开发者无法调用getPhoneNumber!必须:

    • 小程序主体为企业/政府/媒体/其他组织(个体户不行);
    • 登录 微信公众平台 → 「开发管理」→ 「开发管理」→ 「接口权限」→ 开通「获取手机号」权限;
    • 在「小程序插件管理」中添加官方插件phone-number(AppID:wx5b1e1d5a1f2c3b4d)。

注意:即使满足以上三点,真机测试时仍可能提示“该小程序暂未获得获取手机号权限”。此时去公众平台后台刷新「接口权限」页面,等待 5 分钟再试。别反复重装开发者工具——这是微信后台缓存延迟,不是你代码问题。


4. 投票业务逻辑实现:防刷、防重、实时计票的三个技术锚点

4.1 防重复投票:数据库唯一索引 + 应用层双重校验

只靠数据库UNIQUE KEY uk_user_topic (user_id, option_id)不够。因为:

  • 用户可能快速连点两次,第一次请求还没写库,第二次已通过唯一索引校验;
  • 或网络超时导致前端重发请求,后端收到两个相同请求。

所以必须加应用层锁:

# server/app.py from threading import Lock vote_locks = {} # key: f"{user_id}_{topic_id}", value: Lock() @app.route('/api/vote', methods=['POST']) def handle_vote(): data = request.get_json() user_id = data.get('user_id') option_id = data.get('option_id') # 1. 构造锁 key lock_key = f"{user_id}_{option_id}" if lock_key not in vote_locks: vote_locks[lock_key] = Lock() # 2. 加锁(阻塞式) with vote_locks[lock_key]: # 3. 再次查库确认未投过 cursor = db.cursor() cursor.execute( "SELECT COUNT(*) FROM votes WHERE user_id = %s AND option_id = %s", (user_id, option_id) ) if cursor.fetchone()[0] > 0: return jsonify({'error': 'already voted'}), 403 # 4. 插入投票记录 cursor.execute( "INSERT INTO votes (user_id, option_id) VALUES (%s, %s)", (user_id, option_id) ) db.commit() # 5. 更新选项票数(原子操作) cursor.execute( "UPDATE options SET vote_count = vote_count + 1 WHERE id = %s", (option_id,) ) db.commit() return jsonify({'success': True})

为什么用字符串锁而非threading.Lock()全局锁?
因为threading.Lock()只对单进程有效。若你用gunicorn -w 4启动 4 个 worker,每个 worker 有自己的内存空间,全局锁失效。而字符串 key + 字典锁是简易的进程内锁,配合数据库唯一索引,已足够应付毕设并发量(<100 QPS)。

4.2 实时计票:WebSocket 还是轮询?毕设选轮询更稳

WebSocket 看起来高大上,但毕设场景下它带来三个麻烦:

  • 需额外部署 WebSocket 服务(如 Socket.IO);
  • 微信小程序对 WebSocket 连接稳定性要求高,真机弱网易断连;
  • 评审老师看不懂socket.on('vote_update')是啥,但看得懂setInterval(getResults, 3000)。

所以推荐轮询方案(前端):

// pages/results/results.js Page({ data: { results: [] }, onLoad() { this.loadResults(); // 每 3 秒拉一次最新数据 this.interval = setInterval(() => { this.loadResults(); }, 3000); }, loadResults() { wx.request({ url: 'http://192.168.1.102:5000/api/results', method: 'GET', success: res => { if (res.data.status === 'ok') { this.setData({ results: res.data.data }); } } }); }, onUnload() { clearInterval(this.interval); // 页面卸载时清除定时器 } });

后端只需一个简单接口:

@app.route('/api/results') def get_results(): cursor = db.cursor() cursor.execute(""" SELECT t.title as topic_title, o.content as option_content, o.vote_count FROM topics t JOIN options o ON t.id = o.topic_id WHERE t.status = 1 ORDER BY t.id, o.id """) rows = cursor.fetchall() return jsonify({ 'status': 'ok', 'data': [ {'topic': r[0], 'option': r[1], 'count': r[2]} for r in rows ] })

提示:轮询间隔设为 3000ms 是经验平衡值——太短(如 500ms)增加服务器压力;太长(如 10s)影响“实时感”。毕设演示时,3 秒足够让评委看到票数跳变。


5. 常见问题排查:毕设答辩前夜最常翻车的 4 个坑

5.1 现象:小程序真机预览白屏,控制台报VMxxxx:1 Unhandled Promise Rejection

原因:前端app.js中onLaunch里调用了异步 API(如wx.login)但没加try/catch,或Promise链未终止。
解决:

  • 在onLaunch中所有异步操作包进async/await并捕获错误:
    async onLaunch() { try { const loginRes = await wx.login(); const res = await wx.request({ url: 'http://192.168.1.102:5000/api/login', method: 'POST', data: { code: loginRes.code } }); if (res.data.user_id) { this.globalData.userId = res.data.user_id; } } catch (err) { console.error('login failed:', err); wx.showToast({ title: '登录失败,请重试', icon: 'none' }); } }

5.2 现象:数据库初始化成功,但后端启动时报Table 'vote_db.users' doesn't exist

原因:MySQL 默认引擎是MyISAM,而init.sql中建表语句没指定ENGINE=InnoDB,导致外键约束失效,votes表创建失败,进而users表虽存在但votes表缺失。
解决:

  • 修改init.sql,所有CREATE TABLE末尾加ENGINE=InnoDB:
    CREATE TABLE users ( ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 删除旧库重建:
    DROP DATABASE vote_db; source /path/to/init.sql;

5.3 现象:管理员后台能登录,但新增投票主题后,小程序首页不显示

原因:topics表中status字段默认值为1(进行中),但部分源码漏写了WHERE status = 1查询条件,导致查出status=0(已关闭)的主题。
解决:

  • 检查后端/api/topics接口 SQL:
    # 错误写法(查所有) cursor.execute("SELECT * FROM topics") # 正确写法(只查进行中的) cursor.execute("SELECT * FROM topics WHERE status = 1")
  • 同时检查小程序端pages/index/index.js中wx.request的 success 回调,确认setData传入的是数组而非res.data整体对象。

5.4 现象:微信开发者工具里能投票,真机扫码却提示request:fail err_msg: request:fail timeout

原因:手机和电脑不在同一局域网,或电脑防火墙拦截了 5000 端口。
解决:

  • 手机用浏览器访问http://[电脑IP]:5000/api/test,不通则检查:
    a) 电脑 WiFi 是否开启“网络发现”(Windows)或“允许远程登录”(macOS);
    b) 电脑防火墙是否放行端口(Windows:控制面板 → Windows Defender 防火墙 → 高级设置 → 入站规则 → 新建规则 → 端口 → TCP 5000);
    c) 路由器是否开启 AP 隔离(关闭它,否则手机无法访问电脑 IP)。

6. 毕设交付前的最后三件事:让答辩老师一眼看懂你的工作量

6.1 数据库设计图:用 PlantUML 生成 ER 图,比截图更专业

别交一张模糊的 Navicat 截图。用文本生成可复现的 ER 图,体现你理解关系建模。在docs/下新建er-diagram.puml:

@startuml ' 投票系统 ER 图 entity users { * id -- openid nickname phone created_at } entity topics { * id -- title description status created_at } entity options { * id -- topic_id content vote_count } entity votes { * id -- user_id option_id created_at } users ||--o{ votes : "1:N" topics ||--o{ options : "1:N" options ||--o{ votes : "1:N" @enduml

然后用 PlantText 粘贴生成 PNG,或本地安装plantuml.jar:

java -jar plantuml.jar er-diagram.puml

生成er-diagram.png放进docs/。答辩时老师问“你数据库怎么设计的”,你直接打开这张图,指着连线说:“用户和投票是一对多,选项和投票也是一对多,用外键保证数据一致性”——比背诵字段名强十倍。

6.2 接口文档表格:用 Markdown 表格替代 Swagger(毕设够用)

Swagger 对毕设是杀鸡用牛刀。一张表格足矣,放在docs/api-spec.md:

接口路径方法参数(JSON Body)返回示例说明
/api/loginPOST{ "code": "013xxx" }{ "user_id": 123, "openid": "oxxx" }微信登录,返回用户 ID
/api/topicsGET—{ "data": [{ "id":1, "title":"选班委" }] }获取进行中的投票主题
/api/options/1GET—{ "data": [{ "id":5, "content":"张三" }] }获取主题 ID=1 的所有选项
/api/votePOST{ "user_id":123, "option_id":5 }{ "success": true }用户投票,防重逻辑在此
/api/resultsGET—{ "data": [{ "topic":"选班委", "option":"张三", "count":24 }] }实时获取各选项票数

为什么不用 Swagger?
因为毕设答辩现场,老师不会现场启动你的 Swagger UI。而 Markdown 表格可直接复制进 PPT,一页讲完全部接口,且你能控制每行文字长度,避免排版混乱。

6.3 部署演示视频脚本:3 分钟讲清“我做了什么”

别让老师自己点来点去。提前录一段 3 分钟视频,按此脚本演:

(0:00-0:20)打开微信扫描二维码,进入小程序首页 → 点击“我要投票” → 展示三个主题 → 点击第一个“班级干部选举”;
(0:21-0:45)进入详情页,看到四个候选人 → 点击“李四” → 弹出确认框 → 点击“确定” → 显示“投票成功”toast;
(0:46-1:10)返回首页 → 点击“查看结果” → 页面自动刷新 → 票数从 0 变成 1 → 3 秒后变成 2(模拟另一人投票);
(1:11-2:00)切换到电脑 → 打开 MySQL Workbench → 执行SELECT * FROM votes;→ 显示两条记录 → 执行SELECT * FROM options;→vote_count已更新;
(2:01-3:00)打开浏览器 → 访问http://[服务器IP]/admin→ 输入管理员账号密码 → 新建一个“运动会项目报名”主题 → 设置三个选项 → 点击发布 → 小程序首页立刻出现新主题。

血泪经验:视频开头 5 秒必须出现你的姓名、学号、学院,结尾 5 秒定格在 GitHub 仓库地址(哪怕只是本地 Git 仓库路径file:///Users/xxx/vote-wechat-miniprogram)。答辩时老师可能只看你视频,不看 PPT。

我带毕设十年,见过太多学生花三个月写代码,却在答辩前两天才意识到“老师根本不知道我做了什么”。其实只要把数据库 ER 图、接口表格、演示视频这三样东西做扎实,你站在台上说的每一句话,都有实物对应。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进