医院预约挂号系统全栈实战:Spring Boot后端+微信小程序前端
做毕设这件事最难的不是把代码跑起来而是把一个“听起来熟悉、做起来陌生”的题目落成一套完整可演示的系统。医院预约挂号就是这类题目的典型代表每个人都在医院排过队知道挂号大概是怎么回事可真要自己从零做一套基于 Spring Boot 后端、微信小程序前端的预约挂号系统时光“号源怎么不超卖”“医患消息怎么互相发”就够卡好几个晚上。这篇博文把我完成这一整套系统的完整路径写出来——从数据库表怎么设计到后端接口怎么写再到小程序端页面如何对接最后到服务器部署会遇到哪些坑全部摊开讲。如果你正在做类似选题的毕设、实训项目或者单纯想搞懂一个带完整业务流程的小程序项目是怎么搭起来的这篇文章可以直接照着走能少走我当初走过的弯路。1. 医院预约挂号系统到底要做什么——从需求到技术选型的一次完整梳理1.1 用户视角下的功能拆解很多同学拿到这类题目就开始建表写代码写着写着发现需求边界模糊这里加一个按钮、那里加一个字段做到最后自己都理不清。我的建议是反向拆解先把自己当成用户把整个就医预约的流程走一遍每个动作就是一个功能模块。站在患者一侧打开这个小程序要能完成几件事查看医院有哪些科室以及科室下的医生出诊信息点进医生详情页能看到医生的职称、擅长方向、本月剩余号源选一个日期和时段上午/下午确认挂号信息并提交在“我的预约”里看到就诊凭证临近就诊前能查看预约状态如果计划有变需要支持取消预约并被系统限制“临近就诊不能取消”更高级一点还需要能直接给医生发起在线咨询消息——这正是标题里“在线医患交互”的落点。站在医生和管理员一侧需要另一套操作界面一般做成后台管理系统或者医生版小程序页面医生能查看自己被预约的记录医生能给患者回复问诊消息、填写诊断建议管理员能维护科室、录入医生、安排一周的排班生成号源管理员能看到挂号量的统计和营收情况。这套功能跑通之后才算是一个“业务闭环”而不是东一个接口西一个接口的碎片代码。做毕设也好、做实训项目也好答辩和演示的时候最怕的就是你给评委看的流程是断着的——患者挂了号却查不到记录、发了消息却没人能回这种断裂会被一眼看穿。1.2 为什么是 Spring Boot 微信小程序这个组合这个组合在近几年的课程设计和毕业设计里出现频率非常高原因并不复杂第一Spring Boot 是目前 Java 后端最容易上手的框架。内嵌 Tomcat、自动装配、起步依赖比起之前用 SSHSpring Struts Hibernate那套配置地狱Spring Boot 把大部分重复性配置都替你做完了。一个 RestController 加几个注解就能出接口CRUD 谁都能快速写出来这让开发者的精力可以集中在业务逻辑上。第二微信小程序是一个“零安装”的 C 端入口。用户不用下载 App、不用注册账号通过微信授权就能拿到身份信息完成登录天然适合医院这种低频、轻量的预约场景。而且微信开发者工具自带模拟器、真机调试、云开发能力前端调试成本很低。第三从设计模式上讲小程序端只负责 UI 渲染和用户交互所有业务逻辑、数据校验都收在 Spring Boot 后端这种前后端分离的架构是当前 Web 应用的主流形态也方便以后扩展更丰富的前端入口。我在实际开发时后端用了 Spring Boot 2.7.x MyBatis Plus MySQL 8.0 JWT小程序端用的原生微信小程序框架没有引入 uni-app 之类的跨端框架管理员端简单加了一个基于 Thymeleaf 或 Vue 的 Web 管理页面。这里要提醒一点如果你的毕设任务书里没有硬性规定必须用某个前端框架原生微信小程序反而是最稳的选择因为跑通了就行不需要额外增加跨端编译的复杂度。2. 数据库设计挂号不冲突才是整个系统的命门2.1 核心表结构与字段说明医院预约挂号系统听起来业务不复杂但数据库设计得好不好直接决定了后面写业务逻辑时顺不顺畅。我把整套系统的数据表规划成八个核心表科室表、医生表、排班表、患者表、预约挂号表、咨询会话表、消息表、管理员表。以排班表为例这是整个系统最关键的表它直接决定了号源能不能被正确释放和扣减。我重点讲一讲核心设计思路。科室表department字段相对简单重点关注这些字段名类型说明idBIGINT主键nameVARCHAR(50)科室名称descriptionTEXT科室简介iconVARCHAR(255)图标地址sort_orderINT排序权重statusTINYINT是否启用医生表doctor则需要在科室中找到归属同时存挂号费因为不同职称的医生挂号费不一样字段名类型说明idBIGINT主键department_idBIGINT所属科室nameVARCHAR(20)医生姓名titleVARCHAR(20)职称主任/副主任/主治specialtyVARCHAR(255)擅长领域avatarVARCHAR(255)头像introductionTEXT简介visit_feeDECIMAL(10,2)挂号费statusTINYINT出诊状态这里有个容易忽略的细节visiting_fee 要冗余在排班表里不要每次挂号时再实时查医生表。因为医院场景下医生可能会调整挂号费但已经生成的号源费用如果跟着变会造成账目问题和患者纠纷。所以排班表里直接冗余一份金额挂号时读排班表的金额这样历史数据才不会乱。排班表schedule是整个系统的核心表字段如下字段名类型说明idBIGINT主键doctor_idBIGINT医生IDvisit_dateDATE出诊日期periodTINYINT1上午 2下午start_timeVARCHAR(5)开始时间end_timeVARCHAR(5)结束时间total_slotsINT总号源数remain_slotsINT剩余号源数visit_feeDECIMAL(10,2)该时段挂号费versionINT版本号用于乐观锁这个表之所以重要是因为所有并发控制都围绕它展开。你可以想象一个热门科室上午 8 点放号瞬间可能有一两百个患者同时点进去如果后端不做防护两张预约记录可能同时读到“剩余 1 个号”最后生成两条预约订单——这就是典型的超卖问题。关于这一部分的解决方案我会在后面“号源控制”那一节专门展开。预约挂号表appointment记录一次真实的挂号行为字段名类型说明idBIGINT主键patient_idBIGINT患者IDschedule_idBIGINT排班IDdoctor_idBIGINT冗余医生department_idBIGINT冗余科室visit_dateDATE就诊日期periodTINYINT上午/下午appointment_codeVARCHAR(32)取号凭证码statusTINYINT0待就诊 1已就诊 2已取消 3已过期cancel_reasonVARCHAR(255)取消原因create_timeDATETIME预约时间2.2 为什么我要把这些字段都设计成“冗余”在上面的表里可能有同学疑惑预约挂号表里明明有 schedule_id为什么还要冗余 doctor_id、department_id、visit_date直接关联查询不行吗这里要理解“读多写少”的业务特性预约记录一旦生成访问频率极高。患者在“我的预约”页面看一次列表同时要显示医生姓名、科室名称、就诊时间段。如果这些信息都靠 join 关联查询一旦数据量大、接口调用频繁数据库的关联压力会变大而且如果有一天医生被删了虽然一般不会做物理删除患者的预约记录就会查出空数据界面直接崩。把关键信息冗余进预约表的另一个好处是哪怕后台排班被调整或医生停诊患者的历史预约记录依然是“当时看到的样子”。这就是典型的“空间换时间”思路在小型项目里非常实用。我在刚开始做的时候也追求“完全规范化”结果为了展示一条预约记录写了三个表的 join后来干脆把冗余字段加上接口简单了不少这也是一个实际开发经验的积累。2.3 在线医患交互的存储模型“在线医患交互”这个功能在标题里占了很大比重所以消息表的存储模型不能掉链子。我采用“会话 消息”的两层结构会话表consultation一个患者和一个医生之间在某一时间段内建立一个会话标记当前状态是“进行中”还是“已结束”消息表consultation_message挂在会话 ID 下区分 sender_type0患者 1医生每条消息有具体内容、消息类型文本/图片和发送时间。为什么需要会话层因为如果直接在患者表和医生表之间存消息查询“我和某个医生之间的所有聊天记录”这个动作会变成一个大范围模糊查询效率低不说代码也难维护。加了一层会话之后消息查询就是“按会话 ID 拉取”——一次精准查询性能干净利落。另外我给每条消息预留了 msg_type 字段虽然第一版只支持文本但后续如果要加图片、语音、系统通知有字段才能平滑扩展。做数据库设计的时候多想一步后面开发时会少改一次表结构。3. 后端 Spring Boot 实现的核心逻辑——从微信登录到号源锁定的完整链路3.1 小程序登录与 JWT 鉴权小程序端的登录和后端 Web 系统的登录有一个本质区别小程序没有传统意义上的“用户名 密码”输入框它通过微信提供的 code2Session 接口换取 openid用 openid 唯一标识一个用户。具体流程我在项目里是这样跑的小程序端调用wx.login()这一步会拿到一个临时的 code这个 code 有效期只有五分钟而且只能用一次小程序把 code 通过请求发给后端/api/auth/login后端拿 code 去请求微信接口https://api.weixin.qq.com/sns/jscode2session换取 openid 和 session_key这个过程中 appid 和 secret 是后端持有的绝对不能暴露在小程序代码里后端根据 openid 在数据库中查找或创建用户记录然后签发一个 JWT token 返回给前端小程序端拿到 token 之后在本地 storage 里存起来后续所有请求都带上这个 token后端通过拦截器验证身份。这里有一个容易踩的坑很多初学者把wx.login()和wx.getUserProfile()搞混。wx.login()解决的是“你是谁”的身份问题它返回的 code 只用来换取 openidwx.getUserProfile()解决的是“你长什么样”的资料问题它需要用户主动点击授权按钮才能弹出授权框。在预约挂号场景下用户头像昵称不是核心身份信息所以我的做法是首次登录仅用 openid 完成静默注册等用户真正要挂号时再在表单里补充姓名、身份证手机号不再依赖微信的授权弹窗。JWT 这块我用的方案是token 里只放 userId 和 openid加上 HS256 签名有效期设为 7 天。每次请求由 HandlerInterceptor 统一校验如果 token 过期返回特定错误码小程序端收到后自动引导用户重新静默登录。这里还有个优化点——因为 JWT 是无状态的服务端无法主动让 token 失效所以如果用户在小程序里退出了账号只需要前端删掉本地 token 就行不需要后端做什么额外操作。3.2 号源扣减不会再超卖的三种写法接近整个系统里最核心的一个业务患者点击“确认挂号”之后后端要做两件事第一生成一条预约记录第二把对应排班表的 remain_slots 减一。如果接口被高并发同时触发仅仅是“先查后改”就会出大问题。我逐步解释一下这个过程假设现在某专家号只剩 1 个号两个患者几乎同时点了挂号请求 A 读到了 remain_slots 1请求 B 也读到了 remain_slots 1请求 A 执行 insert 订单 update remain_slots 0请求 B 执行 insert 订单 update remain_slots 0最终结果是两人都挂成功了但号源被卖了两次。这个问题不管在毕设里还是在真实系统里都会被面试官或答辩老师追问。我的处理方式有三个层级第一个层级是用数据库的原子更新。写更新的 SQL 时不要先查再用代码更新而是直接用一条语句把条件带上UPDATE schedule SET remain_slots remain_slots - 1 WHERE id #{scheduleId} AND remain_slots 0这条 SQL 执行后返回受影响行数。如果 affected rows 1说明号源扣减成功此时才继续插入预约记录如果 affected rows 0说明号源已经没了直接提示“号源不足”。数据库的行锁在这里天然帮我们挡掉了并发的重复扣减。但这里还有一个细节insert 预约记录时被扣减成功的这 1 个号对应的预约信息也需要被保护不能让两条预约记录同时插入。所以我在 Service 层会加事务Transactional(rollbackFor Exception.class) public AppointmentResult createAppointment(CreateAppointmentReq req) { // 1. 原子扣减号源 int rows scheduleMapper.deductSlots(req.getScheduleId()); if (rows 0) { throw new BusinessException(当前号源不足或排班已关闭); } // 2. 生成预约记录 Appointment appointment buildAppointment(req); appointmentMapper.insert(appointment); return success(appointment); }如果插入预约记录失败事务回滚刚刚扣减的号源也会自动恢复这样就保证了“扣减号源”和“生成订单”的原子性。第二个层级是用乐观锁。我不太建议在这个场景用乐观锁兜底因为原子更新已经够了但如果你们表里恰好有 version 字段也可以用where version #{oldVersion}这种条件更新失败就重试。只是代码会更绕。第三个层级是如果你想在简历上显得更强一点可以提一下分布式锁在高并发场景或者多实例部署时用 Redis 的 SETNX 对 scheduleId 加锁防止扣减号源这一连串操作在应用层执行时分叉。不过对毕设或者课程设计来说数据库原子更新已经完全足够能用简单可靠的方法就不用重型方案这个思路本身也是展示你对架构设计的理解。我实测过用 Apache Jmeter 开 100 个线程同时抢同一节号源用上述原子更新方案最终只会有一个成功的预约记录另一个被正常拒绝数据不会出乱子。3.3 预约状态机与排班管理预约记录不能只有“已预约”和“已取消”两个状态否则医生侧的流程会非常难写。我在第一次做的时候纠结了很久后来发现不如直接定义一个状态机当患者成功提交挂号时状态为 0待就诊患者主动取消时状态变为 2已取消同时把号源归还到排班表就诊日期过了、但医生没有录入任何就诊信息时状态变为 3已过期医生在管理端确认某患者已完成就诊时状态变为 1已就诊。取消预约还有一个重要的规则必须是就诊时间之前至少 2 小时否则不允许取消。这个规则在实际业务里叫“退号窗口期”一是为了避免有人恶意占号二是防止号源释放后没人接。代码上可以用一个 cancelService 来做判断LocalDateTime now LocalDateTime.now(); LocalDateTime deadline schedule.getVisitDate() .atTime(LocalTime.parse(schedule.getEndTime())) .minusHours(2); if (now.isAfter(deadline)) { throw new BusinessException(临近就诊时间无法在线取消); }同时在取消预约的时候需要把号源加回来这个也必须和状态变更放在同一个事务里。我记得第一次做的时候漏掉了“归还号源”测试时发现取消了好几次后明明没有新增号源可剩余号却越变越少排查了半天才反应过来是对应的 SQL 少写了一行。排班管理则是管理员侧的核心操作。管理员选择某个医生、某个日期、上午或下午、总号源数然后批量生成 schedule 记录。我建议管理端提供一个“按周生成”的按钮一次性把七天的排班都建出来别让管理员一天天手动加。这个功能在论文里也非常好写——“支持快捷周排班模板”是个很有展示度的亮点。4. 微信小程序端的设计与接口对接——从零搭起用户操作闭环4.1 小程序页面架构微信小程序端的页面我按用户操作路径做了这样的规划首页医院简介轮播图、科室分类入口、热门医生推荐位医生列表页支持按科室筛选以及按医生姓名搜索医生详情页展示医生简介、擅长的领域、挂号费下方列出未来 7 天的排班时段挂号确认页确认就诊人信息、就诊日期时段、挂号金额点击支付或直接提交我的预约页按状态分组展示待就诊/已就诊/已取消记录在线问诊页展示当前医生会话的消息列表支持输入文本发送个人中心页展示用户头像、手机号、身份证号等资料维护入口。整体用 TabBar 分成首页、科室、消息、我的四个主入口。这里有一个像素细节是微信小程序的导航栏有两块一块是顶部原生导航栏高度一般是 44px全面屏手机要考虑状态栏高度还有一块是底部 TabBar 区域高度约 50px。做页面布局计算时一定不能用死高度建议用wx.getWindowInfo()动态获取状态栏高度再配合wx.getMenuButtonBoundingClientRect()算出胶囊按钮位置否则在刘海屏上会显得很挤。搜索框、自定义导航栏都建议用这个方案做适配。4.2 请求封装与登录态保持原生小程序发请求时wx.request是底层 API但不可能每个页面都裸调这个 API所以我在 utils 目录下封装了一个request.js核心功能自动从 storage 中读取 token并放到请求头里请求返回后如果 code 为 401说明 token 失效自动重新走一遍静默登录流程再重发请求统一处理接口返回的 data 和错误 message页面只需要关心业务数据。一个简化的封装思路是这样const request (url, method, data) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, method: method || GET, data: data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { wx.removeStorageSync(token); handleSilentLogin().then(() { // 重新发起原请求 }); } else { wx.showToast({ title: res.data.message, icon: none }); reject(res.data); } }, fail: (err) reject(err) }); }); };封装好之后页面里调用就非常清爽比如首页加载科室列表const deptList await request(/api/department/list, GET);这里我要提一个非常实用的建议把 BASE_URL 单独放到一个配置文件里同时本地开发时在微信开发者工具里勾选“不校验合法域名”否则每次都要去微信公众平台配置 request 合法域名本地调试会非常痛苦。等真正部署到服务器后再把 BASE_URL 改成实际的 HTTPS 域名。小程序的三种环境开发版/体验版/正式版用的是同一个 appid 但 baseUrl 可能不一样所以我们可以根据__wxConfig.envVersion来区分环境动态拼接 URL这个通常的写法是const envVersion __wxConfig.envVersion; const BASE_URL envVersion develop ? http://localhost:8080 : https://api.yourdomain.com;4.3 挂号流程在小程序端的串联挂号是不是真的成功用户是要看反馈的。小程序端这一整套动作建议拆成三步选择号源、确认信息、提交成功。选择号源这一步页面要从医生详情接口拿到该医生的排班列表。每个排班项展示日期、上午/下午、剩余号数用“约满”/“剩余 n 号”两种状态。这一步的核心在于号源是实时从后端拉的前端的按钮状态必须根据 remainSlots 来判断不能写死。确认信息这一步页面要允许用户编辑就诊人的姓名、身份证号、手机号。一般来说这些资料在“我的”页面已经维护过挂号的时候直接带出来但每个字段都允许修改。注意用户输入的身份证号必须做格式校验前端可以用正则简单过一遍但后端必须再做一遍验证——前端校验是用户体验后端校验才是安全防线。提交成功这一步页面要展示一个很清晰的“预约成功”状态并显示取号凭证。取号凭证 code 我建议由后端生成格式类似HIS202411091030123456时间戳加随机数保证全局唯一。小程序端把它显示成一个显眼的条形码或者单纯的大字凭证码即可就诊时用户向窗口展示这个码管理员能在后台查到对应记录。关于“在线支付”功能如果你用的不是企业主体的小程序微信支付是开不通的。所以我在项目里把“挂号费支付”简化成了“直接生成挂号单费用到院支付”。如果你想让系统看起来更完整可以做一个模拟支付的弹窗点击“提交”后弹出一个确认框询问“确认支付 30 元挂号费”点击确认即完成支付流程实际上后端只是把线上支付字段标记为“到院支付”。这个设计在论文里写成“为后续接入微信支付预留接口”也是合理的处理策略。我见过不少同学为了硬接微信支付卡在商户资质上卡了一星期实在不值得。4.4 消息模块在小程序端的交互设计消息模块的前端其实不复杂就是一个聊天页顶部显示医生姓名中间是历史消息滚动列表底部是输入框和发送按钮。进入页面时先调用“获取会话详情”接口拿到会话 ID 和之前的聊天记录之后用户每次发送就把文本内容 POST 到后端。后端保存成功后返回前端再把这条消息追加到列表里。因为我是用轮询的方式做消息刷新——每 5 秒拉一次“该会话的新消息”所以消息能保持实时性。如果要做成更流畅的体验可以用 WebSocket 长连接推送但毕设阶段轮询足够而且天然没有断线重连、心跳保活这些复杂问题。有一个细节要注意每次发完消息和轮询拉完新消息后都要把滚动条scroll-view的 scroll-top 设置到最底部否则用户会看到一个永远停留在顶部的聊天窗口体验非常差。5. 在线医患交互不止是“发消息”还要讲清楚状态和权限5.1 会话建立与状态控制“在线医患交互”看似只要建一张消息表就行了但如果从头到尾都只有一个无限增长的聊天流系统很快就会失控。我给这个功能设计了两层控制第一层是会话状态。一个患者和一个医生只能有一个“进行中”的会话当患者提交了新消息时后端先查会话是否存在不存在则创建一个存在且状态为“已结束”时则自动开一个新会话。这样可以避免消息全堆在一个永恒会话里也方便做“历史咨询记录”的分段展示。第二层是权限控制。患者只能发起或回复“发给自己的医生”的会话医生端则只能看到“挂自己号的患者”发来的会话。后端实现时不要只靠前端隐藏入口做限制服务端接口上必须加校验——每次查询消息列表时都要根据当前登录用户的 userId 判断其是否为该会话的参与者否则直接返回 403 错误。这种拦截逻辑在答辩时也很容易成为加分项因为很多人的毕设接口是“谁都能查所有数据”的裸奔状态。5.2 消息拉取策略与敏感词过滤因为小程序端的消息采用轮询方案后端接口的设计上我做了两个优化一个优化是“增量拉取”。消息列表接口支持传入 lastMessageId后端只返回 ID 大于这个值的消息这样客户端每次都只拉少量新消息不会越拉越多。比如患者发出消息后 5 秒内刷新会拉到医生刚回复的内容体验和微信聊天几乎一致。另一个优化是敏感词过滤。医患交互场景下消息内容容易被钻空子虽然这个功能在医院内部系统里更多是为了合规但放到毕设里我建议做一层简单的关键词替换——把非法词列表存在一个配置表里发送消息时遍历替换成**再入库。这能侧面说明你考虑到了内容安全和合规审查这个点在论文评审的时候比较加分。还要提一个边界问题消息记录要不要永久保留从实际业务角度看聊天内容涉及医患关系原则上要保留。但从毕设演示角度看如果测试数据太多会显得乱所以可以加一个“仅保留最近 100 条消息”的清理策略或者干脆不加限制看上线的数据量再定。一般答辩不会问这么深但你自己心里要有数。5.3 从交互矩阵反推接口设计为了让整个页面的消息展示和会话管理不混乱我梳理了一个交互矩阵后端接口完全围绕它来设计患者发起新会话POST /api/consultation/start参数带上医生ID患者或医生发送消息POST /api/consultation/message/send拉取会话列表GET /api/consultation/list拉取某个会话的历史消息GET /api/consultation/messages?consultationIdxxlastMessageIdxx医生结束会话POST /api/consultation/end。这几个接口用 RESTful 风格命名语义清晰后面写接口文档时非常省力答辩时讲起来也很有条理。6. 部署上线与踩坑记录——从本地跑通到服务器稳定运行我踩过的那些坑6.1 本地环境搭建与配置清单本地开发的完整环境配置我列在下面如果你照着搭基本不会因为环境问题卡住JDK1.8 或 11推荐用 1.8 或者 17看使用的 Spring Boot 版本MySQL8.0数据库默认字符集 utf8mb4Redis如果你用了缓存或分布式锁需要装一下纯 CRUD 可以不加IDEA装 Lombok 插件微信开发者工具注册小程序测试号即可不需要企业主体Maven3.6IDEA 自带也可以。application.yml 里的配置重点注意数据库连接串MySQL 8.0 必须指定 serverTimezone否则时间字段会报错spring: datasource: url: jdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password接口返回格式方面我统一封装了一个 ResultBean包含 code、message、data 三个字段。这样小程序端封装请求工具时逻辑可以做到非常统一不用每个接口单独写成功/失败的判断。代码风格一致性对后期调试非常重要尤其是一个方法分散在多个类中的场景。6.2 打包部署与 Nginx 反向代理本地跑通之后下一步就是部署到服务器。我在服务器上用的是 CentOS 7 Nginx JDK 8 MySQL步骤是这样的打包之前先检查一下application.yml里的配置是否指向生产数据库或者采用把数据库连接信息放到application-prod.yml里的方式用启动参数来激活mvn clean package -DskipTests java -jar target/hospital-system-1.0.0.jar --spring.profiles.activeprodNginx 这边我配置了一个 server 块把 443 端口的 HTTPS 请求转发到本机的 8080 端口后端服务。小程序的 request 合法域名强制要求 HTTPS所以证书是必须的可以用你已有的域名证书或者各大云厂商的免费证书。server { listen 443 ssl; server_name api.yourdomain.com; ssl_certificate /etc/nginx/ssl/yourdomain.pem; ssl_certificate_key /etc/nginx/ssl/yourdomain.key; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意proxy_pass后面如果带了/会把/api/前缀去掉再转发如果不带/则原样转发。我在第一次配置时因为多写了一个斜杠导致后端所有接口 404排查了半天才反应过来是这个斜杠的语义问题。后端进程管理建议用 systemd 或者 nohup 方式。我自己的习惯是写一个 start.sh 脚本包含停止旧的 java 进程、启动新 jar、记录日志三条命令每次部署时跑一遍脚本就完事#!/bin/bash APP_NAMEhospital-system-1.0.0.jar echo kill old process pid$(pgrep -f $APP_NAME | head -n 1) if [ -n $pid ]; then kill -9 $pid fi nohup java -jar $APP_NAME --spring.profiles.activeprod logs/app.log 21 echo start success6.3 部署过程中的常见问题清单我把自己实际遇到过的、也是最容易栽进去的几个问题整理成表格供你排查问题现象原因解决办法小程序请求报“url not in domain list”没有配置 request 合法域名或域名没有备案小程序后台配置合法域名本地调试可临时关闭校验后端接口不通Nginx 报 502后端进程挂了或 proxy_pass 路径拼接错误检查日志和 Nginx 配置确认端口是否监听时间字段在高并发下偶发错乱serverTimezone 未设置为 Asia/Shanghai数据库连接串中显式指定时区服务器和容器也设置时区图片上传后访问 404静态资源路径没被映射到配置资源映射或在 Nginx 中添加 location 指向上传目录超过 10 个 wx.request 同时发起小程序原生限制并发请求数量为 10对首页并发请求做 Promise 合并或减少首屏请求次数这里我要重点说最后一条小程序的wx.request并发限制是 10 个但首页如果同时拉 banner、科室、医生推荐、预约状态等四五个接口还好可一旦你做了一个“进入页面就全部请求”的无脑设计很容易在弱网环境下瞬间打满并发然后部分请求直接失败。我做完首页请求封装后用真机测试时发现偶发 3-4 个接口失败后来把首页的服务改为“先加载核心数据其余异步加载”的方式问题就解决了。这个优化虽然看起来不大但在移动端项目中是非常实际的体验改进。另外再提醒一个非常隐蔽的坑MySQL 8.0 的驱动包名是com.mysql.cj.jdbc.Driver如果你网上抄代码时还带着老的com.mysql.jdbc.Driver启动虽然不会立刻报错但一旦连接池初始化时会抛 ClassNotFoundException。使用 Spring Boot 时其实连driver-class-name都可以不写让框架自动识别即可少写一行就少一个出错点。7. 答辩准备、功能扩展方向与我的个人体会7.1 答辩老师最常问的问题怎么接技术上能跑通只是第一步答辩环节才是很多同学真正紧张的地方。根据我自己的经验和身边人反馈医院预约挂号系统这类题目答辩老师几乎必问这几个问题第一个问题是“多个用户同时抢最后一个号怎么办”。这个问题直接对应我在 3.2 节讲的原子扣减方案你需要说出 SQL 的原子性配合事务回滚的思路最好再补充一句“如果项目后续部署成多实例可以在 Redis 里用分布式锁做兜底”。这句话一出来维度立刻就不一样了。第二个问题是“在线问诊消息为什么不用 WebSocket”。你可以大方承认用的轮询解释为“微信小程序端对 WebSocket 的支持虽然没问题但轮询在处理弱网、断线重连时更简单稳定同时在当前业务量下 5 秒轮询已经满足实时性要求”。关键是要说清楚取舍而不是被问懵。第三个问题是“排班表和预约表的数据字段为什么要冗余这么多”。你可以用我在 2.2 节的思路答一是降低高频查询页面 join 带来的性能损耗二是确保历史记录稳定不被后续改动影响。把业务场景和设计决策绑定在一起就是很好的答案。第四个问题是“数据库里有哪些索引”。别只说“主键索引”我建议至少建立这几个预约表的 patient_id 索引、schedule 表的 doctor_id 索引、消息表的 consultation_id 索引。这些小细节展示出你关注查询性能而不是写完 CRUD 就结束了。7.2 这套系统还能往哪些方向扩展如果时间充裕或者你希望答辩时展示更多亮点我建议从以下几个方向扩展每个都足够形成一个小节用于论文描述订阅消息通知患者预约成功后通过微信订阅消息模板发送“预约成功通知”就诊前一天发送“明日就诊提醒”。这个功能在业务上很真实代码量也不大排班自动释放定时任务扫描已过期且未就诊的预约记录自动把对应的号源释放回排班表医生接诊闭环医生在管理端确认开始接诊就诊完成后填写诊断建议患者可以在小程序看到建议形成一个完整的“预约-就诊-回执”流程数据可视化管理后台接入 ECharts 或通过自绘统计图表展示近 7 天挂号量趋势和各科室热度分布式会话消息推送如果想把在线互动升级可引入 WebSocket 或者接入第三方即时通讯 SDK让聊天体验从 5 秒轮询升级为毫秒级到达。每一次扩展都要把开发量和收益放在一起权衡。对毕设而言功能不在于多而在于“能讲清楚为什么这么做”。哪怕你最终只做了预约和问诊这两个核心闭环只要把并发控制、角色权限、异常处理的细节讲透同样是一个很高质量的毕设项目。7.3 我做完这套系统的个人感受最后聊几句实在的。我做完这套系统最大的感受是像这种“听起来很常见、做起来全是细节”的题目反而最容易拉开差距。很多同学把项目做成一个“套壳 CRUD”接口全、页面多却没有一个核心业务逻辑是有挑战性的。而医院预约挂号系统天然自带两个难点号源并发、医患消息状态流转。把这两个难点解决好整个项目的技术含金量一下就出来了。我强烈建议你在做的时候不要把“能跑”当成终点。跑通之后至少再花一个晚上的时间去看这几个地方打印一下排班表扣减 SQL 的执行日志观察并发场景下的行为把关闭小程序再进来、token 失效再自动登录这条链路完整验证一遍把医生停止排班后患者端入口是否还能看到过期号源的情况测一下。这些“边界场景”才是答辩时你能自信说出来的底气。如果你正在为这个课题卡壳我希望这篇文章能帮你把一个模糊的“医院预约挂号系统”变成一张清晰的地图。按我上面讲的表结构去建库、按接口设计去写后端、按页面清单去搭小程序、按部署流程去上线这套系统就真的从标题变成了一个可以展示、可以讲解的完整作品。做项目这件事最怕的不是不会而是断续的参考资料让你反复推倒重来希望这份完整记录能让你少走几步弯路。