简介:面向医疗信息化开发者的全栈医疗云平台源码包,以SpringBoot 2.x与Uniapp为核心,复刻京东健康、好医生模式,覆盖在线问诊、私人医生定制、电子处方审核、模拟医保购药、病患跟踪与智能用药提醒等完整业务链。平台拆分为admin-ui后台管理(Vue+ElementUI)、app移动端前台(Uniapp)、elt-server后端API(SpringBoot)三大模块,适合学习中大型医疗系统架构、权限设计及跨端开发的初中级工程师。整包共2000个文件,其中1018个Java文件实现后端服务与业务逻辑,500个Vue文件支撑管理端和PC页面,297个JS文件处理交互逻辑,另有SQL、XML等数据库与配置脚本,压缩后约79.73MB。项目附带MySQL 8.0与Redis相关设计,Swagger文档辅助接口调试。资源包结构清晰,代码、数据库、APP工程齐备,可直接导入运行并二次开发。已有166人学习使用,对想从单体应用进阶到平台级医疗项目的开发者而言,是兼顾业务理解与工程实践的合适参考。
1. 把在线问诊和私人医生定制装进一个医疗云平台:这份 SpringBoot + Uniapp 源码该怎么拆
很多人拿到这套仿京东健康与好医生的医疗云平台源码,第一反应就是把后端用 spring-boot:run 拉起来,再在 HBuilderX 里运行 Uniapp 到浏览器看首页效果。我的习惯恰恰相反——先翻数据库脚本,把用户、医生、问诊单、处方这几张核心表的关系理清楚,再去碰接口和页面。原因不复杂:在线问诊和私人医生定制这两个核心业务,本质上都是围绕「患者—医生—订单—问诊记录」这条数据链在转,SpringBoot 端做得再花哨,Uniapp 端再顺滑,最后都得落到数据库表结构和状态流转上。这套源码覆盖后台管理、PC 端和手机 APP 三端,适合做毕设、做中小诊所的线上问诊原型,也适合想快速理解多端医疗业务闭环的开发者。这篇笔记我把拆项目的完整路径写下来:建表、后端接口、前端闭环、避坑、验证,一路走完。
2. 先看数据库再谈功能:用户、医生、问诊单、处方四张核心表怎么建
在线问诊的业务闭环是这样走的:患者注册登录、选科室或直接选医生、提交问诊单并支付、医生接单、图文或视频问诊、医生开处方或给建议、问诊单关闭。全程涉及的核心实体包括用户、医生扩展信息、问诊单、问诊记录、处方、私人医生服务包、支付订单。这套源码的数据库脚本一般在项目根目录的 sql/ 目录下,也可能在 doc/ 目录下,文件名通常是 init.sql 或 medical_cloud.sql,具体位置看你下载的版本。执行时我建议用 Navicat 或命令行 source 导入,跑通后再人工核对关键表的字段名,因为不同版本的字段注释和命名风格会有差异。
2.1 数据域划分:从患者下单到医生接单的实体关系
先讲这套源码为什么要分两个订单域。问诊单和支付订单是分开的两张表,这个设计是有意为之。问诊单关心的是医疗进度:待接诊、问诊中、待开方、已完成;支付订单关心的是钱:金额、支付状态、退款状态。两者混在一张表里,初看省事,后面做对账、统计医生收入、处理退款时会非常痛苦。这套仿京东健康与好医生的源码把这两个域分得清楚,支付订单表通过业务单号关联问诊单表,是医院类项目里比较标准的结构。
从实体关系看,sys_user 是登录层的主表,通过 user_type 区分患者、医生、管理员;doctor_info 通过 user_id 一对一扩展医生的执业信息;consult_order 是问诊业务的核心表,patient_id 指向患者,doctor_id 指向医生,dept_id 指向科室;问诊记录和处方都挂在 consult_order 下面,属于明细表。私人医生定制则多出 service_pack 和 patient_doctor_bind 两张表,后者把患者和医生形成周期绑定。这套关系看清后再去读后端代码,基本上能猜出每个 Service 在干什么。
2.2 建库建表:核心 SQL 与字段约束
先看用户表。这张表同时服务患者端、医生端和管理后台,登录校验全靠它。建表语句里几个字段的取舍直接决定了后面权限逻辑怎么写。
-- 用户表:同时服务 C 端患者和 B 端医生账号的登录基础 CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT 'BCrypt 加密后的密码', phone VARCHAR(20) DEFAULT NULL COMMENT '手机号,也可用于登录', real_name VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', user_type TINYINT NOT NULL DEFAULT 0 COMMENT '0-患者 1-医生 2-管理员', avatar VARCHAR(255) DEFAULT NULL COMMENT '头像地址', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-启用 0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_type (user_type), KEY idx_phone (phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';user_type 是权限分流的根,登录后跳患者端还是医生后台都由它决定。password 字段要保留 BCrypt 格式,后台登录校验默认走 Spring Security 的 BCryptPasswordEncoder,你要是手动往表里插测试数据,密码也得先跑一次 BCrypt 加密,否则永远登录不进去。idx_user_type 和 idx_phone 这两条索引在数据量大时能顶住按角色筛选和手机号登录的查询,别删。
接下来是医生扩展表。它和 sys_user 是一对一关系,核心是价格字段,前端下单页展示的价格、后端计算订单金额都从这里取。
-- 医生信息扩展表:一对一关联 sys_user,存放执业与排班信息 CREATE TABLE doctor_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL UNIQUE COMMENT '关联 sys_user.id', dept_id BIGINT NOT NULL COMMENT '科室ID', title VARCHAR(30) DEFAULT NULL COMMENT '职称,如主任医师', hospital VARCHAR(100) DEFAULT NULL COMMENT '所属医院', intro TEXT COMMENT '医生简介', service_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '图文问诊价格', vprice DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '私人医生定制价格', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-接诊中 0-停诊', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_dept_id (dept_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生扩展信息表';service_price 和 vprice 是两个容易被忽略但很关键的字段。前者是图文问诊单价,后者是私人医生定制服务包价格。改动价钱的代价不只是前台显示变化,已下单未支付的历史订单会用旧价格还是新价格,代码里必须定好规则。我见过有人在运营后台改一次价格,把正在问诊的订单金额也带偏了,最后只能手动改库,这是标准的翻车场景。
问诊单主表是整条链路的枢纽,字段最多,状态枚举值必须和代码里的枚举类一一对应。
-- 问诊单主表:一条问诊单承载一次完整的在线问诊服务 CREATE TABLE consult_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号,对外展示', patient_id BIGINT NOT NULL COMMENT '患者 sys_user.id', doctor_id BIGINT NOT NULL COMMENT '医生 doctor_info.id', dept_id BIGINT NOT NULL COMMENT '科室ID', consult_type TINYINT NOT NULL DEFAULT 1 COMMENT '1-图文 2-视频 3-私人医生', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待接诊 1-问诊中 2-待开方 3-已完成 4-已取消', symptom_desc VARCHAR(1000) DEFAULT NULL COMMENT '患者症状描述', amount DECIMAL(10,2) NOT NULL COMMENT '实付金额', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_patient (patient_id), KEY idx_doctor (doctor_id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='在线问诊单';status 字段的 5 个枚举值把整条业务链串起来。待接诊时患者端显示取消按钮,问诊中显示聊天入口,待开方时等医生提交处方,已完成整单锁定。后端状态机代码、前端按钮显隐都以这个字段为准。如果初始化脚本里没有写清楚这 5 个枚举值,联调时你一定会遇到「状态不该变却变了」「按钮该亮不亮」的怪事。consult_type 的 1/2/3 三个值也在下单流程里决定价格和页面跳转,注意在代码里保持一致。
2.3 表关系梳理与两个常见设计误区
把四张表的关系在纸上画出来:sys_user 是一切入口,doctor_info 扩展医生档案,consult_order 记录每次问诊交易,问诊记录和处方表挂在 consult_order 下层。典型的主从结构,逻辑清晰。
这套源码里有两个常见误区,你在二次开发时要躲开。第一个是把问诊单和支付订单合并成一张表,省了关联查询,却把对账、退款、收入统计都变成噩梦。第二个是状态字段用裸字符串到处飘,比如 'WAIT_DOCTOR'、'FINISHED' 散落在代码里,改成枚举类收口是必须的。执行完 SQL 脚本后,确认一下四张表的行数和关键字段,然后改 application.yml 里的数据库连接串,再启动 SpringBoot。看到日志里 Tomcat started on port 8080,后端就绪,下一步进后端核心模块。
3. SpringBoot 后端:在线问诊状态机与私人医生定制接口的落地
数据库表建好后,后端最有看头的就是问诊单状态机和私人医生定制两个模块。先说状态机。整个在线问诊业务中,问诊单不是一个普通的增删改查对象,它的状态流转有严格的先后顺序约束,代码里必须体现「不能从待接诊直接跳到已完成」这层控制。
3.1 问诊单状态机:五个状态的流转与边界校验
这套仿京东健康与好医生系统的源码里,状态机落在一个枚举类上,所有状态变更都走统一的 Service 方法,而不是在外面随便 update set status。枚举类把数据库里的 TINYINT 值和业务语义绑定,杜绝魔法数字。
// 问诊单状态枚举:状态变更的唯一依据 public enum ConsultOrderStatus { WAITING(0, "待接诊"), IN_PROGRESS(1, "问诊中"), PENDING_PRESCRIPTION(2, "待开方"), FINISHED(3, "已完成"), CANCELED(4, "已取消"); private final int code; private final String desc; ConsultOrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } }这个枚举的核心作用是把数据库字段和业务场景绑定,后面接收前端传来的状态值时,先通过枚举校验合法性,再进入业务判断,能挡掉一大批非法状态跳变的问题。接下来看接诊接口,这是医生端最常用的操作。
// 医生接诊:待接诊 -> 问诊中 @Transactional public boolean acceptConsult(Long consultId, Long doctorId) { ConsultOrder order = consultOrderMapper.selectById(consultId); // 归属校验:只能接自己的单 if (!order.getDoctorId().equals(doctorId)) { throw new BizException("无权操作该问诊单"); } // 状态校验:只有待接诊可以流转到问诊中 if (order.getStatus() != ConsultOrderStatus.WAITING.getCode()) { throw new BizException("当前状态不可接诊"); } order.setStatus(ConsultOrderStatus.IN_PROGRESS.getCode()); return consultOrderMapper.updateById(order) > 0; }这段代码有两个点值得说。第一,@Transactional 必须加,因为接诊成功后经常要顺带写一条问诊会话记录,两步必须在一个事务里,否则会出现问诊单状态已经是问诊中、会话记录却是空的脏数据。第二,归属校验不能省。我在实际跑这套代码时就踩过这个坑——不校验 doctorId 的话,任何登录后台的医生都能接别人名下的单,在医疗场景里属于严重事故。
问诊完成也有对应的状态校验,通常是在医生提交处方之后,状态从待开方流转到已完成。所有状态变更方法都遵循同样的模式:先查当前状态,再判断能否流转,最后更新。
3.2 私人医生定制:服务包、绑定关系与剩余次数扣减
私人医生定制是这套平台区别于普通在线问诊的核心卖点。业务模式是患者不按次问诊,而是买一个周期服务包,比如 30 天内不限次图文咨询加上定期随访,然后和某个医生建立专属绑定关系。这个模块在数据库层由 doctor_service_pack 和 patient_doctor_bind 两张表支撑,前者定义价格与周期,后者记录患者和医生的绑定及剩余次数。
购买绑定接口的关键约束是「未支付不生效」。绑定关系必须等支付回调成功后写入,而不是在创建订单时就生成,否则用户不付款也能享受服务。
// 患者购买私人医生服务包:创建支付订单,支付回调后再写绑定关系 @Transactional public Map<String, Object> createPrivatePackOrder(Long patientId, Long packId, Long doctorId) { // 1. 查服务包,校验是否在售 DoctorServicePack pack = packMapper.selectById(packId); if (pack == null || pack.getStatus() != 1) { throw new BizException("服务包不存在或已下架"); } // 2. 创建支付订单,状态置为待支付 PayOrder payOrder = new PayOrder(); payOrder.setOrderNo("PD" + System.currentTimeMillis()); payOrder.setUserId(patientId); payOrder.setAmount(pack.getPrice()); payOrder.setStatus(0); payOrderMapper.insert(payOrder); Map<String, Object> result = new HashMap<>(); result.put("orderNo", payOrder.getOrderNo()); result.put("payAmount", payOrder.getAmount()); return result; }支付回调里的逻辑一般在单独的 PayCallbackService 里,核心动作是查支付订单、核对金额、然后插入 patient_doctor_bind 绑定关系。插入绑定时要算好服务周期:开始日期是当天,结束日期是当天加上服务包的 validDays,剩余次数 initialCount 初始化为服务包里的 serviceCount。这个模块最容易出问题的地方是剩余次数扣减,患者每次通过私人医生入口发起问诊,都要先校验绑定状态、再扣减剩余次数,这两个操作必须事务包住或用乐观锁。并发请求下如果两个事务同时读到剩余次数 0,就会扣成 -1,这是很经典的并发脏数据场景。
注意:支付回调是资金入口,这里不要只拼数据库更新,建议把回调处理写成幂等的。同一笔支付回调可能被支付渠道推送多次,接口里要先按 orderNo 查一下绑定关系是否已存在,存在就直接返回成功,不做重复插入。
3.3 统一返回结构与接口联调约定
后端所有接口走统一的 Result 返回结构,包含 code、msg、data 三个字段,前端请求封装先判断 code 是否为 200,不是就直接弹 msg。这套源码的接口风格是 RESTful,路径类似 /api/user/login、/api/consult/list、/api/doctor/search。联调时最需要注意 token 传递:登录成功后返回 token,前端后续每个请求都把它放在请求头 Authorization 里,后端用拦截器校验。如果拿 Postman 测接口,记得在 Headers 里手动加上 Authorization: Bearer 你的token,否则会一直碰到 401。
我在看这套源码时还有一个小建议:先把 Controller 层全部扫一遍,把接口路径和请求方法列表拉出来,和前端 pages.json 里的请求路径比对一次。很多联调问题不是逻辑错,而是路径里的下划线、大小写不一致,导致前端 404。这种问题在 H5 端不显眼,打包成 APP 后更加隐蔽,不如一开始就做一次接口与路由核对。
4. Uniapp 手机端:一个工程跑 APP、H5、小程序的问诊闭环
移动端用 Uniapp 写,工程结构就是标准的 HBuilderX 项目,pages.json 管理页面路由,manifest.json 管理多端配置。为什么选 Uniapp 而不是原生?因为这类业务天然需要同时覆盖患者 APP、医生端、微信端,一套 Vue 语法打包出三个产物,对中小团队性价比最高。第一次拿到项目,我建议先用 HBuilderX 导入,运行到浏览器看 H5 效果,确认页面正常后再考虑打包 APP 或微信小程序。
4.1 manifest.json 多端配置:APP、H5、小程序各调各的
多端配置集中在 manifest.json。这里最带坑的是两个:微信小程序 appid 要换成自己申请的,H5 路由模式要选对。
{ "name": "综合医疗云平台", "app-plus": { "usingComponents": true, "nvueStyleCompiler": "uni-app", "compilerVersion": 3, "distribute": { "android": { "targetSdkVersion": 30, "minSdkVersion": 21, "abiFilters": ["armeabi-v7a", "arm64-v8a"] } } }, "h5": { "router": { "mode": "hash" } }, "mp-weixin": { "appid": "wx你的appid", "setting": { "urlCheck": false } } }h5.router.mode 建议用 hash,不要轻易改 history。原因很实际:如果后端接口做了跨域限制,history 模式刷新二级页面会 404,hash 模式没有这个问题。targetSdkVersion 30 是 Android 10 以上的要求,以后要上应用市场,这个值一般不能低于 30。minSdkVersion 21 表示最低支持 Android 5.0,如果业务不需要兼容老设备,提到 23 或 24 能少做很多兼容适配。mp-weixin 里的 urlCheck 在开发调试阶段可以先设为 false,不然真机上请求非白名单域名会被微信拦截。
4.2 问诊流程前端实现:提交问诊单、跳转支付与图文会话
前端问诊流程对应后端三个接口:提交问诊单、创建支付订单、拉取会话消息。页面上患者从医生详情页进入,选择问诊类型、填写症状描述、提交后跳支付页,支付成功进入会话页。核心逻辑在提交问诊单这个方法里。
// 提交问诊单:表单校验 + 调用后端创建接口 async submitConsult(formData) { // 表单校验:症状描述必填,且不能超过 500 字 if (!formData.symptomDesc || formData.symptomDesc.trim().length === 0) { uni.showToast({ title: '请填写症状描述', icon: 'none' }); return; } if (formData.symptomDesc.length > 500) { uni.showToast({ title: '症状描述不能超过500字', icon: 'none' }); return; } const res = await this.$http.post('/api/consult/create', { doctorId: formData.doctorId, deptId: formData.deptId, consultType: Number(formData.consultType), symptomDesc: formData.symptomDesc.trim() }); if (res.data.code === 200) { // 创建成功,携带 consultId 跳转支付页 uni.navigateTo({ url: `/pages/pay/index?consultId=${res.data.data.consultId}&amount=${res.data.data.amount}` }); } else { uni.showToast({ title: res.data.msg, icon: 'none' }); } }这里有几个细节我一般会重点盯。第一,字数限制前端先做一层,后端还会再校验一次,两处都别省。第二,提交成功后跳转支付页只带 consultId 和 amount,不要在支付页重新向后端要价格,金额以后端返回为准,防止页面被篡改价格。第三,consultType 是数字 1/2/3,前端下拉选择后要转成 Number 再提交。我见过不少人字符串和数字没转换,后端一比对类型不一致,直接报参数错误,这个坑很隐蔽。
图文会话页面是交互最重的页面,通常用 scroll-view 渲染消息列表,数据通过轮询或 WebSocket 拉取。源码里如果用的是轮询,你要特别留意定时器清理逻辑——页面 onUnload 时必须 clearInterval,否则页面退出后还在请求,既浪费流量又可能造成内存泄漏。H5 浏览器里偶尔出问题不明显,一打成 APP 挂在后台,耗电和流量都很可观。
4.3 请求封装与登录态保持:拦截器里藏着的两个坑
前端请求封装在 utils/request.js 里,拦截器做的事主要有三件:自动带 token、统一处理业务错误码、网络异常提示。
// 请求拦截器:统一处理 token 与业务错误码 this.$http.interceptors.request.use(config => { const token = uni.getStorageSync('token'); if (token) { config.header.Authorization = `Bearer ${token}`; } return config; }); this.$http.interceptors.response.use(res => { if (res.data.code !== 200) { // 登录过期,清空 token 并跳回登录页 if (res.data.code === 401) { uni.removeStorageSync('token'); uni.reLaunch({ url: '/pages/login/index' }); } uni.showToast({ title: res.data.msg, icon: 'none' }); return Promise.reject(res.data); } return res.data; });拦截器写法本身没有玄学,但有两个坑值得说。第一是 token 的存储 key 要全局统一,有人用 token,有人用 access_token,存储和读取的 key 不一致,登录后请求还是 401,这是最常见的低级错误。第二是 401 处理别用 uni.navigateTo 跳登录页,如果页面栈很深,返回层级会错乱,我习惯用 uni.reLaunch 清掉整个页面栈再进登录页。这两个细节在真机调试时最容易暴露,H5 上反而看不出来。
4.4 微信小程序打包与真机调试注意点
运行到微信小程序时,需要在小程序开发者工具里导入项目,这时候 manifest.json 里的 mp-weixin.appid 必须换成真实的,否则预览会报invalid appid。另外,Uniapp 项目里如果涉及用户头像、手机号等组件,在小程序端要使用微信生态的授权组件,写法跟 H5 不一样,这点容易忽略。
注意:H5 端跑通了不代表 APP 端没问题。真机调试时,接口地址要从 localhost 改成局域网 IP 或正式域名,iOS 的 ATS 策略还要求 HTTPS。建议在项目里单独建一个 config.js 集中管理接口 baseURL,按环境切换,避免到处改字符串。
5. 避坑记录:从建库到打包最容易翻车的五个点
这个项目从数据库到前端打包,踩坑点比我想象的多。下面这五条是我复现这套源码时真实遇到的,按「现象、原因、解决」记下来,供你对照排查。
5.1 数据库连不上:serverTimezone 与 allowPublicKeyRetrieval
现象:SpringBoot 启动时日志报Communications link failure,或者能连但查询时间比本地时间差 8 小时。
原因:MySQL 8.x 默认时区是 UTC,连接串里没加 serverTimezone,驱动会按服务器会话时区处理,导致时间错乱;allowPublicKeyRetrieval 参数缺失则会在首次连接时报Public Key Retrieval is not allowed。
解决:统一在 application.yml 里按下面的写法配置:
spring: datasource: url: jdbc:mysql://localhost:3306/medical_cloud?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true这两个参数一起写,是我接 MySQL 8 项目时第一时间确认的事。遇到连接类报错,别急着怀疑代码,先看连接串。
5.2 H5 跨域:allowedOrigins 和 allowCredentials 的冲突
现象:H5 页面在浏览器里访问,后端日志没有任何报错,但浏览器 console 里全是 Access-Control-Allow-Origin 相关的红色错误。
原因:前端跑在 5173 或 HBuilderX 内置浏览器端口,后端跑在 8080,不同源。后端 CORS 配置写的是allowedOrigins("*"),同时又开了allowCredentials(true),这两个配置是互斥的,浏览器会直接拦截。
解决:把 allowedOrigins 改成 allowedOriginPatterns,这是 Spring 5.3 之后专门解决通配符加凭证冲突的写法。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这个配置类放在后端任意一个被扫描的包下即可生效。跨域问题排查的通用步骤是:先看浏览器报错原文,再确认后端有没有输出 OPTIONS 请求日志,最后对 CORS 配置。别一上来就怀疑前端封装。
5.3 真机访问不了 localhost:Android 明文流量被拦
现象:H5 端调试一切正常,打成 APK 装到手机后,所有接口都超时或报错。
原因:真机访问不了 localhost。开发时后端接口写的是http://localhost:8080,在手机里这个地址指向手机自己。另外 Android 9.0 及以上默认禁止明文 HTTP 流量,就算把地址改成局域网 IP,也会被系统的网络安全策略拦下来。
解决:接口地址抽到 config.js 统一管理,真机调试时改成局域网 IP,如http://192.168.x.x:8080;打包前确认打包配置里能容忍开发环境的明文请求。正式发布就用 HTTPS 域名。Android 明文流量的问题,如果打包工具没法全局放开,可以通过网络安全配置只允许特定域名明文访问,但这套源码一般不需要搞那么细。
5.4 SpringBoot 版本太高:javax 与 jakarta 的迁移大坑
现象:项目使用的是 SpringBoot 2.x 源码,换成 SpringBoot 3.x 依赖后,编译报错说很多类找不到,或者运行时 NoClassDefFoundError。
原因:SpringBoot 3.x 把 javax.* 迁移到了 jakarta.*。如果源码里大量包名是 javax.servlet、javax.validation,依赖却是 3.x,整套代码跑不起来。
解决:先看 pom.xml 的 parent 版本。我的做法是:如果源码是 SpringBoot 2.x,不要轻易升 3.x,保持 2.7.x 这个 2 系最终版本,配合 mybatis-spring-boot-starter 2.x。如果要升 3.x,得把所有导入 javax 的文件批量改成 jakarta,这不止是替换 import 那么简单,有些第三方库本身就没适配 3.x,建议刚拿到项目时不要折腾升级。版本问题最磨人,保留和源码一致的版本永远是最稳的。
5.5 问诊单状态被跳过:前端隐藏按钮治标不治本
现象:患者没支付,后端就把问诊单置成问诊中;或者医生还没接单,患者端就显示了聊天入口。
原因:前端在某个事件里直接调用了改状态的接口,绕过了状态机校验;或者后端 Service 层没做状态前置校验,接诊接口入口处也不检查当前状态。
解决:所有状态变更必须走统一 Service 方法,方法内先查当前状态,再判断能否流转到目标状态。别指望前端按钮控制来兜底——前端的隐藏只是用户体验,后端的校验才是安全底线。我在改这类系统时习惯把每个状态的可达目标状态列成一张表,逐个 Service 方法对照,很快就能找出漏掉校验的位置。
6. 跑通全流程的验证清单:六个检查点一次过
拿到这套源码后,单靠页面点击很难确认整个链路没问题。我每次从零部署一套多端代码,都会强制自己按下面这六个检查点走一遍:
| 检查点 | 操作 | 预期结果 |
|---|---|---|
| 1. 数据库初始化 | 执行 init.sql | 出现 sys_user、doctor_info、consult_order 等核心表 |
| 2. 后端启动 | mvn spring-boot:run | 日志出现 Tomcat started on port 8080 |
| 3. 登录接口 | Postman POST /api/user/login | 返回 token,且 user_type 正确 |
| 4. 创建问诊单 | POST /api/consult/create | 返回 consultId 和金额 |
| 5. 模拟支付回调 | 手动调用支付回调接口 | 问诊单状态变问诊中,私人医生绑定期生效 |
| 6. 前端真机预览 | HBuilderX 运行到手机 | 完整走一遍下单、会话、开方流程 |
第 5 个检查点是最容易被忽略的。支付回调是个黑匣子,前端支付页只是一个跳转壳,真正决定数据状态的是后端回调接口。我建议你先把支付流程里的回调地址改成自己可调用的测试地址,然后用 Postman 手动触发一次回调,看状态字段是否按预期变化。这一步能提前暴露很多问题,比如回调幂等没做、绑定关系重复插入、回调里查不到订单。等这套跑通了,再用微信支付或支付宝的沙箱环境走真实回调。
六个检查点全部通过后,我会再做一件小事:把后端的统一返回结构打印一份完整日志,模拟一个前端传错参数的场景,确认错误码和提示能对应上。这样后面前端同事对接时,至少不会出现「跳转支付页但没带 consultId」这种全链路断档的低级失误。
从那以后,我每次拿到一套多端医疗源码,都强制自己把这个验证清单从头走一遍,尤其是支付回调这个最容易藏问题的环节。前端界面再好看,最后真正扛住线上需求的,还是数据状态和接口边界这两层。希望帮到你。
本文还有配套的精品资源,点击获取