ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于微信小程序+SSM的医院挂号系统设计与毕业设计指南

基于微信小程序+SSM的医院挂号系统设计与毕业设计指南 1. 毕业设计的核心微信小程序 SSM 的医院挂号系统怎么做对我来说大学最后一个学期最让人头疼的不是代码本身而是不知道一个“基于微信小程序的医院挂号系统 SSM毕业论文”到底要搭到多深才算合格。说实话这类项目的业务逻辑不算复杂患者打开小程序选科室、选医生、选时间段、提交预约后台把订单存进 MySQL医生或管理员能看到预约情况。但正因为业务听上去简单很多人反而把系统做成了 CRUD 练习最后在答辩现场被老师一句“并发怎么办挂号冲突怎么处理”问得说不出话。这篇博文是围绕这套系统做完整复盘的经验帖从架构选型、SSM 常用注解、小程序端列表加载更多、顶部导航栏高度适配到论文结构、答辩准备一条线全部讲清楚。适合谁看第一类是准备做毕业设计的学生尤其选题里带“小程序”和“SSM”这两组关键词的同学第二类是在微信生态里接触过 wx.login()但前后端配合还没完全打通的新手。我默认你有一点 Java 基础和 HTML 知识但还没到熟练工水平。这个项目的价值在于前端用微信小程序用户不用下载 App扫码即用后端用 SSMSpring SpringMVC MyBatis这是高校数据库课程和毕设题目里出现频率最高的一套组合学会之后找工作面试也可以拿来讲项目。系统能解决医院线下排队的真实场景也能作为毕业设计完整体现分层设计、数据库设计和并发控制能力。业务范围不算广但五脏俱全。2. 整体架构与设计思路2.1 模块边界怎么划先别急着写代码。我第一版直接写 Controller结果所有代码都挤在一个 Service 里后面每加一个功能就要改一大片。后来我重新按三个端来切分小程序用户端患者、Web 管理端医生或管理员用 SSM 里的 JSP 模板或者独立页面都可以、后端服务端统一提供 restful 接口。注意这里是三个端不是两个。很多同学只做小程序端等答辩时老师问“医生怎么排班你怎么查看预约记录”就只能干瞪眼。管理端我建议不要做得很花哨能登录、能维护科室、能维护医生信息、能看到某天预约名单就够了。管理端用 SSM 的 SpringMVC 返回视图页面其实写起来很快关键是能让整个业务流程闭合。后端服务的接口设计建议统一返回一个 Result 对象里面至少包含 code、msg、data 三个字段。这样做的好处非常直接小程序端不管请求成功还是失败拿到的结构都一样前端判断 code 再决定渲染什么页面。很多同学会忽略这个约定结果接口一会返回 String一会又返回 Map前端拿到数据一脸懵联调效率极低。2.2 为什么用 SSM 而不是 Spring Boot有人会问现在企业里 Spring Boot 才是主流为什么毕业设计还要用 SSM核心原因就两点第一很多学校选题库里直接挂着“SSM”论文模板也按 SSM 的分层来写你换成 Spring Boot 之后论文里的 Spring、SpringMVC、MyBatis 三个层次不好交代技术栈和章节标题对不上第二SSM 的分层非常清晰Spring 管对象生命周期SpringMVC 管请求分发MyBatis 管数据库访问答辩时你更容易展开讲设计原理。Spring Boot 把很多东西自动化了使用起来更简单但反而不太容易体现“你懂原理”。不是说 Spring Boot 不行。如果你导师确认可以用 Spring Boot那当然没问题。但今天这篇以 SSM 为主线。曾经有学弟问我项目内部用了 Spring Boot论文里写 SSM 行不行千万别这么干。答辩老师问你启动类在哪、自动配置怎么理解、配置文件里 spring.factories 是什么意思你会解释得很别扭。项目代码里的技术栈和论文标题里的技术栈必须完全一致这是毕业设计的第一条潜规则。从教学和评审角度SSM 对理解“请求进来之后数据是怎么流动的”更友好。你自己也会被逼着把 Controller、Service、Mapper 三层分清楚而不是依赖一键生成的脚手架。2.3 数据库与并发设计要提前想清挂号系统最核心的其实是排班和预约不是用户注册。一张排班表 schedule一张预约表 appointment用 doctorId、scheduleId、date、timeSlot 把医生、时间和号源串起来。最关键的问题在于怎么避免“同一时刻同一排班被两个患者抢到同一个号”。我用的是最朴素但可靠的方案MySQL 唯一索引 事务 原子更新。这个方案在后文第 4 章展开。先给一个大致的表结构让你对系统规模有数patient 患者表openid、昵称、手机号等department 科室表科室名、位置、简介doctor 医生表所属科室、姓名、职称、简介schedule 排班表医生、看诊日期、时间段、总号源、剩余号源appointment 预约表患者、排班、预约状态、创建时间这套设计并不高级但能把业务闭环跑通而且论文里有得写。3. SSM 后端的关键实现细节3.1 三层架构在项目里到底怎么落不要被“三层架构”这个说法吓到。放在挂号系统里其实就是三层职责Controller 层接收请求、解析参数、把 Service 的结果转换成 JSON 返回Service 层处理业务规则比如挂号前检查剩余号源、挂号成功后扣减号源、取消预约时恢复号源Dao 层Mapper写 SQL 语句和数据库打交道我见过很多同学为了省事把 SQL 直接写在 Controller 里。页面数据确实能出来但项目做完代码没法维护更麻烦的是论文里“分层设计”这一节你绕不开。到时候老师问 Service 层解决什么问题你说“我也不知道就是听学长说要分三层”这会很尴尬。来看一个典型的挂号 Controller 写法Controller RequestMapping(/appointment) public class AppointmentController { Autowired private AppointmentService appointmentService; ResponseBody RequestMapping(value /create, method RequestMethod.POST) public Result create(RequestBody AppointmentDTO dto, HttpServletRequest request) { String openid (String) request.getAttribute(openid); return appointmentService.createAppointment(openid, dto); } }这段代码里出现了 Controller、RequestMapping、Autowired、ResponseBody、RequestBody都是 SSM 里最高频的注解。重点不在代码量而在你想清楚每个注解分别干了什么事。Service 层的实现更强调业务逻辑Service public class AppointmentServiceImpl implements AppointmentService { Autowired private AppointmentMapper appointmentMapper; Autowired private ScheduleMapper scheduleMapper; Transactional(rollbackFor Exception.class) Override public Result createAppointment(String openid, AppointmentDTO dto) { // 1. 校验排班是否存在 // 2. 原子扣减号源 // update schedule set remain_num remain_num - 1 // where id #{scheduleId} and remain_num 0 // 3. 写入 appointment 记录 // 4. 返回成功和订单信息 } }这里有个必须记住的细节Transactional 上面的 rollbackFor 一定要写成 Exception.class。很多教材里只写 Transactional默认情况下它只会让 RuntimeException 回滚而你代码里抛出的通常是 Exception初始化事务没触发回滚就会出现“号源扣减了但预约记录没写进去”这种脏数据。3.2 SSM 常用注解速查表我当时整理了一张表背下来基本能应对大多数接口开发。分享给你注解放置位置作用挂号系统中的用法Controller类标记Controller层组件接收并处理小程序请求ResponseBody方法或类把返回值序列化为JSON给小程序返回 {code,msg,data}RequestMapping类或方法映射URL到具体方法如 /doctor/list、/schedule/detailPathVariable方法参数取URL路径中的动态值获取 /schedule/{doctorId} 里的 doctorIdRequestBody方法参数把请求体JSON绑定到对象小程序提交预约JSON时使用Autowired字段或构造器按类型注入依赖把Service注入ControllerService实现类标记业务层组件AppointmentServiceImpl 等Repository接口标记DAO层组件让 Mapper 被 Spring 管理Transactional方法或类开启事务保证挂号扣库存和写记录一致实际项目里高频出现的 SSM 注解基本就这些。你不需要把每个注解的底层原理都背得滚瓜烂熟但至少要能说出这个注解放在哪一层、解决什么问题、为什么需要它。3.3 小程序端怎么跟后端连通小程序和 SSM 后端通信最常用的方式是 wx.request()。举个例子获取医生列表wx.request({ url: ${BASE_URL}/doctor/list?departmentId${departmentId}, method: GET, header: { content-type: application/json }, success(res) { if (res.data.code 200) { this.setData({ doctorList: res.data.data }); } } });本地开发时需要注意直接在微信开发者工具里访问 http://localhost:8080 会被拦截你需要勾选开发者工具右上角的“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”选项。这一步不做你会一直看到 request: fail。真正发布上线肯定要准备域名和 HTTPS 证书。微信小程序规定请求的 URL 必须已经在“小程序管理后台-开发设置-服务器域名”里配置。支付类接口还可能要求更高这里我建议毕业设计阶段先把接口跑通上线注意事项在论文里补充说明即可。# 前后端联调时基地址建议单独放一个配置文件 const BASE_URL http://localhost:8080/api; module.exports { BASE_URL }; # 这样小程序所有页面都引用同一个地址后面换服务器不用改几十处4. 数据库设计与并发控制4.1 核心表结构细节毕业设计的表数量在 6 张左右就足够了。我用的是这些表CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信用户唯一标识, nickname varchar(100) DEFAULT NULL, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE department ( id int NOT NULL AUTO_INCREMENT, dep_name varchar(100) NOT NULL, location varchar(255) DEFAULT NULL, intro varchar(500) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE doctor ( id int NOT NULL AUTO_INCREMENT, dep_id int DEFAULT NULL, name varchar(50) NOT NULL, title varchar(50) DEFAULT NULL, intro varchar(500) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE schedule ( id int NOT NULL AUTO_INCREMENT, doctor_id int NOT NULL, work_date date NOT NULL, time_slot varchar(20) NOT NULL COMMENT 上午/下午, total_num int NOT NULL, remain_num int NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_schedule (doctor_id, work_date, time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE appointment ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, schedule_id int NOT NULL, order_no varchar(64) DEFAULT NULL, status int DEFAULT 0 COMMENT 0待就诊 1已完成 2已取消 3已过期, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_schedule (user_id, schedule_id), KEY idx_schedule_id (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;请你特别注意 appointment 表里的唯一索引 uk_user_schedule。这个索引可以防止同一个用户对同一个排班重复提交预约。即使前端发送了两个完全相同请求数据库也会挡住第二次插入。4.2 如何防止重复号假设现在只剩 1 个号两个患者同时提交预约。如果代码写成先 SELECT 剩余数量再 UPDATE 把剩余数量减一那很可能两个人都在“SELECT”时看到剩余 1最后超卖。解决办法不是靠 Java 里 synchronized而是把检查放在数据库层面。核心 SQL 是这一句UPDATE schedule SET remain_num remain_num - 1 WHERE id #{scheduleId} AND remain_num 0;执行这条语句时MySQL 会对匹配到的行加锁。如果 update 返回值是 1说明号源扣减成功如果返回值是 0说明没有剩余号源直接返回“号源不足”。为什么不能只用 Java 程序的 synchronized因为一旦你把项目部署到多台服务器或者 Tomcat 内部出现线程池并发synchronized 只能锁住当前进程里的一把锁多个实例之间依然会互相撞。用数据库原子更新不管你部署了多少台服务器底层数据都能保证一致。论文里写“本系统采用数据库级原子更新避免超卖”这个表达是有分量的。4.3 定时任务清理过期号现实场景中患者预约了但不去号源一直被占住医生排班就废了。我加了一个定时任务每天把超过就诊时间仍未到诊的预约状态改为“已过期”同时把对应的号源回补到 schedule 表。实现方式有很多种。简单场景用 Spring 自带的 Scheduled 也可以但 SSM 项目一般会用 Spring MVC 的配置文件加一个 task 命名空间。你需要额外注意定时任务里做数据库回补时要同时查询 appointment 和 schedule最好也开启事务保证两步操作要么全成功、要么全失败。这个功能如果不做表面上不影响主流程但会让整个项目少一个“亮点”。答辩时老师听到“过期号自动释放”这个设计会认为你考虑到了真实运营场景这是加分项。5. 小程序端的重点功能实现5.1 自定义顶部导航栏与高度适配医院类小程序经常要做自定义顶部导航栏把当前科室、搜索框、消息入口都放进导航区。但自定义导航会遇到一个坑不同手机的状态栏高度不一样。你如果写死一个高度iPhone 上正常安卓手机上内容可能顶到电量图标下面。你需要动态拿到状态栏高度和胶囊按钮位置然后算出导航栏真实高度。典型代码const sysInfo wx.getSystemInfoSync(); const menuButtonRect wx.getMenuButtonBoundingClientRect(); const statusBarHeight sysInfo.statusBarHeight; const navBarHeight (menuButtonRect.top - statusBarHeight) * 2 menuButtonRect.height; this.setData({ statusBarHeight, navBarHeight });这段代码的原理是微信小程序的胶囊按钮右上角三个点差不多是垂直居中在导航栏区域的。所以导航栏总高度可以近似估算成“胶囊按钮距离状态栏底部的高度乘以 2再加上胶囊按钮本身高度”。解决了微信小程序顶部导航栏高度问题之后自定义导航才不会被系统自带的导航栏覆盖。页面配置文件里还要加一个设置{ navigationStyle: custom }不加这一行就算你算出了 navBarHeight系统原生的导航栏依然会照常显示自定义区域会被挤在下面版式非常奇怪。这个坑我自己踩过花了一下午才反应过来。5.2 页面列表加载更多“微信小程序页面列表加载更多”是我在做预约记录列表时被逼着研究的。后端分页返回数据前端滚动到底部自动加载下一页这个交互如果做不好用户会以为 App 卡死了。我在页面的 data 里维护两个变量page 和 hasMore。每次请求成功后把新数据追加到原有列表后面并更新 page。触底加载用 onReachBottom 触发Page({ data: { appointmentList: [], page: 1, pageSize: 10, hasMore: true }, onLoad() { this.loadList(true); }, onReachBottom() { if (this.data.hasMore) { this.loadList(false); } }, loadList(reset) { const page reset ? 1 : this.data.page; wx.request({ url: ${BASE_URL}/appointment/myList, data: { pageNum: page, pageSize: this.data.pageSize }, method: GET, success: res { const records res.data.data.records; this.setData({ appointmentList: reset ? records : this.data.appointmentList.concat(records), hasMore: res.data.data.pages page, page: page 1 }); } }); } });关键点有两个第一不要直接在 data 里用 this.data.appointmentList.push()要一次性 setData 一个拼接后的新数组第二加到接口返回的总页数以后马上把 hasMore 置为 false避免页面疯狂发请求。列表加载这部分的体验直接影响用户对系统稳定性的判断。5.3 提交挂号信息wx.login() 用户授权微信小程序获取用户身份最常规的方式是 wx.login()。小程序端拿到一个临时 code发给后端后端用 code 向微信服务器换取 openid 和 session_key。注意session_key 和 appsecret 只能留在服务端绝对不能在小程序代码里出现。一个精简的流程是wx.login({ success: res { wx.request({ url: ${BASE_URL}/user/login, method: POST, data: { code: res.code }, success: resp { const token resp.data.data.token; wx.setStorageSync(token, token); } }); } });后端拿到 code 后调微信接口换 openid然后生成一个自定义 token 返回给小程序。小程序后续每个请求都在 header 里带上 token后端通过 token 反查用户身份。这样你就不需要把 openid 放在 URL 参数里传安全性更好。很多同学会想着在小程序端拼 appsecret目的是想自己直接调微信接口换 openid。这种做法是大忌。appsecret 一旦出现在小程序包里就等于是把后台钥匙交给了所有人。论文里一定要写清楚敏感信息保存在服务端小程序端只保存临时 token。6. 前后端联调常见问题速查6.1 网络请求怎么排查前后端联调是最消磨耐心的一步。我不建议你一开始就陷入黑盒猜测而应该打开微信开发者工具的 Network 面板看请求是否发出、状态码是多少、响应体是什么。常见问题可以按状态码快速归类404后端的 Controller URL 写错了优先检查 RequestMapping 的路径是不是多了一层。500要立刻看后端控制台的异常栈多数是 Mapper 的 SQL 参数没对、表名字拼错这种事情。403常见于跨域问题。SSM 项目里如果没配置 CorsFilter小程序端跨域请求会被拦截后端需要加一个支持跨域的过滤器。request:fail本地调试时没有勾选“不校验合法域名”或者后端根本没启动。timeout先拿浏览器直接访问同样的地址确认后端接口真的能通再回小程序里查原因。顺便说一句不要把时间浪费在反复点击“刷新”上面。每次出问题第一件事是分清楚“是前端没发出请求还是后端返回了错误”。把问题从中间切开你就能少踩很多坑。6.2 常见错误码与排查清单我在项目过程中整理了一张速查表分享给你现象可能原因排查建议wx.request 一直 fail后端没启动、地址错误、域名校验未关闭先用浏览器请求同一地址后台日志 NullPointerException接口没有拿到 openid 或 token 为空到开发者工具 Storage 面板确认 token 是否写入数据库查询中文乱码JDBC URL 没加编码参数在连接地址后加 characterEncodingutf8提示“订单不存在”前端传的 scheduleId 和后端数据库 ID 对不上先查 schedule 表真实数据自定义导航栏位置错乱只写死高度没动态计算用 wx.getMenuButtonBoundingClientRect 处理页面返回空列表后端分页页码从 0 还是 1 开始不统一和后端约定 pageNum 从 1 开始前端传值保持一致这里提一下“微信小程序 10002”这类错误码。它常见的含义是请求繁忙或服务异常。遇到以后不要急着改业务逻辑先去后端看日志。错误码只是最外层提示真正的病因往往在服务端异常栈里。你如果能在论文的测试部分写一张“功能测试与问题分析表”也会让论文更有说服力。6.3 一个很实用的联调小技巧在小程序端把接口基地址统一抽到 config.js 里。不要在每个页面里直接写 http://localhost:8080 这种裸地址否则你换电脑、换服务器时会被折磨哭。我自己的项目里是这样组织的// config.js const BASE_URL http://localhost:8080/api; module.exports { BASE_URL };页面里使用的时候就引用这个文件后续上线只需要改一个地方。平时调试后端也可以把本地端口固定在 8080这样不用频繁回改小程序配置。再配合开发者工具的“编译模式”你可以给不同页面设置不同的启动参数快速模拟不同科室、不同医生的页面数据比手动在页面上反复操作要快得多。7. 毕业论文怎么写、答辩怎么准备7.1 论文结构和常见丢分点毕业设计论文一般包括绪论、相关技术介绍、系统分析、系统设计、系统实现、系统测试、总结。大多数人的问题不是字数不够而是技术介绍和系统设计严重脱节。举个例子技术介绍这一章写到 Spring 的时候大段大段抄官方定义等到系统设计的时候又开始画 DFD 图、流程图全篇几乎没提“本项目里 Spring 到底发挥了什么作用”。这会让老师觉得你不是真会而是纯拼凑。换一种写法会好很多在“相关技术介绍”里写 Spring 的 IoC 和 AOP 时直接加一句“在本项目中IoC 用于统一管理 Controller、Service、Mapper 对象降低层与层之间的耦合事务管理通过 AOP 实现确保预约和扣减号源的同生共死”。这样技术介绍和系统设计就产生联动论文读起来会有很强的连续感。系统测试部分也不要只贴“登录成功”的截图。要针对挂号系统的核心场景展开比如注册登录、选择科室、选择医生、提交预约、取消预约、号源不足等。测试用例表应该包含编号、测试项、预期结果、实际结果、是否通过。把并发挂号测试作为单独一项写进去效果会更好。7.2 答辩必问的几个问题答辩时老师不会把整个项目从头到尾看完他只会挑几个关键点问你。我用真实经历整理出几个高频问题问你的系统怎么保证挂号不冲突答在 schedule 表执行原子更新update schedule set remain_num remain_num - 1 where id ? and remain_num 0同时把扣减号源和插入预约放在同一个事务里保证数据一致性。问openid 和 token 是怎么设计的答前端 wx.login() 获取临时 code后端用 code 换 openid再生成自定义 token 返回给小程序。小程序后续请求在 header 里带 token后端解析 token 得到用户身份。问核心表之间有什么关系答科室和医生是一对多医生和排班是一对多排班和预约是一对多。其中 appointment 表通过 user_id、schedule_id 关联 schedule 表和 user 表。问你这个系统的不足是什么答我没有接入在线支付后续可以增加微信支付模块目前没有主动推送提醒后续可以结合小程序订阅消息在就诊时间前提醒患者。回答这类问题时要冷静描述逻辑不要背代码。老师问你“为什么用这个方案”你要说清楚它比另一种方案好在哪。比如数据库原子更新比 synchronized 更可靠这才是设计层面的思考。7.3 答辩前我做的几个加分改动这里分享一些我自己的零散经验不保证每个老师都喜欢但至少能提升整体完成度。第一给所有列表页加上空状态和加载状态。比如预约记录为空时显示“暂无预约记录”而不是一片空白。这个小改动很基础但很多同学的页面都忽略了。第二把“取消预约”做成独立功能。用户取消预约后后端要把 schedule 表的 remain_num 加回去。很多同学只做到“提交预约”就收工了缺少逆向流程答辩时容易被问“用户想取消怎么办”。第三在后端预留一个“预约过期自动回补号源”的定时任务实现即使你的演示时间不够也可以在论文里说明实现思路。这种系统设计层面的细节比你花两天憋一个花哨动画要有用得多。我记得项目推进到后期最费时间的不是写代码而是前后端约定接口字段。如果你能在开头就和队友或自己定好 Result 返回结构和分页参数后面的开发会顺很多。这个项目做完之后我对“先设计、再编码”这句话的感受比听十遍课都深。希望这篇复盘能帮你在微信小程序 SSM 的路线上少走一点弯路。
返回列表