SSM+微信小程序实现医院体检预约管理系统:从表设计到上线避坑
发布时间:2026/9/17 5:34:56 作者:尧图编辑部 阅读量:1,286

简介一款面向移动医疗信息化学习与实践的完整源码案例基于微信小程序与SSMSpring、SpringMVC、MyBatis构建医院体检管理系统适合后端开发、小程序入门及医疗信息化从业者参考。资源包共857个文件约33.35MB其中包含java、js、vue等后端与前端源码wxml、wxss等微信小程序界面文件以及png、svg图片素材和sql数据库脚本zip包内目录结构清晰便于按功能模块查阅。已有206人学习具有较好的参考价值。通过该案例可掌握前后端分离架构下小程序预约、体检管理、报告查询等核心流程理解Spring依赖注入、SpringMVC处理请求及MyBatis持久层解耦设计学习用户、预约、套餐、报告等数据表建模思路。案例覆盖用户注册登录、体检套餐选择、在线支付、后台系统管理等功能模块适合在课程设计或项目实战中借鉴也可为医疗行业信息化建设提供落地参考。1. 从“毕业设计”到“能上线体检预约”这个项目到底在做什么凡是搜过“医院体检管理系统”的人都见过一大批只有增删改查的CRUD代码后台管理员录入套餐小程序端拉列表、提交预约结束。真正让这类项目拉开档次的不是页面好不好看而是三条业务主线的完整度——预约下单的状态流转、体检报告的回传与展示、以及前后端各自的鉴权边界。微信小程序负责C端用户的预约与报告查看SSM后端负责组织机构、套餐管理、订单处理和报告回传二者通过HTTP接口协作本质上是一个业务闭环清晰、适合练手也适合改造的前后端分离项目。这个案例设计合适的读者是已经写过Java Web但没完整做过微信小程序对接的人以及准备把它改成毕业设计或简历项目的学生。你从解压这个ZIP开始最该关注的不是代码量而是表结构怎么定、预约单的状态谁在推进、小程序端拿到的token到底服务端认不认。下面按我搭这类项目习惯的顺序讲先定表再写后端接口再写小程序页面最后把前后端联调和上线排查的坑交代清楚。2. 先定数据模型体检预约与报告回传的表结构怎么设计2.1 核心表拆解套餐、预约、报告三者之间的关系体检业务的用户侧动作其实只有三个选套餐、约时间、看报告。管理侧动作也三个维护套餐、排班审核、回传报告。因此表不用多但每张表都要把状态和关联关系写明白。我通常拆成四张业务表加两张基础表t_user小程序用户、t_package体检套餐、t_order预约单、t_report体检报告、t_package_item套餐明细一对多、t_doctor医生或体检机构。t_package和t_order之间是普通的多对一一个用户可下多单。最关键的是t_order要冗余package_name、package_price等快照字段而不是下单后实时JOIN套餐表。因为套餐价格或名称随时可能被管理员修改订单一旦生成就必须保留下单那一刻的商品快照这是电商订单表的通用做法体检订单也不例外。报告表和订单表是一对一一个预约单只对应一份报告报告表里冗余user_id是为了免去多表查询。2.1.1 预约单的状态字段不要用枚举写死在代码里预约单的状态字段我见过两种极端写法一种用tinyint存0/1/2代码里写满魔法数字另一种用VARCHAR存中文看着直观但没法扩展。常见做法是存数字在代码里定义常量类统一管理。体检业务的订单状态相对固定0-已预约、1-已到检、2-体检中、3-报告已出、4-已取消、5-已完成。后端接口只负责前置状态的校验比如“报告已出”只能由后台调报告回传接口触发小程序端无论如何不能直接改状态。2.2 建表SQL关键字段与索引说明下面给出四张核心表的建表SQL去掉了冗余注释保留生产环境中能直接用的字段设计。注意这里用的是MySQL 5.7语法字符集统一为utf8mb4因为报告结论里可能出现患者自述、特殊符号等生僻字。CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(128) NOT NULL COMMENT 微信openid唯一, name varchar(32) DEFAULT NULL, phone varchar(20) DEFAULT NULL, id_card varchar(32) DEFAULT NULL COMMENT 身份证号报告需要, gender tinyint(1) DEFAULT NULL COMMENT 0女 1男, age int(11) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT小程序用户表; CREATE TABLE t_package ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT 套餐名, price decimal(10,2) NOT NULL, description varchar(512) DEFAULT NULL, status tinyint(1) DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体检套餐表; CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号展示用, user_id bigint(20) NOT NULL, package_id bigint(20) NOT NULL, package_name varchar(64) NOT NULL COMMENT 套餐快照, package_price decimal(10,2) NOT NULL COMMENT 下单时价格快照, appoint_date date NOT NULL COMMENT 预约日期, appoint_time varchar(16) DEFAULT NULL COMMENT 时间段如 08:00-10:00, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 订单状态, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_appoint_date (appoint_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体检预约订单表; CREATE TABLE t_report ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, doctor_name varchar(32) DEFAULT NULL COMMENT 总检医生, conclusion text COMMENT 体检结论, report_url varchar(256) DEFAULT NULL COMMENT PDF报告地址, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_id (order_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体检报告表;这段SQL里有几个值得注意的参数选择order_no设计为VARCHAR且与主键ID分离理由是用户看到的是订单号后台按主键ID做关联二者互不干扰生成规则可以用时间戳加随机数而非自增ID避免被遍历。appoint_date用DATE类型不要用DATETIME因为预约只精确到天时间段单独用VARCHAR维护。report_url建议存相对路径或者对象存储的key不要把完整HTTP地址写死否则后端换域名时历史数据全部失效。索引方面t_order表的核心查询是“查某个用户的预约列表”和“查某天有哪些预约”所以idx_user_id和idx_appoint_date是必加的两个普通索引。复合索引(user_id, status)在用户端按状态筛选时更高效但考虑到预约系统数据量不大单独索引足够表设计不要上来就堆索引写入压力会变大。2.3 状态流转的落库规则状态机不一定要引入单独的表或者状态机框架在SSM项目里用Service层方法控制流转即可。比如用户端只能发起“取消预约”操作后端要做两个校验该订单属于当前登录用户、当前状态为0-已预约。只有同时满足才允许执行UPDATE语句。后台管理员回传报告时前端请求里带上orderId和reportUrl后端校验该订单不是“已取消”然后更新t_order.status3并插入或更新t_report这两步必须放在一个事务里。3. SSM后端接口层从Controller到Mapper的请求链路怎么搭3.1 SSM三个框架各自负责什么SSM是Spring SpringMVC MyBatis的组合。在这套架构里Spring是容器管理Service层对象的创建和依赖注入SpringMVC是Web层框架负责把HTTP请求路由到Controller方法、完成参数绑定和JSON序列化MyBatis是持久层框架把Mapper接口的方法和XML里的SQL一一映射。三者各司其职但经常有新手把业务逻辑写在Controller里导致Controller又长又没法测试。常见的分层做法是Controller只做参数接收和结果封装Service写业务规则Mapper只做数据读写。依赖注入这块要特别注意Controller注入ServiceService注入Mapper全部通过Autowired或构造器注入完成。事务注解Transactional加在Service方法上不要加在Controller上因为Controller可能同时调用多个Service方法事务粒度不好控制。3.2 统一返回体ResultT和全局异常处理小程序端和后端交互最烦的就是每个接口返回格式不一样。有的返回codemsgdata有的直接返回裸JSON前端解析代码写了一堆判空。我做SSM接口第一步永远是写一个泛型返回体public class ResultT { private Integer code; // 0成功 非0失败 private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 0; r.msg success; r.data data; return r; } public static T ResultT error(Integer code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } // getter/setter 省略 }code用0表示成功避免用HTTP状态码糊弄。因为业务异常场景很多套餐已下架、预约时间已满、订单状态不允许取消这些都不是HTTP层面错误但必须给前端明确的业务码。data是泛型字段成功时放业务数据失败时为null。全局异常处理使用SpringMVC的RestControllerAdvice捕获BusinessException和兜底的Exception统一转成Result.error(...)返回避免出现堆栈信息直接喷到小程序端的情况。3.3 Controller层代码示例预约列表与取消预约以用户端最核心的两个接口为例拆开看参数设计和返回结构。RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; // 查询当前用户的预约列表 GetMapping(/list) public ResultListOrderVO list(RequestParam Long userId, RequestParam(required false) Integer status) { ListOrderVO list orderService.listByUser(userId, status); return Result.success(list); } // 取消预约 PostMapping(/cancel) public ResultVoid cancel(RequestParam Long orderId, RequestParam Long userId) { orderService.cancelOrder(orderId, userId); return Result.success(null); } }两个接口都显式传userId而不从Session取是因为后续接微信登录时要换成token解析用户身份开发现阶段用参数传递便于调试。status用required false标记为可选参数前端传了就按状态过滤不传就返回全部。OrderVO不是t_order的实体类而是聚合了套餐信息、报告状态等展示字段的视图对象避免把数据库字段直接暴露给前端。MyBatis的XML里对应listByUser的查询要用动态SQL处理可选的状态参数select idlistByUser resultTypecom.example.vo.OrderVO SELECT o.id, o.order_no, o.package_name, o.package_price, o.appoint_date, o.appoint_time, o.status, r.report_url, r.conclusion FROM t_order o LEFT JOIN t_report r ON o.id r.order_id WHERE o.user_id #{userId} if teststatus ! null AND o.status #{status} /if ORDER BY o.create_time DESC /select这里用LEFT JOIN而不是INNER JOIN因为已预约但未体检的订单没有报告记录普通JOIN会把它们过滤掉用户端就看不到“进行中”的订单了。if标签是MyBatis动态SQL最常用的写法test条件里写的是Mapper接口方法入参的属性名。3.4 后端跨域问题小程序端为什么不用CORS小程序和普通浏览器不一样它不是Web页面环境不执行CORS预检逻辑所以后端不需要配置CrossOrigin或全局CORS过滤器。小程序端请求是wx.request直接发出去的只要后端接口返回正常的JSON就能收到数据。这一点很多做浏览器前后端分离的开发者会搞混。但有一个坑要处理SSM项目如果用Tomcat部署接口路径如果带项目名比如/hospital/api/order/list小程序端request的URL必须写完整路径。建议后端配置servlet-mapping把路径映射为/api/*或者直接在Tomcat里把项目部署为ROOT这样请求路径就是/api/order/list简洁也少出错。4. 小程序端预约全流程的实现路径与关键函数4.1 小程序目录结构与request请求封装微信小程序的页面开发现在主流是两种原生WXML/WXSS/JS或者用uni-app框架写一套代码编译到多端。对于医院体检管理系统这个场景原生就够用因为页面数量一般不超过15个不需要跨端。目录上我会按功能分包pages/index放首页和套餐列表、pages/order放预约、pages/user放个人中心。request请求封装是必须做的一步因为每个页面都要调后端接口不可能每个页面都写一遍wx.request完整参数。封装的核心是统一baseURL、自动附带token、统一处理业务码。// utils/request.js const BASE_URL https://your-domain.com/api; function request(url, method, data, needAuth true) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, Authorization: needAuth ? wx.getStorageSync(token) : }, success(res) { if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request, BASE_URL };这段封装里有两处容易忽略的逻辑一是needAuth参数登录接口本身不能带无效token否则会被后端拦截导致死循环二是业务码为0时才走resolveHTTP层返回200但业务失败的情况也一并兜住了。BASE_URL不要写在业务页面里后期换域名只需要改这一个文件。微信开发者工具中要把“不校验合法域名”勾上才能本地联调真机预览则必须在后台配置request合法域名。4.2 预约流程从套餐选择到订单提交的页面逻辑预约是这个小程序的核心操作流。用户在套餐列表页选择套餐进入详情页确认项目和价格点击“立即预约”弹出日期选择器选完日期后调用后端创建订单接口。这里注意一个细节小程序的日期选择器picker的modedate可以直接使用但需要限制可选范围不能选过去的日期。// pages/order/create.js Page({ data: { packageId: , packageName: , packagePrice: , appointDate: , minDate: this.getTodayStr() }, getTodayStr() { const d new Date(); return ${d.getFullYear()}-${d.getMonth()1}-${d.getDate()}; }, onDateChange(e) { this.setData({ appointDate: e.detail.value }); }, submitOrder() { const { packageId, appointDate } this.data; if (!appointDate) { wx.showToast({ title: 请选择预约日期, icon: none }); return; } const request require(../../utils/request.js).request; request(/order/create, POST, { packageId: packageId, appointDate: appointDate }).then(data { wx.redirectTo({ url: /pages/order/list }); }); } })submitOrder里的逻辑值得展开说明点击提交后前端只做了“日期非空校验”真正的业务校验全部交给后端。比如套餐是否下架、当天预约人数是否满、用户是否重复预约这些判断放在后端Service层前端不做重复逻辑避免用户绕过页面直接调接口。预约成功后跳转到订单列表页用户能看到新订单状态为“已预约”。4.3 体检报告的展示结构化数据优先于PDF文件报告回传这块有两个极端做法一种只传一个PDF链接小程序端用web-view组件打开看但预览体验差、加载慢另一种把每项指标都结构化存储前端一行一行渲染。实操中折中方案是报告结论、医生建议、总检签名等关键信息结构化存储明细项目数据量大且格式固定用PDF文件存储。小程序端优先展示结构化数据提供PDF下载入口。web-view组件要绑定商务号或已认证的小程序才能打开外部网页个人开发者在开发环境会受限。所以报告展示页面尽量用原生组件渲染后端返回的JSON数据只在需要在线预览完整报告时才跳转web-view。后端返回的报告内容按照conclusion、advice、items三段式设计其中items是JSON数组里面每一项包含itemName、result、unit、refRange和flag偏高/偏低/正常前端用wx:for循环渲染表格。5. 联调避坑与上线前必须检查的6个配置点5.1 token的过期与静默刷新小程序端最容易忽略的一环体检系统的用户可能几周才打开一次小程序微信的wx.login拿到的code换取的session_key有效期不固定后端签发的token如果过期用户操作时就会突然被踢下线。建议的联调方案是小程序启动时先检查本地存储的token如果没有或已过期就调用wx.login拿code发给后端换取新token。后端要在/api/auth/login接口中同时校验code和用户是否存在不存在则自动注册这样用户无感知完成登录。这里有一个从后端视角看的经验值token的有效期不要设置太长24小时最合适。用户在预约周期内可能会多次打开小程序每次启动都会刷新token自然不会中断体验。后端拦截器要做“白名单”处理/api/auth/**放行其余接口全部校验token缺少token或token过期时返回401小程序端request封装统一跳转到登录页。5.2 上线前配置检查清单微信小程序不是写完代码就能上线的有很多配置项必须在开发者后台和技术后台同时完成。下面这份清单是我每次发布前都会过一遍的配置项配置位置检查要点request合法域名微信公众平台-开发管理必须是HTTPS不能带路径和端口ICP备案需完成业务域名微信公众平台-开发管理仅当使用web-view时必配服务器HTTPS证书后端服务器小程序端强制HTTPS自签名证书不可用后端接口路径小程序代码BASE_URL确认是线上域名不是localhost数据库连接信息后端jdbc.properties确认线上库地址、账号、密码已替换体验版二维码微信开发者工具上传代码后设为体验版管理员扫码测试完整流程第六项容易被忽略appid要换成正式的不要用测试号测试号的wx.login拿到的code在后端换不到有效的session。真机预览时开发者工具的调试模式可以用“不校验合法域名”但体验版和正式版都不行。5.3 两个高频联调报错的排查方向联调时最常遇到的报错是request:fail。这个错误几乎都是域名问题大概率是合法域名没配、HTTPS证书失效或者域名根本没有备案成功的.icu这类后缀域名。排查方法是先用浏览器访问后端接口地址能返回JSON说明服务端正常问题出在小程序侧的域名校验。另一个高频报错是后端能收到请求但返回code500这时优先看后端控制台日志不要在小程序端瞎试。常见原因是数据库时间字段格式不符小程序传的日期是2025-06-30字符串MyBatis映射到java.util.Date时报转换异常解决办法是POJO里时间字段用String接收或者SQL里用STR_TO_DATE函数转换。本文还有配套的精品资源点击获取