预约挂号小程序源码解析:号源并发控制与数据库设计
简介这份资源是面向计算机专业毕业设计场景的微信小程序医院预约挂号系统配套文档适合正在准备 Java 方向毕设的本科生与初学者参考。内容围绕微信小程序前端、Spring Boot 后端与 MySQL 数据库展开涵盖用户管理、医生管理、预约挂号、通知提醒等核心模块并讨论了数据安全、高并发处理与系统稳定性等实现要点可帮助读者理解一个完整挂号系统的业务闭环与技术选型思路。压缩包内共 1 个 docx 文件约 883KB以论文式文档形式组织包含摘要、目录及绪论等章节便于直接对照撰写开题报告与毕业设计说明书。目前已有 385 人学习下载可作为毕设选题、系统设计与论文写作的参考素材。1. 预约挂号小程序为什么“号源”比“页面”难做一百倍很多人第一次看医院预约挂号小程序的源码注意力都落在页面上——科室列表、医生卡片、预约按钮觉得这就是个表单提交系统。但真正让这类系统翻车的从来不是界面而是号源。一个医生上午放 20 个号100 个人同时点“预约”谁拿到、谁落空、数据库里那条记录怎么保证不重复、不超卖这才是底层机制的核心。这份毕业设计源码把微信小程序端、后台管理端和 MySQL 数据表串成了一条完整链路正好拿来拆解“预约”这件事从点击到落库的全过程。搞懂它你再看任何带库存、带名额的系统都会是同一套思路。2. 号源模型预约挂号的数据结构为什么长这样2.1 从“医生”到“号源”的实体拆分初学者容易把“医生”和“可预约的号”混成一张表觉得医生表里加个“剩余号数”字段就够了。但真实挂号场景里同一个医生在不同日期、不同时段放号号源是随时间滚动的。常见的实现思路是把实体拆成三层医生doctor描述“谁”排班schedule描述“什么时候出诊、放多少号”预约记录appointment描述“谁在什么时间占了哪个号”。这份源码的数据库设计里科室表 dept、医生信息表、患者信息表、挂号记录表是分开的E-R 图里也能看到科室与医生、患者与挂号之间的一对多关系。这种拆分的动机很直接医生信息会改职称、简介排班每天在变预约记录要留痕三者生命周期不同硬塞进一张表改一个字段就得全表锁。2.2 号源扣减放在哪一层前端、后端还是数据库一个反直觉的结论号源扣减绝不能信任小程序端。小程序页面上的“剩余 3 个号”只是展示快照用户点下去到请求到达服务器之间可能已经过去几百毫秒别人早抢走了。所以扣减必须发生在后端业务层并且要和写预约记录放在同一个事务里。常见的实现思路是后端收到预约请求后先SELECT ... FOR UPDATE锁住对应排班行判断剩余号数大于 0再插入预约记录、更新剩余号数最后提交事务。这份源码用的是 Spring Boot MyBatis 的组合MyBatis 的映射文件里写更新语句Spring 的Transactional管事务边界。下面这段是这类系统里最典型的扣减逻辑我按源码结构还原了关键部分// 预约服务层扣减号源与写记录必须在同一事务 Transactional(rollbackFor Exception.class) public Result book(AppointmentDTO dto) { // 1. 行锁住排班防止并发下多个请求同时读到相同余量 Schedule schedule scheduleMapper.selectForUpdate(dto.getScheduleId()); if (schedule null || schedule.getRemain() 0) { return Result.fail(号源已满); } // 2. 幂等校验同一患者同一排班不允许重复预约 int exist appointmentMapper.countByPatientAndSchedule( dto.getPatientId(), dto.getScheduleId()); if (exist 0) { return Result.fail(请勿重复预约); } // 3. 扣减余量update 带 remain 0 条件做二次兜底 int rows scheduleMapper.decreaseRemain(dto.getScheduleId()); if (rows 0) { return Result.fail(号源已满); } // 4. 写入预约记录状态初始为待就诊 appointmentMapper.insert(buildAppointment(dto)); return Result.ok(); }这段代码里有两个关键点值得说透。第一selectForUpdate对应 SQL 里的SELECT ... FOR UPDATE它在事务内对排班行加排他锁其他事务的相同查询会阻塞直到当前事务提交这是最朴素的悲观锁方案。第二decreaseRemain的 SQL 通常写成UPDATE schedule SET remain remain - 1 WHERE id #{id} AND remain 0用remain 0做条件更新即使锁没生效数据库层面也不会把余量扣成负数。参数上scheduleId定位具体排班patientId用于幂等判断两者缺一不可。失败时看什么如果返回“号源已满”但页面显示还有号多半是展示快照过期如果出现重复预约记录说明幂等校验被绕过或没加唯一索引。2.2 为什么还要在数据库加唯一索引只靠代码里的countByPatientAndSchedule判断是不够的因为两个并发请求可能同时查到 0 条记录然后都插入。血泪经验是在appointment表上建(patient_id, schedule_id)的唯一索引让数据库做最后一道防线。插入冲突时捕获DuplicateKeyException转成“请勿重复预约”的友好提示。这样即使应用层逻辑有漏洞数据也不会脏。这份源码的挂号记录表设计里患者与排班的关联字段就是按这个思路组织的虽然文档没明说索引但这是这类系统合格实现的基本盘。3. 小程序端到后端一次预约请求的完整链路3.1 微信小程序的 MVC 分层与请求封装微信小程序官方框架本身是逻辑层、视图层、基础层三层结构对应.js、.wxml、.wxss文件。这份源码的小程序端把页面逻辑写在.js里通过wx.request调后端接口。常见的实现思路是封装一个统一的请求工具把 baseURL、token、错误提示收口避免每个页面重复写。比如// utils/request.js统一封装请求处理登录态与错误 const BASE_URL https://your-domain/api; function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { content-type: application/json, // token 从本地缓存取登录后写入 Authorization: wx.getStorageSync(token) || }, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.data.code 401) { // 登录态失效跳转登录页 wx.navigateTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }这段封装的价值在于把“登录态失效跳转”“业务错误提示”“网络异常兜底”三件事集中处理。参数上options.url是相对路径data是请求体header里带 token。小程序端个人中心、科室查看、预约挂号这些页面都复用这个工具页面代码只关心业务数据。注意wx.request的域名必须在小程序后台配置合法域名否则真机调试直接失败这是新手最常踩的坑之一。3.2 科室与医生列表的查询路径科室查看页面的交互是左侧科室、右侧医生这种布局对应两次查询先查科室列表再按科室 ID 查医生。后端接口通常是GET /api/dept/list和GET /api/doctor/list?deptIdxx。MyBatis 的映射文件里写SELECT * FROM doctor WHERE dept_id #{deptId}参数就是科室主键。这里有个设计取舍医生列表要不要带排班信息如果带一次查询要 join 排班表数据量小时没问题如果医生多、排班多列表页会变慢。常见的做法是列表页只查医生基本信息点进详情再查该医生未来一周的排班和余量。这份源码的科室内容查看界面和预约挂号界面是分开的正好对应这种“列表轻、详情重”的拆分。3.3 预约提交后的状态流转预约记录不是插进去就完事它有状态待就诊、已就诊、已取消。后台管理端的医生预约管理就是围绕状态做操作。状态流转的底层机制是有限状态机虽然这份源码可能只用几个整数字段表示但设计上要保证非法流转被拒绝比如“已取消”不能直接改成“已就诊”。常见的实现思路是在服务层判断当前状态只允许合法迁移并在更新时带上原状态条件UPDATE appointment SET status 2 WHERE id #{id} AND status 1影响行数为 0 就说明状态已被别人改过返回提示。这种乐观并发控制在后台多人操作时很有用。4. 后台管理端管理员权限与数据维护的底层逻辑4.1 单管理员模型下的权限边界这份源码的用例图里只有一个管理员角色拥有医生管理、科室管理、病患管理、挂号管理、系统管理五类权限。单角色模型实现简单登录后所有菜单可见后端接口用拦截器校验 session 或 token 即可。但要注意权限边界不能只靠前端隐藏菜单后端每个写接口都要校验登录态否则直接构造请求就能改数据。常见的实现思路是 Spring 拦截器里判断请求头 token 是否有效无效返回 401。参数上管理员登录成功后服务端生成 token 存 Redis 或 session前端每次请求带上。这份源码的后台管理界面有六大类内容管理对应不同的 Controller拦截器统一挂在/admin/**路径上。4.2 医生信息管理与前台展示的数据一致性后台改医生信息前台小程序要能看到最新内容这中间的数据一致性靠数据库单点保证后台写库前台读库没有缓存层时天然一致。但一旦加了缓存比如把医生列表放 Redis就要处理更新后失效。这份源码规模小大概率是直连数据库所以不存在这个问题。不过从设计角度看医生信息表里哪些字段前台可见、哪些仅后台可见要有明确约定。比如医生的登录账号、内部备注不该出现在小程序接口的返回体里。常见的做法是后端定义 VO视图对象只把前台需要的字段塞进去而不是直接返回实体。这是防止信息泄露的基本功。4.3 挂号记录的审核与查询后台的医生预约管理要能按医生、按日期、按状态筛选预约记录。SQL 层面就是动态拼接条件MyBatis 的if标签在这里很常用select idselectAppointments resultTypeAppointmentVO SELECT a.id, p.name AS patientName, d.name AS doctorName, a.appoint_date, a.status FROM appointment a JOIN patient p ON a.patient_id p.id JOIN doctor d ON a.doctor_id d.id where if testdoctorId ! nullAND a.doctor_id #{doctorId}/if if teststatus ! nullAND a.status #{status}/if if teststartDate ! nullAND a.appoint_date gt; #{startDate}/if /where ORDER BY a.appoint_date DESC /select这段映射的关键是where自动处理第一个AND避免拼接出WHERE AND的语法错误。参数doctorId、status、startDate都允许为空为空时该条件不生效。查询结果用 VO 承接把患者名、医生名一起带出来减少前端二次请求。注意日期比较用gt;转义这是 XML 里的硬性要求写错直接解析失败。5. 避坑与排查这类挂号系统最容易翻车的地方5.1 号源超卖页面显示有号提交却失败现象是用户看到余量大于 0点预约却提示“号源已满”。原因通常是展示数据是几秒前的快照期间号被抢完或者扣减逻辑没加remain 0条件导致并发下扣成负数后又被修正。解决方式是扣减 SQL 必须带条件更新并在返回失败时刷新页面余量而不是只弹提示。5.2 重复预约同一患者占多个号现象是同一患者对同一排班产生两条记录。原因是幂等校验只在应用层做并发时两个请求都通过校验。解决办法是在appointment表建(patient_id, schedule_id)唯一索引捕获唯一键冲突异常转成友好提示这是最可靠的兜底。5.3 登录态丢失小程序端请求全部 401现象是用户刚登录切个页面就提示未登录。原因多半是 token 存了内存没落本地缓存页面刷新后丢失或者后端 token 过期时间太短。解决方式是把 token 写入wx.setStorageSync请求封装里统一读取后端过期时间按业务合理设置并在 401 时引导重新登录而不是静默失败。5.4 日期时区错乱预约日期差一天现象是用户选 5 号后台显示 4 号。原因是小程序端用new Date()取本地时间后端用数据库默认时区两边不一致。解决方式是前后端统一用yyyy-MM-dd字符串传日期数据库字段用DATE类型避免带时区的时间戳在序列化时偏移。5.5 后台改数据前台不生效缓存或连接池问题现象是后台改了医生简介小程序端还是旧内容。原因可能是查询走了缓存没失效或者多数据源配置下读写不同库。排查时先确认接口返回体是否已更新再看是否有缓存层最后检查数据库连接配置是否指向同一实例。小系统里最常见的是浏览器或小程序开发者工具的缓存清缓存重进即可排除。6. 设计取舍为什么小系统反而要把事务和索引做扎实看完这套源码的结构会发现它技术上并不花哨微信小程序原生框架、Spring Boot、MyBatis、MySQL都是成熟组合。但恰恰是这种“看起来简单”的系统最能暴露设计取舍的分量。号源扣减用悲观锁还是乐观锁取决于并发量日预约量几十的小系统SELECT ... FOR UPDATE足够且直观上千并发时行锁会成为瓶颈才需要考虑 Redis 预扣减加异步落库。这份源码面向毕业设计场景选择事务内行锁加条件更新是成本和正确性的合理平衡。另一个取舍是唯一索引和业务校验的关系。很多人觉得代码里判断过就够了但数据库约束是最后一道后悔药应用层逻辑会随需求改动而出现漏洞索引不会。我一般会这么理解业务校验负责给用户友好提示数据库约束负责保证数据永远不脏两者不是二选一而是都要有。验证这套机制是否真的 work方法很朴素用两个终端同时发预约请求看最终记录数和余量是否对得上把余量改成 1并发提交 10 次只允许成功 1 次手动插一条重复记录看唯一索引是否拦截。这些测试不需要压测工具写个简单脚本循环调接口就能复现。从那以后我每次碰带名额、带库存的功能都会先把“条件更新 唯一索引 事务边界”这三件事过一遍再去看页面长什么样。希望帮到你。本文还有配套的精品资源点击获取