微信小程序预约挂号系统毕设开发:数据库设计与并发控制全攻略
简介一份基于微信小程序的预约挂号系统毕业设计项目前台采用微信开发者工具实现后台基于 Java SSM 框架与 MySQL 数据库覆盖管理员、医生、用户三类角色包含用户管理、医生信息、排班、预约、取消预约与调班申请等完整业务流程适合计算机相关专业课程设计、毕业设计及小程序开发学习者参考。压缩包共 1210 个文件约 18.92MB其中 png、jpg 为界面与素材图java、xml、properties 为后端源码与配置vue、js、json 为后台管理页面逻辑wxml、wxss 为小程序页面结构另含 sql 数据库脚本、bat 一键安装运行脚本及说明文档便于本地快速部署。已有 77 人学习浏览资源目录组织清晰从后端接口到小程序端页面均有完整代码可供二次开发或直接作为毕设演示项目也可帮助理解 SSM 框架与小程序前后端配合的典型实现。1. 从窗口排队到小程序秒级挂号这个毕设为什么值得做打开任何一家三甲医院的预约应用高峰期里你能看到的只有加载转圈患者要反复切换科室才能确认某个医生今天是否放号。预约挂号系统这个毕设选题本质上是把一个典型业务场景搬进微信小程序——用户选科室、看医生、选时段、锁定号源、提交订单一套流程闭环下来既覆盖了完整的前后端交互又自然带出了并发控制、事务一致性和微信生态适配这类评审老师爱问的点。这篇笔记按「需求建模 → 数据库设计 → 小程序端 → 后端并发 → 部署验收」的顺序把基于微信小程序的预约挂号系统拆成可照做的完整路线。它适合正在开题或写代码卡壳的计算机专业学生也适合想快速跑通医疗预约流程的开发者。2. 预约挂号系统从需求到建表先把号源模型立住2.1 三个角色两套流程把“预约”拆成可落地的业务规则做预约挂号系统有个常见误区上来就画页面写接口结果做到一半发现医生出诊表要不要可配置、患者取消后号源能不能释放这类问题都没想清楚。我的经验是先把角色和流程钉死后面的数据和代码才有主心骨。系统可以按三个角色拆患者通过小程序浏览科室和医生、选择就诊日期及时段、提交预约并支持取消医生只需要看自己某个日期的预约列表知道自己哪几个时段被占满毕设里一般不给医生开放小程序做管理后台即可管理员负责维护科室、医生、出诊时段和号源总量也承担查看订单统计、处理异常预约的职责。两套主流程分别是「预约挂号」和「取消预约」它们共享同一个核心资源——号源Schedule。设计业务规则时有三个点最好提前定死否则后面每改一处都要动表结构。第一每个医生按日期和时段生成号源比如某个医生上午 8:00-9:00 放 10 个号而不是只存一个整天总量第二取消预约有时间限制主流做法是就诊当天的号不允许线上取消前一天 24 点前可取消第三患者在同一时段只能有一条有效预约避免同一人占多个号。这三条规则直接决定了下文的表字段和状态机。2.2 五张核心表的DDL科室、医生、患者、号源、订单预约挂号系统的数据库设计并不复杂但要想让评审老师挑不出毛病五张核心表的关系要清晰科室一对多医生号源落在医生维度订单关联患者和号源。下面这组建表语句是我在同类项目里反复用的基础版本字段做了精简直接能跑在最常见的 MySQL 5.7/8.0 上。-- 科室表 CREATE TABLE t_department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL COMMENT 科室编码如 surgery-01, name VARCHAR(64) NOT NULL COMMENT 科室名称, intro TEXT COMMENT 科室简介, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序权重越小越靠前, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_code (code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT科室表; -- 医生表 CREATE TABLE t_doctor ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_id BIGINT NOT NULL COMMENT 所属科室ID, name VARCHAR(32) NOT NULL COMMENT 医生姓名, title VARCHAR(32) COMMENT 职称主任医师/副主任医师/主治医师, avatar VARCHAR(255) COMMENT 头像URL, specialty VARCHAR(255) COMMENT 擅长领域, reg_fee DECIMAL(8,2) NOT NULL DEFAULT 0 COMMENT 挂号费, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生表;继续看患者表、号源表和订单表这三张的约束条件才是本项目的核心。患者表围绕微信 openid 建立唯一键一个微信用户对应一条患者记录号源表把「医生 出诊日期 时段」三个维度组成唯一键并用total_count与used_count记录配额和已用量订单表则通过唯一键把同患者同号源的有效预约挡在数据层之外。-- 患者表一个微信用户一条记录 CREATE TABLE t_patient ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(64) COMMENT 微信昵称, real_name VARCHAR(32) COMMENT 患者真实姓名, phone VARCHAR(20) COMMENT 手机号, id_card VARCHAR(32) COMMENT 身份证号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者表; -- 号源表医生在某个日期的某个时段放了多少号 CREATE TABLE t_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT 医生ID, dept_id BIGINT NOT NULL COMMENT 科室ID冗余便于按科室查, visit_date DATE NOT NULL COMMENT 出诊日期, start_time VARCHAR(8) NOT NULL COMMENT 时段开始 HH:mm, end_time VARCHAR(8) NOT NULL COMMENT 时段结束, total_count INT NOT NULL DEFAULT 10 COMMENT 号源总数, used_count INT NOT NULL DEFAULT 0 COMMENT 已约数量, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可约 1约满 2停诊, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_doctor_date (doctor_id, visit_date, start_time), KEY idx_date (visit_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT号源表; -- 预约订单表 CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL COMMENT 业务订单号, patient_id BIGINT NOT NULL COMMENT 患者ID, schedule_id BIGINT NOT NULL COMMENT 号源ID, doctor_name VARCHAR(32) COMMENT 冗余医生姓名, dept_name VARCHAR(64) COMMENT 冗余科室名, visit_date DATE NOT NULL COMMENT 就诊日期, visit_time VARCHAR(16) COMMENT 就诊时段如08:00-09:00, fee DECIMAL(8,2) NOT NULL DEFAULT 0 COMMENT 挂号费, status TINYINT NOT NULL DEFAULT 0 COMMENT 0已预约 1已取消 2已完成 3已爽约, cancel_reason VARCHAR(255) COMMENT 取消原因, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, cancel_time DATETIME COMMENT 取消时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), UNIQUE KEY uk_patient_schedule (patient_id, schedule_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;字段和索引有几个地方需要说明。uk_patient_schedule这个唯一键本意是防止同一患者重复预约同一号源但它有个副作用患者取消预约后这一对patient_id, schedule_id仍然存在想要再次预约同号源会插入失败。因此实际开发中我一般会去掉这个唯一索引改到 Service 层用条件判断后面第 5 章会讲具体做法。version是为乐观锁预留的用来防超卖第 4 章会用到。订单表里冗余doctor_name和dept_name是刻意的列表页和详情页不需要每次都连表查医生和科室这是毕设里性价比很高的优化。2.3 号源状态机与预约链路可约、约满、取消如何流转数据库表设计好还需要把状态流转讲清楚。状态机是答辩时的高频问题我可以直接给一套可用的定义。号源状态只有三种0 可约、1 约满、2 停诊。初始是0当used_count total_count时状态置为1管理员维护排班时可手动把某时段置为2停诊。订单状态则复杂一些0 已预约、1 已取消、2 已完成、3 已爽约。预约创建后状态为0患者在规定时间内取消则进入1同时号源的used_count减一状态从约满回到可约就诊日期过去且状态仍为0的由定时任务批量更新为3 已爽约。整条预约链路可以概括为一个受事务保护的动作序列患者选号源 → 后端校验号源可约且患者无冲突订单 → 原子扣减号源used_count→ 插入订单表 → 返回订单号。这里有个顺序决策先扣号源再插订单可以保证即使后续插订单失败事务回滚后号源恢复如果反过来先插订单再扣号源则有可能出现订单存在但号源没扣上。两条路都能靠事务兜底但先扣号源更符合直觉排查时也不容易出歧义。3. 小程序端跑通预约链路wx.login、列表加载与下单3.1 用wx.login()换用户身份新版登录接口的正确接法小程序端的用户体系不同于网页核心是微信提供的wx.login()获取临时code后端拿这个 code 调用微信接口换取 openid 和 session_key。毕设里不需要做太复杂的账号密码体系把 openid 作为患者表唯一键即可。需要注意老教程里常见的wx.getUserInfo()弹窗授权在基础库 2.27.1 之后已经被微信限制主动弹窗拿不到昵称头像。现在的规范做法是用button的open-typechooseAvatar让用户主动选头像用input typenickname填写昵称。所以登录页一般拆两步先静默登录拿到 openid再让用户自愿补充头像昵称。// pages/login/login.js const app getApp(); Page({ data: { requesting: false }, onLoad() { // 先看本地 token 还在不在避免每次进小程序都走 wx.login const token wx.getStorageSync(token); if (token) { wx.switchTab({ url: /pages/index/index }); return; } }, handleLogin() { if (this.data.requesting) return; this.setData({ requesting: true }); wx.login({ success: async (res) { if (!res.code) { wx.showToast({ title: 登录失败, icon: none }); this.setData({ requesting: false }); return; } try { const result await app.request(/api/login, { method: POST, data: { code: res.code } }); if (result.code 0) { wx.setStorageSync(token, result.data.token); wx.switchTab({ url: /pages/index/index }); } else { wx.showToast({ title: result.msg, icon: none }); } } finally { this.setData({ requesting: false }); } }, fail: () { this.setData({ requesting: false }); wx.showToast({ title: 登录失败请重试, icon: none }); } }); } });代码里有两个容易忽略的参数点。requesting标志位是为了防止用户连续点击登录按钮造成重复请求wx.login失败时res.code不存在必须提前return否则后端会收到一个空的 code。app.request是封装好的 Promise 版wx.request它要统一做三件事拼接基础域名、往 header 里塞 token、把 HTTP 非 200 状态统一转成业务错误提示。后端/api/login的职责是拿 code 换 openid然后为该 openid 查找或创建患者记录最后生成一个自定义 token 返回。实际的 code2Session 请求应当由后端发出涉及appid和secret的代码绝不能写进小程序前端包否则反编译后就会泄露密钥。3.2 科室与医生列表分页加载和顶部导航栏高度适配预约首页一般展示科室卡片点进科室后是医生列表再点医生进入排班页。列表的常规做法是分页加载onReachBottom触底时请求下一页配合loading状态防止重复触发。// pages/doctor/list.js Page({ data: { deptId: , list: [], page: 1, pageSize: 10, hasMore: true, loading: false }, async onLoad(options) { this.setData({ deptId: options.deptId }); await this.loadList(1, true); }, // 触底加载下一页 async onReachBottom() { if (!this.data.hasMore || this.data.loading) return; await this.loadList(this.data.page 1, false); }, async onPullDownRefresh() { await this.loadList(1, true); wx.stopPullDownRefresh(); }, async loadList(page, reset) { this.setData({ loading: true }); try { const res await app.request(/api/doctor/list, { data: { deptId: this.data.deptId, page, pageSize: this.data.pageSize } }); const rows res.data.list || []; this.setData({ list: reset ? rows : this.data.list.concat(rows), page, hasMore: page * this.data.pageSize res.data.total }); } finally { this.setData({ loading: false }); } } });这里的关键参数是hasMore的计算用page * pageSize total判断是否还有下一页。新手常犯的错误是在onReachBottom里累加page后再请求结果快速滚动时连续触发多次出现列表数据重复。配合loading标志位可以挡住绝大部分重复请求。页面顶部导航栏的高度适配也是列表页常见的坑。自定义导航栏时wx.getWindowInfo()拿到的statusBarHeight是状态栏高度胶囊按钮的位置要用wx.getMenuButtonBoundingClientRect()获取两者相加才是导航栏实际高度。如果直接写死 44px在 iPhone 和 Android 真机上会出现标题偏上或偏下视觉上非常明显。毕设阶段更推荐用默认导航栏省掉这套适配但答辩时若能说清这两个 API 的差异能加分不少。3.3 预约下单页日期选择、号源锁定与防止二次提交预约下单页是整个小程序端最核心的交互。患者选择就诊日期后小程序向后端请求该日期下该医生的号源列表用户点选某个时段再点「确认预约」提交。这里需要在前端做好两件事日期范围限制只能选从今天起未来 7~14 天、提交按钮的防重复点击。// pages/booking/confirm.js const app getApp(); Page({ data: { doctorId: , doctor: null, dates: [], selectedDate: , scheduleList: [], selectedScheduleId: null, submitting: false }, onLoad(options) { this.setData({ doctorId: options.doctorId, dates: this.buildDates(7) }); }, // 生成未来7天的可选日期 buildDates(days) { const list []; const now new Date(); for (let i 0; i days; i) { const d new Date(now.getTime() i * 86400 * 1000); const month d.getMonth() 1; const day d.getDate(); list.push({ label: ${month}月${day}日, value: ${d.getFullYear()}-${String(month).padStart(2, 0)}-${String(day).padStart(2, 0)} }); } return list; }, async chooseDate(e) { const date e.currentTarget.dataset.date; this.setData({ selectedDate: date, selectedScheduleId: null }); const res await app.request(/api/schedule/list, { data: { doctorId: this.data.doctorId, date } }); const schedules (res.data || []).map(s ({ ...s, displayTime: ${s.startTime} ~ ${s.endTime}, remaining: s.totalCount - s.usedCount })); this.setData({ scheduleList: schedules }); }, chooseSchedule(e) { this.setData({ selectedScheduleId: e.currentTarget.dataset.id }); }, async submitOrder() { if (this.data.submitting) return; const { selectedScheduleId, doctorId } this.data; if (!selectedScheduleId) { wx.showToast({ title: 请先选择就诊时段, icon: none }); return; } this.setData({ submitting: true }); try { const res await app.request(/api/order/create, { method: POST, data: { doctorId, scheduleId: selectedScheduleId } }); if (res.code 0) { wx.redirectTo({ url: /pages/order/detail?id${res.data.orderId} }); } else { wx.showToast({ title: res.msg, icon: none }); // 预约失败刷新当前号源供用户重选 this.chooseDate({ currentTarget: { dataset: { date: this.data.selectedDate } } }); } } finally { this.setData({ submitting: false }); } } });submitting的防重复逻辑要放在请求之前判断且在finally中复位这样即使接口报错也能恢复点击。号源剩余量remaining在前端计算展示但提交时不要传remaining后端只认scheduleId最终余量必须以后端为准否则趁手改成前端的数字就毫无意义。另外页面离开时需要清理状态。如果用户在预约中途切走再次回来自动刷新号源列表避免看到已过期的余量。这个动作放在onShow里触发比较稳妥比在onLoad里只请求一次更符合真实场景。4. 后端接口与并发控制号源扣减怎样做到不超卖4.1 预约业务最少需要哪六个接口小程序端页面跑通之后后端接口的职责就很清晰了。整套系统最少需要六个接口支撑核心流程其他统计、管理功能可以绕后补。接口方法入参出参说明/api/loginPOST{ code }{ token, userInfo }登录换 token/api/department/listGET无[{ id, name, intro }]科室列表/api/doctor/listGETdeptId, page, pageSize{ total, list }分页医生列表/api/schedule/listGETdoctorId, date[{ id, startTime, endTime, totalCount, usedCount }]某医生某天的时段列表/api/order/createPOST{ scheduleId }{ orderId, orderNo }创建预约/api/order/cancelPOST{ orderId }无取消预约这六个接口里前四个都是查表操作压力不大真正考验设计的是后两个写接口。/api/order/create要同时完成校验、扣减号源、创建订单三步/api/order/cancel则要完成状态校验、更改订单、释放号源三步。两者都必须放在数据库事务里执行并且要处理并发场景。接口路径和参数名建议与微信小程序端保持一致。这个app.request封装会默认带上 token后端的拦截器对除/api/login之外的接口统一校验身份这样每个业务接口里不需要重复解析患者身份直接从当前登录上下文里取patientId即可。4.2 先查再插的危险与原子扣减用UPDATE控制号源预约系统最容易翻车的地方是并发超卖。假设某个号源总量是 10当前used_count是 9此时两个患者同时发起预约请求。如果代码写成「先查 used_count小于 total_count 就插入订单」两个请求都会读到 9然后各自把used_count 1数据库里最终结果是 11超卖一个号。正确思路是把扣减这一步做成原子的条件更新。MySQL 的UPDATE语句本身是行级原子操作只要把判断条件写进WHERE子句就能挡掉超卖UPDATE t_schedule SET used_count used_count 1, version version 1 WHERE id #{scheduleId} AND used_count total_count;执行完这条语句后通过 JDBC 的int rows update(...)判断受影响行数。rows 1说明扣减成功可以继续插订单rows 0说明号源已经被抢完直接抛出业务异常。这一步利用的是数据库行锁两个并发请求同时对同一行做条件更新时后一个会等前一个提交后才执行此时used_count已经变大条件不满足自然失败。在此基础上Service 层还需要处理「同一患者重复预约同一时段」的校验。可以这样组织事务方法Service public class OrderService { Resource private ScheduleMapper scheduleMapper; Resource private OrderMapper orderMapper; Transactional(rollbackFor Exception.class) public OrderCreateResult create(Long patientId, Long scheduleId) { // 1. 检查患者是否已有同日期有效预约 ScheduleEntity schedule scheduleMapper.selectById(scheduleId); if (schedule null) { throw new BizException(号源不存在); } if (schedule.getStatus() 1 || schedule.getStatus() 2) { throw new BizException(该时段不可预约); } OrderEntity exists orderMapper.selectActiveByPatientAndDate(patientId, schedule.getVisitDate()); if (exists ! null) { throw new BizException(您已预约该日期的时段); } // 2. 原子扣减号源 int rows scheduleMapper.decreaseCount(scheduleId); if (rows 0) { throw new BizException(号源已被约完请更换时段); } // 3. 创建订单 OrderEntity order new OrderEntity(); order.setOrderNo(generateOrderNo()); order.setPatientId(patientId); order.setScheduleId(scheduleId); order.setDoctorName(schedule.getDoctorName()); order.setDeptName(schedule.getDeptName()); order.setVisitDate(schedule.getVisitDate()); order.setVisitTime(schedule.getStartTime() - schedule.getEndTime()); order.setFee(schedule.getRegFee()); order.setStatus(0); orderMapper.insert(order); return new OrderCreateResult(order.getId(), order.getOrderNo()); } }注意事务方法的粒度校验、扣减、插入必须在同一个事务内。decreaseCount对应上面那条条件 UPDATE它的 Mapper XML 里除了WHERE id ? AND used_count total_count还可以加上AND status 0把停诊的号源也挡在外面。异常必须抛运行时异常事务才能回滚如果捕获后吞掉就会出现「订单没插进去但号源被扣掉」的数据不一致。version字段在条件更新里虽然存在但没用到这是因为used_count total_count已经具备并发的判断能力。保留version是给答辩时讲乐观锁思路用的如果评审老师问「怎么防并发超卖」你可以说条件更新是数据库行的原子自增也可以再加一句更严密的版本号方案是把version #{oldVersion}写进 WHERE更新失败则重试。两者选一个讲透即可不必都实现。4.3 取消预约与号源释放事务里的行锁与回滚边界取消预约比创建容易踩坑因为它涉及两个表的写操作订单状态改成已取消号源used_count减一。如果这两个操作不在事务里一旦释放号源失败患者取消了但号源没有恢复其他患者就少一个号。创建预约时不需要显式加行锁因为条件更新本身锁住了那一行但取消预约必须先读取订单状态再更新这里建议用悲观锁防止并发取消和并发改状态打架。示例代码如下Transactional(rollbackFor Exception.class) public void cancel(Long patientId, Long orderId) { // 1. 行锁锁定订单防止并发操作同一条订单 OrderEntity order orderMapper.selectByIdForUpdate(orderId); if (order null || !order.getPatientId().equals(patientId)) { throw new BizException(订单不存在); } if (order.getStatus() ! 0) { throw new BizException(当前状态不可取消); } LocalDate visitDate order.getVisitDate().toLocalDate(); LocalDate today LocalDate.now(); if (visitDate.isBefore(today.plusDays(1))) { throw new BizException(就诊前一天起不可线上取消); } // 2. 更新订单状态为已取消 orderMapper.updateStatus(orderId, 1, LocalDateTime.now()); // 3. 释放号源 scheduleMapper.releaseCount(order.getScheduleId()); }-- 释放号源注意用 GREATEST 防止 used_count 被减成负数 UPDATE t_schedule SET used_count GREATEST(used_count - 1, 0) WHERE id #{scheduleId};selectByIdForUpdate是SELECT ... FOR UPDATE的 Mapper 封装它会把订单行锁住直到事务提交。锁的目的是让相同的订单在同一时间只有一个取消请求进去后面的请求等锁释放后读到状态已经是 1直接抛出不可取消。GREATEST是为了防止由于数据异常导致used_count减成负数属于防御性写法。这个事务的回滚边界值得多写一句如果第 3 步释放失败抛出异常前两步也会自动回滚订单保持已预约状态号源未释放数据保持一致。所以千万不要在 Service 里 try-catch 后把异常吞掉一定要让事务感知到异常。毕设评审时这个细节比接口本身更容易被追问。5. 预约挂号系统常见问题与避坑指南5.1 真机环境下所有请求失败合法域名与HTTPS没配现象小程序在微信开发者工具里一切正常一扫码在真机上打开所有接口都报request:fail url not in domain list。原因开发工具默认勾选了「不校验合法域名」接口随便个http://localhost:8080都能通真机上微信强制要求请求地址是 HTTPS 且在后台配置过合法域名。解决开发阶段在开发者工具「详情 → 本地设置」勾选「不校验合法域名」同时把真机调试的「不校验域名」打开可以先用http://192.168.x.x:8080联调提审前去微信公众平台配置 request 合法域名并给服务器配上 HTTPS 证书。这条几乎是每个小程序毕设的必经之坑提前在文档里写清楚评审演示时才不会白屏。5.2 快速双击提交同一时段幂等控制缺位现象演示时快速点两次「确认预约」生成了两条相同的预约记录号源被扣两次。原因前端明明加了submitting拦截但真实场景下可能有多个页面入口、接口重试、或网络延迟导致第一次请求还没返回用户又点一次。前端的标志位只能挡正常用户挡不住并发请求。解决分两层处理。前端保留submitting拦截只作为体验优化后端在创建预约的 Service 里做「同患者同日期已有有效预约」的校验也就是第 4 章里selectActiveByPatientAndDate的存在性检查。另外可以给创建接口加一个幂等键参数requestId订单表增加request_id唯一索引重复请求直接插入失败这是最彻底的方案。毕设里做到「后端存在性校验」就够了幂等键可作为扩展加分项。5.3 并发压测时号源超卖条件更新用错或漏掉现象用 JMeter 并发 100 个请求打某个号源的创建接口预约成功数超过了号源总数。原因最常见的两种——一是写成了先查后插没做原子扣减二是条件更新 SQL 写错了比如WHERE used_count total_count或者没带AND used_count total_count。前者并发读旧值后者在高并发下边界条件没挡严。解决统一使用第 4.2 节的条件更新并在 Mapper 里写死WHERE id ? AND used_count total_count。压测前可以先用脚本造一个 total_count10 的号源开 20 个线程同时打断言成功数不超过 10。跑通了这条答辩讲并发控制就稳了。5.4 预览二维码别人打不开体验成员与真机调试现象点工具栏「预览」生成的二维码发给同学对方扫码提示「无法显示页面」或白屏。原因预览二维码只对项目成员和体验成员有效。微信小程序如果没有把所有协作者加进体验成员名单非体验用户扫码就看不到。解决在微信公众平台后台「成员管理 → 体验成员」里添加对方微信号。开发阶段更推荐用「真机调试 2.0」它生成的二维码只需要对方是微信好友就能打开不需要配体验成员迭代也更快。这里要提醒一句体验版能打开不代表正式版能通过审核真正提交需要用具备合法域名的线上接口。5.5 订阅消息只能发一次一次性授权与模板设计现象预约成功后想要给患者发一条「预约成功通知」再发一条「就诊提醒」发现第二次发送失败用户收不到消息。原因微信的订阅消息是「一次性订阅」用户点一次授权按钮只能支撑一次下发。如果让用户在一个页面点了两个不同模板的授权每个模板各自获得一次下发机会但次数并不累加。解决设计模板时把两条消息拆开。预约成功页面弹出wx.requestSubscribeMessage让用户同时勾选「预约成功通知」和「就诊提醒」两个模板后端分别记录两个模板的授权状态每次真正下发一条后该模板的可用次数就消耗掉。如果用户未授权就诊提醒后端可以改在就诊前一天再弹一次授权请求但要克制弹窗太频繁会被微信侧限制。毕设阶段可以只保留预约成功通知一条减少交互复杂度然后把「就诊提醒」作为扩展点写进论文。6. 上线部署与答辩优化三个动作让毕设从“能跑”到“能验收”6.1 小程序上线三件套备案域名、HTTPS证书、合法域名配置拿到一个能跑的本地项目到可以给评审老师演示、甚至发布体验版中间还有三件套要补齐已备案的域名、HTTPS 证书、小程序后台的合法域名配置。购买云服务器后把后端 jar 包部署上再用 Nginx 做反向代理即可server { listen 443 ssl; server_name api.yourdomain.com; ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }确认域名证书生效后到微信公众平台「开发管理 → 开发设置 → 服务器域名」加入https://api.yourdomain.com。配置生效有几分钟延迟改完别急着测。整个环节最耗时的是 ICP 备案正常需要一周左右预留时间要充足这也是开发工具里能跑和交付体验版之间的真正差距。6.2 用Redis扛高并发号源扣减升级思路MySQL 条件更新能扛住毕设演示和百级并发压测但如果想在答辩时讲出层次可以提 Redis 扣减号源的方案把号源余量预热到 Redis用DECR原子自减扣到负数则拒绝预约。下单成功后异步把订单写进数据库Redis 和数据库之间通过定时对账保证一致。这里要注意Redis 方案虽然并发能力更强却引入了缓存与数据库不一致的复杂度毕设中如果不做对账脚本不建议主推。把它作为「后续优化方向」放在论文结尾比强行实现一个半吊子方案更稳妥。6.3 验收演示脚本与数据准备答辩演示最怕现场临时找数据。提前在数据库里准备好两个科室、三位医生、未来三天每个时段 5~10 个号源并准备一个已取消和一个已完成的订单做状态展示。演示动线按「登录 → 选科 → 选医 → 选时段 → 提交预约 → 查看订单 → 取消预约 → 再约」走一遍中途故意演示一次「号源已满」的拦截体现后端校验的存在感。我是第一次做类似选题时把精力全放在写接口上结果答辩前才发现订阅消息发不出去、预览二维码别人扫不开连着两个晚上都在补配置。现在做成方案我再不会漏掉这三件事登录接口和域名配置最先验证真机跑通再开发新功能一切前端展示的号源余量都只当参考以后端返回为准每一张状态表的变化都要在数据库里能回溯到时间点。希望这些从需求建模到部署验证的细节能帮你在这个毕设上少走几段弯路。祝一次过稿希望帮到你。本文还有配套的精品资源点击获取