简介:这是一份面向计算机专业毕业设计或课程设计的微信小程序预约挂号系统项目,覆盖管理员、医生、用户三类角色,包含科室与医生信息、排班、预约、取消预约、调班申请等核心模块;后台采用 Java SSM 框架,搭配 MySQL 数据库,小程序端基于微信开发者工具实现,业务闭环清晰,并配有本地运行配置说明。压缩包共 1210 个文件,约 18.92MB,以 png、vue、java、js、wxml/wxss、json 等为主,前端与小程序页面文件充足,Java 和 XML 承担后台接口与配置,SQL 文件用于初始化数据库,另有 bat 脚本方便安装、运行与构建,目录结构便于快速检索。内容包含管理后台 vue 页面、小程序端页面、图标素材、项目配置及部署脚本,适合需要参考前后端交互流程、数据库表设计或二次开发的人群。已有 77 人学习,可作为毕业设计答辩或课程项目实践的可靠参考。
1. 预约挂号小程序:为什么这个毕设方向值得做
凌晨三点在某三甲医院排队挂号,队伍里全是裹着大衣打盹的人——这是很多小城市医院的日常。预约挂号系统想解决的就是这件事:把号源放到线上,让患者按时间段来,而不是起大早排队。用微信小程序做载体,是因为它不需要用户安装 App,扫码即用,天然适合“偶尔用一次”的医疗场景。这个标题下的完整交付物通常包括小程序前端、后端接口、管理后台和数据库设计,覆盖了一个真实业务系统从 C 端到 B 端的全部链路,这也是它常被选为毕设题目的原因。适合的人群很明确:准备做毕设的在校生、想练手全栈开发的初学者,以及需要给医院或诊所搭建线上预约demo的从业者。读完这篇文章,你能知道它内部到底有哪些模块、代码怎么组织、数据库怎么设计,以及上线时最容易踩的坑在哪。
2. 预约挂号系统的技术骨架:小程序端 + 服务端 + 数据库的边界划分
2.1 页面结构与应用边界:挂号小程序到底拆成几个模块
预约挂号小程序从用户视角看,通常分成四个核心模块:登录与账号体系、科室与医生列表、号源与时间选择、预约记录与取消。管理端另算一套,常见做法是单独做一个 Web 管理界面,给医院工作人员维护排班和查看预约明细。前端部分由微信小程序原生语法完成,页面文件涉及wxml、wxss、js、json四种文件,每一个页面一个目录。
我一般会用微信开发者工具新建项目,选择 JavaScript 基础模板,不启用 TypeScript,因为毕设场景下讲究的是“能短时间跑通”,原生 JavaScript 加微信云开发的组合能把前后端运维成本压到最低。页面结构上,tabBar 通常配置首页、预约、我的三大入口,其中“预约”页面内部嵌套科室列表和医生排班两个子页面。
模块拆分的价值在于把复杂度隔离。登录、挂号和查询记录这三个模块完全独立,互不依赖,前端可以分步联调。开发时每完成一个 tab 页就先用模拟数据渲染,这一步能提前暴露大部分页面跳转和数据格式问题,比后端写完再一次性接入效率高得多。
2.2 云开发 vs 自建后端:两种放弃纠结的选型标准
这是做这个项目时第一个要拍板的决策。常见方案有两种:一种是用微信云开发,数据库、云函数、存储都在腾讯云端,不需要自己买服务器;另一种是自建后端,用 Node.js、Java Spring Boot 或 Python Flask 写接口,数据库用 MySQL,部署在自己的云主机上。
| 对比维度 | 云开发方案 | 自建后端方案 |
|---|---|---|
| 服务器成本 | 有免费额度,入门期基本零成本 | 至少需要一台云主机,年费几百起 |
| 部署难度 | 云函数上传即用,无需 Nginx | 需要配置域名、HTTPS 证书、反向代理 |
| 数据安全 | 数据库权限可控,但需理解安全规则 | 完全自主可控,可做复杂事务 |
| 毕设答辩亮点 | 展示了 Serverless 思想 | 展示了传统后端全栈能力 |
| 适合人群 | 时间紧、以前端为主 | 后端能力想突出展示 |
面向毕设这个场景,我的倾向是自建后端。原因很现实:答辩老师对“使用云开发”已经麻木了,反而对接口设计、数据库建表这种传统技术点更感兴趣。而且自建后端能让你掌握从建表到接口联调的全流程,简历上也更容易写清楚项目职责。如果你完全没有服务器运维经验,或者预算为零,那选云开发也不丢人——完成度比技术选型更重要。
2.3 会话与登录:unionid、openid 与 token 的取舍
登录是预约系统的身份基座,处理不好会出现“用户明明登录了,预约时又要求登录”的尴尬。微信小程序里的登录链路是:小程序端调用wx.login拿到临时code,把code发给后端,后端用code换取openid和session_key。openid是用户在某个小程序下的唯一 ID,够用;unionid是同一微信开放平台账号下的统一 ID,在毕设里基本用不上,不用给自己加复杂度。
// 登录接口示例:微信登录 code 换 openid const express = require('express'); const axios = require('axios'); const app = express(); app.use(express.json()); app.post('/api/login', async (req, res) => { const { code } = req.body; const appid = '你的小程序appid'; const secret = '你的小程序secret'; // 用 code 换取 openid,这是微信服务器的固定端点 const { data } = await axios.get(`https://api.weixin.qq.com/sns/jscode2session`, { params: { appid, secret, js_code: code, grant_type: 'authorization_code' } }); // 正常情况下 data.openid 就是用户唯一标识 // 后续生成 token 返回给前端,前端请求时携带即可 res.json({ openid: data.openid, token: '你生成的token字符串' }); });这段代码的核心是wx.login产出的code是一次性的,5 分钟内有效,且只能换一次。如果前端重复使用同一个code,后端的返回结果会报errcode: 40029。这是联调时最常见的登录报错。token 我习惯用jsonwebtoken生成,payload 里只放openid和过期时间,服务端提前设置 7 天有效期,让用户下次打开小程序仍是登录态。不要自己写随机字符串来作为 token,容易出会话串号的问题。
3. 从 0 到 1 跑通小程序端:登录、医生列表与号源选择
3.1 最小可运行的项目骨架:app.js 与 tabBar 配置
微信小程序的骨架由根目录的app.js、app.json、app.wxss和project.config.json组成。app.json是全局配置文件,需要声明页面路由和tabBar。
{ "pages": [ "pages/index/index", "pages/register/register", "pages/mine/mine", "pages/doctor/doctor", "pages/order/order" ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/register/register", "text": "预约" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] }, "window": { "navigationBarTitleText": "在线预约挂号", "navigationBarBackgroundColor": "#2b6cb0" } }这份配置的要点在于:第一项pages列表里的第一个页面是启动页,通常放首页;tabBar的pagePath必须与pages里的路径完全一致,否则编译报错;navigationBarTitleText是导航栏标题,每个页面也可以用自己目录里的json文件覆盖它。写配置时常犯的错误是把pages里的路径漏了扩展名,比如写成"pages/index/index.js",系统会直接编译不过。
3.2 微信一键登录:从 code 到 token 的完整链路
小程序端登录按钮调起wx.login,获取code后传给后端接口,拿到token后存入缓存。
// 小程序端登录逻辑 login() { wx.login({ success: async (res) => { if (res.code) { // 把 code 发给后端换取 token const result = await request.post('/api/login', { code: res.code }); // 将 token 持久化,后续请求都带上它 wx.setStorageSync('token', result.data.token); wx.setStorageSync('openid', result.data.openid); wx.navigateBack(); } else { wx.showToast({ title: '登录失败', icon: 'none' }); } } }); }这里有个容易被忽略的逻辑:wx.login在用户未授权手机号的情况下也能调用成功,因为code换取的是openid,不是手机号。不要给用户弹更大的授权窗,让他们觉得负担很重。预约时如果需要绑定手机号,可以单独在“我的”页面里做补充。token 存到wx.setStorageSync后,每次请求从getStorageSync取出来塞进header,比每次重新调wx.login都更方便、更可靠。后端写一个中间件统一校验token,验证失败时返回401,前端收到401后再重新走登录流程。
3.3 号源列表与剩余号数:数据绑定和 wx:for 渲染要点
号源列表页是用户访问频率最高的页面,核心交互就是左侧科室分类、右侧医生列表(包含医生姓名、职称、剩余号数),点击医生后进入排班详情。数据用wx:for循环渲染,渲染结构如下:
<view class="doctor-item" wx:for="{{doctors}}" wx:key="id"> <view class="doctor-info"> <text>{{item.name}}</text> <text class="title">{{item.title}}</text> </view> <view class="remain"> <text wx:if="{{item.remain > 0}}" class="has-remain">余号 {{item.remain}}</text> <text wx:else class="no-remain">约满</text> </view> </view>wx:key必须绑定列表项里的唯一字段,一般用id或数据库主键。如果忽略wx:key,控制台会警告且列表重绘时可能出现渲染错位。wx:if和wx:else是兄弟节点,中间不能插入注释或其他节点。剩余号数的显示由后端返回的remain字段决定,后端在返回数据时直接做判断,不要在 JS 里再二次过滤,两个端各判断一层不仅浪费代码,还容易出现“显示有余号、点击却挂号失败”前后端不同步的问题。
4. 后端接口与数据库设计:让号源扣减不超卖、查询不卡顿
4.1 五张核心表的设计与字段边界
预约挂号系统最少需要五张表:用户表、科室表、医生表、排班表(号源表)、预约记录表。这是业务闭环的下限,缺一张业务就断链了。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | openid、name、phone、created_at | openid 加唯一索引 |
| department | name、intro | 科室名称,前端分类展示用 |
| doctor | name、title、department_id、intro | 职称如主任医师、副主任医师 |
| schedule | doctor_id、date、period、total、remain | period 表示上午/下午,remain 是余号数 |
| appointment | user_id、schedule_id、status、created_at | status 表示已预约/已取消/已完成 |
schedule表是核心。total表示该排班的总号数,remain表示剩余号数,每次挂号成功后remain = remain - 1。这里务必用整数字段,不要用字符串存数字,否则后续做加减运算时极容易踩类型坑。appointment表的status建议用整数枚举:0已取消、1已预约、2已完成,不推荐用中文直接存储,后端判断逻辑写if (status === 1)比if (status === '已预约')更稳妥,排序和索引效率也更高。
4.2 号源扣减:数据库层面如何避免“超挂”
号源超挂是预约系统最严重的逻辑 Bug,原因往往在于先查询余号、再更新余号这两步之间存在时间差。假设两个用户同时看到了remain = 1,都去挂号,如果后端先SELECT再UPDATE,最终两个人都可能挂号成功,remain最后变成-1。数据库的兜底方案是用原子更新语句。
-- 原子扣减号源:只更新余号大于 0 的记录 UPDATE schedule SET remain = remain - 1 WHERE id = ? AND remain > 0;这条 SQL 能确保同一时刻只有一个请求更新成功。应用层还要处理affectedRows的返回值——如果影响行数为 0,说明号源已被抢完,立即返回“该时间段已约满”。在 NACOS 把这条 SQL 执行后,注册中心发生了什么?没有,这是 MySQL 自身的事务能力。真正需要关注的是后端配合事务:先执行上述UPDATE,再INSERT一条预约记录,两步之间不会出现数据割裂。很多小白做这个项目会把扣减逻辑放在前端 JS 里算出remain - 1再 UPDATE,这是把并发风险敞开了。
如果用的是 Node.js + MySQL,连接池操作时一定要用事务包裹UPDATE和INSERT两条语句,否则会出现号源扣了但预约记录没生成的情况。事务写法是BEGIN、两条 SQL、COMMIT,任一步失败则ROLLBACK。
4.3 管理端查询与导出:Excel 导出的字段格式
管理端是面向医院工作人员的,核心功能是查看某天的预约名单和取消记录。后端提供一个/api/appointments/export接口,返回 JSON 数组,前端管理界面用表格渲染即可。字段顺序保持与数据库表一致,方便调试。
router.get('/appointments/export', async (req, res) => { const { date } = req.query; // 联查医生表和排班表,拼出医生姓名和就诊时间 const rows = await db.query(` SELECT a.id, u.name AS user_name, u.phone, d.name AS doctor_name, s.date, s.period, a.status FROM appointment a LEFT JOIN user u ON a.user_id = u.id LEFT JOIN schedule s ON a.schedule_id = s.id LEFT JOIN doctor d ON s.doctor_id = d.id WHERE s.date = ? `, [date]); res.json({ data: rows }); });这段联查的重点在LEFT JOIN的使用。预约记录是主体,它关联了用户、排班、医生,任何一条记录都可能缺少排班信息(比如排班被管理员删除),用LEFT JOIN保证预约记录不会因为关联表缺数据而丢失。需要注意 SQL 里变量日期用?占位符、不用字符串拼接,防止 SQL 注入。
5. 预约挂号全流程避坑:最常见的 5 个翻车点
5.1 微信开发者工具正常、手机预览白屏
这是最让人恼火的 Bug,代码在开发者工具上完全正常,手机扫码预览页面一片空白。根本原因是开发者工具默认不校验合法域名,而手机端是真实网络环境,所有的接口请求地址必须在小程序管理后台配置为request合法域名。域名必须为 https,且不能带端口号。解决方法是先确认自己是否使用了 IP 加端口的形式请求接口——调试时可以这样做,但真机预览必须替换成已备案且配置好 HTTPS 证书的域名。另外,app.json里漏配了某个pages页面路径时,真机也会白屏,开发者工具日志里会有 “page not found” 报错,注意看。
5.2 再次打开小程序提示登录过期,token 失效
用户经常隔几天再打开小程序,发现需要重新登录。原因是token设置了 7 天有效,而用户可能一个月没打开。解决思路是在小程序端request封装里做全局拦截——当后端返回401时,静默调用wx.login获取新 token,成功后重发原请求。这个机制能显著降低用户的登录频率感,也能保证预约操作不会因为登录态失效而中断。
5.3 号源显示“约满”但数据库里有余额
后端返回的列表页remain字段和数据库里的schedule.remain不一致。常见原因是前端渲染的是页面初始化时缓存的数据,切换科室、切换日期时没有重新请求接口,用户看到的是旧的号源状态。解决方法是每次进入排班页都在onShow生命周期里重新拉取号源数据,而不是写在onLoad里——onLoad只执行一次,onShow每次页面展示都会触发。前端页面从一个 tab 切到另一个 tab 再切回来时,不会触发onLoad,只会触发onShow,这个差别就是玄学所在。
5.4 取消预约后余号没变回来
用户取消预约后,appointment.status改为 0,但排班表的remain没有加 1。原因是很多实现只改了预约状态,没做配套更新。正确做法是在取消接口里同时执行两条 SQL:先查这条预约对应哪个schedule_id,然后更新状态为已取消,再把该排班的remain + 1。和号源扣减一样,二条 SQL 必须放进事务,否则半路报错会让数据变得不一致。
5.5 数据库时间字段有时差,排班日期错位
数据库存的是服务器时间,服务器时区若设置为 UTC,中国用户看到的日期就会晚 8 个小时。排班表按日期查询时经常出现“今天”和“明天”分界错乱。解决方法是建表时给created_at等字段设置DEFAULT CURRENT_TIMESTAMP,并且在连接数据库的 URL 里显式指定时区参数,例如timezone: '+08:00'。不要从后端先把时间转好再传,统一交给数据库取本地时间。
6. 让预约系统撑住早高峰:限流、缓存与压测的三个习惯
微信预约系统有一个独特的访问特征:号源通常在早上 8 点统一放号,用户会提前 10 分钟涌入,这 10 分钟的请求量可能是全天峰值的几十倍。如果只在开发环境里跑通功能,上线后大概率会卡死,因此上线前必须做三件事:接口限流、热点缓存、并发压测。
接口限流最简单的落地方式是固定窗口计数,用一个内存计数器维护每分钟请求数,超过阈值直接返回“系统繁忙,请稍后重试”:
const rateLimit = new Map(); function limit(openid) { const current = Date.now(); const windowKey = Math.floor(current / 60000); // 每分钟一个窗口 const count = rateLimit.get(openid + windowKey) || 0; if (count >= 30) return false; rateLimit.set(openid + windowKey, count + 1); return true; }这段代码有一个缺陷:Map 只增不减会占内存。生产环境要定期清理过期 key,或在项目里用现成的rate-limiter-flexible库,不必自己造轮子。热点缓存则是号源列表的常见优化点:把“明日科室列表 + 医生号源”在每天 0 点写入内存缓存,前端请求直接从缓存读,数据库仅在预约动作时发生写操作,这能分化很大一部分查询压力。压测工具我常用wrk或ab,先用 50 并发跑 10 分钟看错误率,再把并发提到 200,观察接口响应时间曲线,如果平均响应时间超过 1 秒,就要考虑加一层 Redis 或调整 SQL 索引。
另外有一个习惯建议:每次发版前把自己的小程序跑一遍完整链路——登录、查号源、预约、取消、再看余号恢复。这五个步骤就是整个系统的命脉,任何一环出问题,业务都断了。某次我印象很深,一次改表结构时漏迁移了一个字段,结果用户预约全都成功、医生端却看不到名单,排查了一个下午才发现是字段映射错位。从那以后,我上线前一定先用真机把五步流程走一遍,再去看后台数据库对应表里的数据,两头对得上才算发版完成。这套方法帮你守住质量底线,希望帮到你。
本文还有配套的精品资源,点击获取