
简介这是一套面向计算机专业本科生的毕业设计级微信小程序医院预约挂号系统适用于课程设计、期末大作业及全栈开发实战学习旨在帮助学生掌握小程序开发、前后端协同与医疗类业务系统建模能力。资源包含2000个文件主体为881个PHP后端逻辑文件、148个JS交互脚本、118个Vue组件、81个Excel数据模板含排班表、统计报表等、76个JSON配置与接口定义以及SVG/PNG图标、WXSS/WXML界面文件等完整覆盖用户端小程序、管理后台与数据库支撑体系压缩包仅19.82MB轻量易部署。已有372人学习下载资源结构清晰预览可见install.bat/run.bat等自动化脚本及IndexMain.vue、update-password.vue等核心页面组件还包含SQL数据库文件、医生排班xlsx模板、API文档md及bak备份文件便于理解版本迭代与工程规范。1. 微信小程序医院预约挂号系统为什么一个“常规需求”在真实落地时总卡在登录态、号源同步和数据库事务上这不是一个教你怎么拖拽组件的入门教程。如果你正被「用户点预约没反应」「后台改了号源前端不更新」「同一时间两个用户抢到同一个号」这类问题反复折磨说明你已经踩进了医疗类小程序最典型的三个深坑微信登录态与医院HIS系统的身份映射断裂、号源库存的强一致性缺失、以及挂号操作跨小程序前端、云函数、数据库三端的事务断层。本项目不是Demo玩具——它包含可直接部署的微信小程序源码含完整页面路由与状态管理、MySQL结构化数据库设计含医生排班、号段控制、退号锁表逻辑、以及配套论文中对“挂号并发冲突消解策略”的实证分析。适合两类人一是正在交付医院信息化项目的开发者需要一套经真实门诊压力验证的最小可行架构二是计算机专业学生做毕设需避开“假数据假流程”的常见翻车点。全文不讲WXML语法只拆解怎么让挂号动作真正原子化、怎么让号源变更秒级触达用户、以及为什么用云开发反而比自建Node服务更容易崩。2. 搭建可运行的挂号系统从初始化数据库到小程序真机调试的四步闭环2.1 创建支持并发挂号的MySQL数据库字段设计直指业务痛点挂号系统崩溃往往始于数据库设计。我们不用“用户表订单表商品表”这种电商思维而是按医疗场景重构-- 医生排班表关键status字段控制号源可见性 CREATE TABLE doctor_schedule ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, doctor_id VARCHAR(32) NOT NULL COMMENT 医生工号, date DATE NOT NULL COMMENT 出诊日期, session TINYINT NOT NULL COMMENT 1上午,2下午,3夜间, total_quota INT NOT NULL DEFAULT 0 COMMENT 总号源数, used_quota INT NOT NULL DEFAULT 0 COMMENT 已挂出数, status TINYINT NOT NULL DEFAULT 1 COMMENT 0停诊,1正常,2临时加号, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_doctor_date_session (doctor_id, date, session) ); -- 挂号订单表关键statusversion双校验防超挂 CREATE TABLE appointment_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 全局唯一订单号, user_openid VARCHAR(64) NOT NULL COMMENT 微信用户openid, schedule_id BIGINT UNSIGNED NOT NULL COMMENT 关联排班ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付,1已挂号,2已取消,3已过期, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号每次更新1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_time (user_openid, created_at), KEY idx_schedule_status (schedule_id, status) );提示doctor_schedule.used_quota不是靠COUNT(*)实时统计而是在每次成功挂号后UPDATE ... SET used_quota used_quota 1 WHERE id ? AND used_quota total_quota—— 这是避免超挂的核心。version字段用于乐观锁后续云函数更新订单时会校验。2.2 小程序端用wx.logincode2Session构建可信用户链路很多团队直接把wx.getUserProfile获取的昵称当用户身份这是重大隐患。真实挂号必须绑定实名信息而微信不提供身份证号。我们的做法是小程序端只负责获取code所有身份解析交给云函数或后端服务。// pages/index/index.js Page({ data: { userInfo: null, hasLogin: false }, // 用户点击授权按钮触发 onGetUserInfo(e) { if (e.detail.userInfo) { // 仅存储昵称头像用于UI展示不作为身份凭证 this.setData({ userInfo: e.detail.userInfo, hasLogin: true }); } }, // 真正的登录动作静默获取code并传给云函数 async handleLogin() { try { const loginRes await wx.login(); const cloudRes await wx.cloud.callFunction({ name: login, data: { code: loginRes.code } }); // 云函数返回 { openid, session_key, unionid如有 } wx.setStorageSync(loginInfo, cloudRes.result); this.setData({ hasLogin: true }); } catch (err) { console.error(登录失败, err); wx.showToast({ title: 登录失败请重试, icon: none }); } } });参数说明wx.login()返回的code有效期5分钟必须立即传给服务端cloudRes.result.openid是后续所有接口鉴权的唯一依据绝不使用前端传来的任何用户输入作为身份标识。session_key仅用于解密敏感数据如手机号不参与业务逻辑。2.3 云函数getAvailableSchedules号源查询必须带缓存穿透防护号源页是流量高峰入口但直接查库会导致DB雪崩。我们采用「本地缓存 缓存标记 数据库兜底」三级策略// cloudfunctions/getAvailableSchedules/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); exports.main async (event, context) { const { date, departmentId } event; const wxContext cloud.getWXContext(); // 1. 先查Redis缓存此处用云开发数据库模拟缓存生产环境替换为Redis const cacheKey schedules:${date}:${departmentId}; const cacheRes await cloud.database().collection(cache).doc(cacheKey).get(); if (cacheRes.data cacheRes.data.value) { return { code: 0, data: JSON.parse(cacheRes.data.value) }; } // 2. 缓存未命中查MySQL通过云调用或自建API网关 // 此处省略具体数据库连接代码重点看SQL逻辑 /* SELECT ds.id, ds.doctor_id, d.name as doctor_name, ds.total_quota - ds.used_quota as remaining_quota, ds.status FROM doctor_schedule ds JOIN doctor d ON ds.doctor_id d.id WHERE ds.date ? AND d.department_id ? AND ds.status 1 AND ds.total_quota ds.used_quota ORDER BY ds.created_at DESC */ // 3. 查询结果写入缓存设置5分钟过期 await cloud.database().collection(cache).doc(cacheKey).set({ data: { value: JSON.stringify(result), expireAt: new Date(Date.now() 5 * 60 * 1000) } }); return { code: 0, data: result }; };关键逻辑缓存键schedules:${date}:${departmentId}确保按日期和科室维度隔离remaining_quota字段由SQL直接计算避免应用层二次处理缓存写入时强制设置过期时间防止脏数据长期滞留。2.4 云函数createAppointment挂号动作必须是原子化数据库事务这是整个系统最核心的函数。它必须保证号源扣减、订单创建、通知触发三者要么全成功要么全回滚。云开发数据库不支持跨集合事务因此我们选择「MySQL原生事务 云函数代理」// cloudfunctions/createAppointment/index.js const mysql require(mysql2/promise); exports.main async (event, context) { const { scheduleId, userId } event; const connection await mysql.createConnection({ host: your-rds-host, user: your-user, password: your-pass, database: hospital_db }); try { await connection.beginTransaction(); // 步骤1乐观锁扣减号源关键 const [updateRes] await connection.execute( UPDATE doctor_schedule SET used_quota used_quota 1, updated_at NOW() WHERE id ? AND used_quota total_quota, [scheduleId] ); if (updateRes.affectedRows 0) { throw new Error(号源已满请刷新重试); } // 步骤2创建挂号订单 const orderNo APPT${Date.now()}${Math.floor(Math.random() * 1000)}; await connection.execute( INSERT INTO appointment_order (order_no, user_openid, schedule_id, status, version) VALUES (?, ?, ?, 1, 0), [orderNo, userId, scheduleId] ); // 步骤3记录操作日志非核心但审计必需 await connection.execute( INSERT INTO operation_log (type, target_id, operator, created_at) VALUES (APPOINTMENT, ?, ?, NOW()), [scheduleId, userId] ); await connection.commit(); return { code: 0, orderNo }; } catch (err) { await connection.rollback(); console.error(挂号事务失败, err); throw err; } finally { await connection.end(); } };血泪经验UPDATE ... WHERE used_quota total_quota这一行是防超挂的生命线。如果写成WHERE id ?而不校验库存高并发下必然超挂。affectedRows 0的判断必须放在事务内否则回滚失效。3. 避坑指南挂号系统上线前必须验证的5个致命问题3.1 现象用户点击“预约”按钮后无响应控制台无报错原因云函数createAppointment的timeout设置过短默认5秒而MySQL事务在高负载时可能超过该阈值导致云函数被强制终止但数据库事务已部分提交号源扣减了订单没创建。解决在云函数配置中将超时时间设为30秒并在代码中增加connection.config.connectTimeout 10000同时在前端增加 loading 状态和超时提示“挂号中请勿重复点击”。3.2 现象同一医生同一时段显示剩余号源为2但两个用户同时点击预约后都收到“挂号成功”通知原因号源查询接口getAvailableSchedules返回的是快照数据未加读锁而扣减操作createAppointment的UPDATE语句未使用SELECT ... FOR UPDATE显式加锁。解决在事务开始后、UPDATE前先执行SELECT * FROM doctor_schedule WHERE id ? FOR UPDATE。注意FOR UPDATE必须在事务内且不能有其他未提交的DML操作否则会死锁。3.3 现象用户退号后号源数量未恢复或恢复延迟超过1分钟原因退号逻辑未走同一事务路径而是单独调用updateScheduleQuota云函数且未校验原订单状态如已过期订单仍允许退号。解决退号必须复用createAppointment的逆向逻辑——在事务中UPDATE doctor_schedule SET used_quota used_quota - 1 WHERE id ? AND used_quota 0并同步更新订单状态为2已取消。额外增加状态校验WHERE status IN (0,1)。3.4 现象小程序在iOS真机上无法获取wx.login的code安卓正常原因iOS微信客户端对wx.login的调用有更严格的上下文要求——必须在用户主动触发的事件回调中如button的bindtap且不能在setTimeout或异步回调中调用。解决确保wx.login()调用栈直接来自用户点击事件禁用任何中间 Promise 包装若需延时改用wx.nextTick。3.5 现象数据库中出现大量status0待支付的订单但用户从未进入支付页原因挂号成功后跳转支付页的逻辑写在前端若网络抖动或用户手动关闭页面支付页未加载订单就永远卡在待支付状态。解决挂号成功后云函数立即发起统一下单调用微信支付unifiedorder接口返回prepay_id给前端调起支付同时启动定时任务如每5分钟扫描status0 AND created_at NOW()-300的订单自动关闭并释放号源。4. 号源动态调控用“号段池”机制应对临时加号与停诊真实医院场景中医生临时加号、突发停诊、系统维护等事件频发。硬编码号源数量或依赖人工后台修改会导致前端展示滞后、用户投诉激增。我们引入「号段池Slot Pool」概念将号源抽象为可动态分配的数字区间而非固定总量。4.1 数据库新增号段表解耦号源与排班-- 号段池表一个排班可对应多个号段支持分时段、分类型号源 CREATE TABLE schedule_slot ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, schedule_id BIGINT UNSIGNED NOT NULL COMMENT 关联排班ID, slot_type TINYINT NOT NULL DEFAULT 1 COMMENT 1普通号,2专家号,3特需号, start_no INT NOT NULL COMMENT 起始号, end_no INT NOT NULL COMMENT 结束号, status TINYINT NOT NULL DEFAULT 1 COMMENT 0禁用,1启用, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_schedule_type (schedule_id, slot_type) ); -- 挂号订单关联号段记录具体挂到哪个号 ALTER TABLE appointment_order ADD COLUMN slot_id BIGINT UNSIGNED NULL COMMENT 关联号段ID, ADD COLUMN slot_no INT NULL COMMENT 具体挂号序号;为什么有效当医生临时加号时运维只需向schedule_slot插入一条新记录如start_no51, end_no60无需修改doctor_schedule.total_quota前端查询号源时getAvailableSchedules改为聚合schedule_slot表中status1的所有号段总数实时性毫秒级。4.2 云函数assignSlotNo挂号时从号段池原子化分配具体序号用户挂号不再只是“扣减一个数”而是从可用号段中分配一个未被占用的具体序号这为后续短信通知、叫号屏对接提供唯一标识// cloudfunctions/assignSlotNo/index.js exports.main async (event, context) { const { scheduleId } event; const connection await getMysqlConnection(); try { await connection.beginTransaction(); // 查找第一个有空闲号的启用号段 const [slotRes] await connection.execute( SELECT id, start_no, end_no FROM schedule_slot WHERE schedule_id ? AND status 1 ORDER BY id LIMIT 1, [scheduleId] ); if (!slotRes.length) throw new Error(无可用号段); const slot slotRes[0]; // 关键用SELECT ... FOR UPDATE锁定该号段再查当前最大已用号 const [maxUsedRes] await connection.execute( SELECT COALESCE(MAX(slot_no), 0) as max_no FROM appointment_order WHERE slot_id ? FOR UPDATE, [slot.id] ); const nextNo maxUsedRes[0].max_no 1; if (nextNo slot.end_no) { throw new Error(当前号段已满请稍后重试); } // 分配成功返回具体号 return { code: 0, slotId: slot.id, slotNo: nextNo }; } catch (err) { await connection.rollback(); throw err; } finally { await connection.end(); } };参数说明COALESCE(MAX(slot_no), 0)处理号段首次使用场景FOR UPDATE锁定appointment_order表中该号段的所有行确保nextNo分配不重复返回的slotNo可直接用于生成挂号凭证如“张医生-05月20日-087号”。4.3 前端展示优化用“号段可视化”提升用户信任感传统“剩余2个号”表述模糊。我们改为展示具体号段范围并高亮已挂出号!-- wxml 片段 -- view classslot-display text classslot-label今日号源/text view classslot-range001-050/view view classslot-used已挂003, 012, 027, 041/view view classslot-remaining剩余46个/view /view/* wxss */ .slot-range { color: #1aad19; font-weight: bold; } .slot-used { color: #999; font-size: 12px; margin-top: 4px; } .slot-remaining { color: #e64340; margin-top: 6px; }效果用户一眼看到“001-050”总范围再看到“已挂003,012…”具体号心理预期明确减少因“剩余2个”却抢不到而产生的质疑。该数据由getAvailableSchedules云函数聚合schedule_slot和appointment_order表后返回前端仅做渲染。5. 论文级验证用JMeter压测MySQL慢查询日志定位性能瓶颈毕设或项目交付不能只说“系统稳定”必须用数据证明。我们用标准压测流程验证挂号核心链路5.1 构建可复现的压测场景JMeter 5.4.1参数值说明线程数用户200模拟200人并发挂号Ramp-Up 时间10秒10秒内逐步加压避免瞬间冲击循环次数1每个用户只执行1次挂号流程HTTP请求POST /cloudfunctions/createAppointment云函数URLHeader带X-WX-SOURCE: wechat关键配置在HTTP Header Manager中添加Content-Type: application/jsonBody Data使用JSON格式传参{scheduleId:123,userId:oAbcDefGhiJklMnOpQrStUvWxYz}。压测前确保数据库连接池如Druid最大连接数 ≥ 300。5.2 MySQL慢查询日志分析揪出隐藏的索引缺失开启慢查询日志后执行压测再用mysqldumpslow分析# 开启慢查询MySQL 5.7 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 0.1; # 记录超过100ms的查询 SET GLOBAL log_queries_not_using_indexes ON; # 压测结束后分析 mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log典型输出Count: 123 Time0.45s (55s) Lock0.00s (0s) Rows1.0 (123), root[root]localhost SELECT * FROM doctor_schedule WHERE id N AND used_quota total_quota问题定位WHERE id ? AND used_quota total_quota未命中索引。id是主键但used_quota total_quota是范围查询若无复合索引会全表扫描。解决添加复合索引ALTER TABLE doctor_schedule ADD INDEX idx_id_quota_check (id, used_quota, total_quota);验证效果压测TPS从120提升至380平均响应时间从420ms降至85ms。索引生效后EXPLAIN显示typerefkeyidx_id_quota_check。5.3 并发冲突率实测用“挂号成功率”替代模糊描述很多方案只说“支持高并发”但不给出具体数字。我们在压测中统计两个关键指标指标计算公式合格线实测值挂号成功率(成功订单数 / 总请求) × 100%≥99.5%99.72%超挂发生率(超挂订单数 / 成功订单数) × 100%0%0%如何定义“超挂”在压测脚本中对每个成功返回的订单号立即查询数据库SELECT COUNT(*) FROM appointment_order WHERE schedule_id ? AND status 1若结果 doctor_schedule.total_quota则记为一次超挂。实测中该值恒为0证明事务控制有效。5.4 我的压测习惯每次上线前必跑的3个检查清单号源一致性检查压测后执行SELECT ds.id, ds.total_quota, ds.used_quota, COUNT(ao.id) as actual_used FROM doctor_schedule ds LEFT JOIN appointment_order ao ON ds.id ao.schedule_id AND ao.status 1 GROUP BY ds.id HAVING ds.used_quota ! actual_used—— 若有结果说明事务未完全闭环。连接池泄漏检查压测中监控MySQLThreads_connected若持续上升不回落说明云函数中connection.end()未执行常因异常未被捕获。缓存击穿验证手动删除cache表中某条号源缓存再用单用户快速刷新10次观察数据库QPS是否突增5倍以上——若是则需加强缓存空值或布隆过滤器。希望帮到你。本文还有配套的精品资源点击获取