简介:医疗信息化是数字化转型的重要领域,在线问诊、电子处方、健康档案管理等系统需求快速增长。从技术视角看,SpringBoot作为主流的后端开发框架,凭借快速构建RESTful接口和成熟的生态体系,成为支撑业务逻辑的首选;Uniapp则通过一套代码多端编译,显著降低APP与H5的研发成本,二者结合能够高效实现远程医疗场景下的完整业务闭环。本文详细解析了基于SpringBoot与Uniapp打造的综合医疗云平台,涵盖在线问诊状态机、私人医生会员体系、排班管理、电子处方流转、支付回调幂等处理等核心设计,并给出了数据库表结构、后端鉴权、WebSocket消息推送及跨端适配的工程实践方案,为医疗项目研发和全栈开发者提供了一套可复用的参考路径。 这两年医疗健康赛道的系统需求特别多,从在线问诊到私人医生服务,产品形态越来越重,光靠一个后台管理加一个简单的H5页面根本撑不起完整业务闭环。我之前完整落地过一个基于SpringBoot + Uniapp的综合医疗云平台,仿的就是京东健康、好医生这类头部产品的核心流程,包含后台管理、PC端、手机APP三端,集成了在线问诊、私人医生定制服务、电子处方、健康档案管理这些模块,代码和数据库都有完整沉淀。如果你正准备做医疗类项目,或者想参考一套可复用的全栈方案,这篇文章应该能帮你省掉大量调研和踩坑的时间。
先说下这套系统能做什么:用户端APP负责挂号问诊、图文咨询、私人医生服务购买,医生端可以接单、回复问诊、开处方,后台管理负责科室医生管理、排班、订单统计、内容管理,PC端则面向运营人员和医生工作站。技术栈选了SpringBoot做后端服务,Uniapp做跨端APP,MySQL存业务数据,Redis扛热点缓存,前后端分离,接口统一走RESTful风格。规模上,这是一套适合小团队自研或者作为毕业设计、课程设计升级版的完整方案。
1. 项目整体设计与架构思路
1.1 医疗云平台的核心业务闭环
做医疗平台和做电商平台最大的区别在于:业务链路长、角色多、数据敏感。一个完整的在线问诊流程,从用户发起问诊到最终药品配送,中间要经过选科室、选医生、支付、医生接诊、图文对话、开处方、审方、配送多个环节,任何一个节点断了,整个体验就废了。
我先梳理一下这套系统要跑通的几个核心闭环:
- 在线问诊闭环:用户提交问诊单 -> 系统分配医生 -> 医生接诊 -> 图文/视频沟通 -> 医生开处方 -> 药师审方 -> 用户查看电子处方 -> 选择购药或线下自取
- 私人医生服务闭环:用户浏览服务套餐 -> 下单购买 -> 系统分配专属医生团队 -> 建立健康档案 -> 按周期随访 -> 服务到期提醒续费
- 运营管理闭环:管理员维护科室医生 -> 排班设定 -> 用户预约 -> 统计问诊量 -> 佣金结算
这三个闭环对应了三种不同的设计思路。问诊链路是核心中的核心,必须把状态机设计清楚;私人医生更偏会员订阅制,要有服务周期管理的概念;运营管理则是标准的后台CRUD加报表统计。
1.2 技术选型:为什么是SpringBoot + Uniapp的组合
这套组合在医疗类项目里非常成熟。SpringBoot生态完善,做微服务也好、单体也好,上手成本低,接第三方支付、短信、阿里云OSS都很方便,社区资料多,遇到问题几乎都能搜到解决方案。Uniapp则解决了多端复用的问题,一套代码同时编译到iOS、Android、H5,对中小团队来说节省的成本非常可观。
具体选型上:
- 后端:SpringBoot 2.7.x + MyBatis-Plus + Spring Security + JWT + Redis + RabbitMQ(可选)
- 前端AUniapp + Vue2(也可以用Vue3版本)+ uView UI组件库
- 管理后台:Vue2 + Element UI(独立PC端项目)
- 数据库:MySQL 5.7+,表结构用InnoDB引擎,字符集utf8mb4
- 文件存储:阿里云OSS(头像、处方图片、问诊图片)
- 接口文档:Swagger/Knife4j自动生成
为什么不用Spring Cloud微服务?医疗平台如果业务量没到千万级,单体应用加缓存、加异步队列完全够用,拆得太细反而增加部署和运维成本。这套系统我的建议是做成模块化的单体应用,按业务分包(user、doctor、order、consult、prescription、admin等),后续真要拆微服务也方便。
1.3 三端协同:后台管理、PC端、APP怎么分工
三端不能各做各的,职责必须清晰。用户APP面向患者,只处理与用户相关的功能——注册登录、找医生、问诊、订单、档案。医生端APP与用户端同源,通过角色切换进入工作台处理接诊事宜。PC管理后台面向平台运营人员,管理医生入驻审核、排班、敏感词过滤配置、订单处理、财务统计。
接口设计上要注意权限隔离。用户端接口统一走 /api/user/前缀,医生端走 /api/doctor/前缀,管理后台走 /api/admin/前缀,各自有独立的Spring Security配置链,JWT里带上角色信息,拦截器做二次校验。这样三端共用一个后端服务,但权限边界清晰,不会出现用户拿着自己的token调管理接口的情况。
2. 核心功能模块与关键设计
2.1 在线问诊的完整状态机设计
在线问诊是整套系统的命脉,我花了最多精力设计它的状态流转。问诊单在不同角色操作下要经历多个状态,如果只靠一个status字段用魔法数字到处判断,代码很快就乱成一团。
我最后用了一套前后端统一的枚举体系:
public enum ConsultStatus { PENDING_PAY(0, "待支付"), WAIT_ASSIGN(1, "待分配"), WAIT_ACCEPT(2, "待接诊"), CONSULTING(3, "问诊中"), WAIT_PRESCRIPTION(4, "待开方"), WAIT_AUDIT(5, "待审方"), COMPLETED(6, "已完成"), CANCELED(7, "已取消"), REFUNDING(8, "退款中"), REFUNDED(9, "已退款"); }设计状态机时有几个关键决策:
- 待支付状态独立存在。用户提交问诊单后先锁定医生和时段,15分钟内不支付自动取消,释放医生资源
- 待分配和待接诊拆成两个状态。因为有的用户指定医生,有的走平台智能分配,分配逻辑和医生接单逻辑要分开处理
- 开方和审方必须分开。医生开完处方不能直接生效,要经过药师端审核,这是医疗合规的基本要求,哪怕系统里做的是简化版流程,状态机上也要留出这一步
问诊过程中的通信我用了WebSocket做实时消息推送,后端通过Spring的SimpMessagingTemplate向指定用户推送新消息、订单状态变更通知等。同时保存一份消息记录到数据库,保证用户刷新页面后历史消息不丢。
2.2 私人医生定制服务的会员体系设计
私人医生服务本质上是一个会员订阅产品。用户购买的不是一次问诊,而是一段时间内的专属服务。这块设计和在线问诊完全不同,我拆成了几个层次:
- 服务套餐表:定义套餐名称、时长(月/季/年)、价格、包含次数(每月几次图文问诊、几次电话问诊)
- 订单表:记录用户购买记录,包含订单号、套餐ID、服务生效时间、到期时间
- 服务关系表:用户购买后,为用户分配一个私人医生团队,形成用户和医生的绑定关系
- 随访任务表:根据套餐规则自动生成随访任务,比如购买后第3天做首次回访、之后每2周回访一次
这块最容易踩的坑是"服务次数"的核销逻辑。比如用户买了月度套餐包含10次图文问诊,每次用完要不要扣次数?如果用户在下单当天就把10次用完了,后面20多天怎么办?我最后设计的方案是:优先扣套餐次数,套餐次数用完后自动降级为普通问诊按次付费,但享受专属医生的优先接诊通道。这样既保证了用户的体验,又不会让平台亏本。
2.3 排班系统与医生资源管理
排班是整个平台能否运转顺畅的隐形瓶颈。没有排班系统,用户选了医生却没人接诊,体验极其糟糕。我设计了在线排班模式:
医生在APP端(或PC工作台)设置自己未来1-2周的可接诊时间段。管理员在后台可以统一排班,也可以让医生自助排班。核心表设计:
CREATE TABLE `doctor_schedule` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `doctor_id` bigint(20) NOT NULL COMMENT '医生ID', `work_date` date NOT NULL COMMENT '出诊日期', `period_type` tinyint(4) NOT NULL COMMENT '时段类型:1上午 2下午 3晚上', `start_time` varchar(10) NOT NULL COMMENT '开始时间', `end_time` varchar(10) NOT NULL COMMENT '结束时间', `max_count` int(11) NOT NULL DEFAULT '10' COMMENT '最大接诊人数', `current_count` int(11) NOT NULL DEFAULT '0' COMMENT '当前已约人数', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0停用 1启用', PRIMARY KEY (`id`), KEY `idx_doctor_date` (`doctor_id`,`work_date`) ) ENGINE=InnoDB;用户端展示医生排班时,直接查这张表,把current_count >= max_count的时段置为灰色不可选,并显示"已约满"。用户选定时段后创建问诊单,联动事务锁表更新current_count,用乐观锁版本号避免并发超约。
3. 数据库设计的核心表结构拆解
3.1 用户体系与医生体系的建模
用户和医生虽然角色不同,但有大量公共字段(手机号、头像、昵称)。我没有把两个角色拆成完全独立的两张表,而是用统一用户表加扩展表的方式:
- t_user:核心用户表,存手机号、密码、昵称、头像、状态、注册时间。
- t_user_profile:用户扩展信息,存真实姓名、身份证号、病史简介、过敏史等。
- t_doctor:医生表,认证手机号、执业证书编号、所属科室ID、职称、擅长领域、个人简介。
- t_doctor_audit:医生入驻审核记录表,记录提交资料、审核状态、审核意见。
用户端注册时只会往t_user写一条数据,医生入驻时走t_doctor表,后台审核通过后会把doctor_id和user_id关联起来。这种设计的好处是,未来如果要做药师角色、运营角色,可以复用同一套用户体系,只需要新增扩展表就好。
3.2 问诊与处方核心表
问诊相关的表是整个系统的核心中的核心,我列一下最终沉淀下来的核心表:
- t_consult_order:问诊订单主表,关联用户、医生、科室、排班时段、问诊类型(图文/电话/视频)、费用、状态、创建时间。
- t_consult_message:问诊沟通消息记录,存消息内容、类型(文本/图片)、发送方角色、发送时间。
- t_prescription:处方表,关联问诊订单,记录诊断结果、处方备注。
- t_prescription_item:处方明细表,一条处方对应多种药品,记录药品名称、规格、用量用法、天数、数量。
- t_drug:药品目录表,药品名、通用名、生产厂家、规格、价格、库存。
处方状态和问诊状态是联动的。问诊单进入"待开方"状态后,医生可以创建处方,开方后进入"待审方",药师审核通过后处方变为有效状态,同时问诊单自动流转为"已完成"。这些状态流转我写了统一的状态机服务类,避免在业务代码里到处散落状态判断。
3.3 订单支付与健康档案的存储策略
支付订单我单独建了t_payment_order表,不跟业务订单表混在一块。原因是支付回调是异步的,一个业务订单可能会对应多次支付尝试(比如第一次支付超时,用户重新发起支付),把支付记录单独存更清晰。支付表核心字段:支付单号、业务订单号、订单类型(问诊/药品/私人医生服务)、支付渠道(微信/支付宝)、支付金额、支付状态、回调时间。
健康档案这块,我的建议是用JSON字段存非结构化数据。用户的血压、血糖、过敏史这些信息字段不固定,每次问诊医生还可能更新,建一堆字段列反而难维护。我在t_health_profile表里设计了profile_data JSON字段,把不同维度的健康数据分类存,查询时按需解析。MySQL 5.7以上对JSON字段支持已经足够好,这种场景用JSON比硬拆列要灵活得多。
4. 后端SpringBoot核心实现细节
4.1 工程结构与关键依赖
后端工程按业务模块分包,结构清晰。标准的maven工程长这样:
medical-cloud ├── medical-common // 公共模块:统一返回、异常处理、常量 ├── medical-framework // 框架模块:安全认证、Redis、配置 ├── medical-system // 系统模块:用户、角色、菜单、字典 ├── medical-doctor // 医生模块:医生、排班、科室 ├── medical-consult // 问诊模块:问诊订单、消息、评估 ├── medical-prescription // 处方模块:处方、药品 ├── medical-order // 订单模块:业务订单、支付订单 ├── medical-admin // 管理端接口 └── medical-api // 对外接口聚合模块关键依赖就几个,不追求多但一定要稳定:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency>4.2 基于JWT的用户鉴权与权限控制
医疗平台涉及大量用户隐私,鉴权设计不能马虎。我用的方案是JWT + Redis黑名单机制:
- 用户登录成功后,后端签发JWT,有效期设置为7天(APP场景低频操作,可以适当延长),同时把token的jti存在Redis里,过期时间与JWT一致
- 用户每次请求带上Authorization头,拦截器解析JWT并校验签名、有效期、角色
- 用户退出登录时,把jti加入Redis黑名单,实现服务端主动失效token
- 修改密码、账号异常时,直接清掉Redis中该用户的所有token
@Component public class TokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String token = request.getHeader("Authorization"); // 解析JWT,获取userId和roles // 校验Redis黑名单 // 放入SecurityContext chain.doFilter(request, response); } }这里有个细节:用户端和后台管理员走两套登录接口,但可以用同一个JWT工具类。JWT里的claims用role字段区分角色,接口授权用@PreAuthorize("hasRole('DOCTOR')")注解控制,不用专门写一堆if判断,代码干净不少。
4.3 在线问诊接口与消息推送实现
在线问诊的核心接口包括:创建问诊单、支付回调、取消订单、分配医生、医生接诊、发送消息、查询历史消息、创建处方、审核处方。这里拿创建问诊单举例,可以看到事务和锁怎么配合:
@Transactional(rollbackFor = Exception.class) public ConsultOrder createConsultOrder(ConsultOrderDTO dto) { // 1. 校验用户状态 User user = userMapper.selectById(dto.getUserId()); // 2. 校验医生排班时段是否可约(乐观锁版本号) DoctorSchedule schedule = scheduleMapper.selectById(dto.getScheduleId()); int rows = scheduleMapper.decreaseAvailableCount(schedule.getId(), schedule.getVersion()); if (rows == 0) throw new BusinessException("该时段已被约满"); // 3. 创建问诊单 ConsultOrder order = new ConsultOrder(); order.setOrderNo(generateOrderNo()); order.setStatus(ConsultStatus.PENDING_PAY.getCode()); // 4. 锁定医生 // 5. 返回订单号,等待用户支付 return order; }消息推送用的是Spring WebSocket。用户和医生建立WebSocket连接后,后端把连接会话和userId绑定,发送消息时根据目标角色和userId定位到具体会话。为了兼容负载均衡场景,我用Redis的发布订阅模式做了消息中转:WebSocket服务收到消息后先发到Redis频道,所有WebSocket节点订阅频道后,再推送给目标会话。这样即使以后扩展多节点部署,消息也不会丢。
4.4 支付回调和退款流程的坑
支付这块我踩过最多的坑。微信和支付宝支付回调是异步的,回调可能延迟、可能重复、可能顺序颠倒,所以回调处理一定要做幂等。
我的处理方式是:回调接口先根据支付单号查询本地支付状态,如果已经是"已支付"就立即返回成功,不再重复处理业务逻辑。同时开启事务把"更新支付状态"和"更新业务订单状态"放在同一个事务里,任何一个失败都回滚,避免出现支付单显示已支付但业务订单还是待支付的状态不一致。
退款流程相对简单一些,但也需要异步化处理。用户提交退款申请 -> 后台审核 -> 调用支付渠道退款接口 -> 回调更新状态。需要注意的是,微信退款和支付宝退款都要传原支付金额和退款金额,如果金额不对会直接拒绝,所以退款前最好做一次金额校验。
5. Uniapp前端实现与跨端适配
5.1 项目结构与页面规划
Uniapp前端工程按功能模块和角色拆分布局,用户端和医生端在同一个工程里,通过tabBar和页面路由区分。
用户端核心页面:首页(医生推荐、快捷问诊入口)、科室列表、医生列表、医生详情、问诊详情(聊天界面)、订单列表、我的档案、私人医生购买页。
医生端页面:工作台(待接诊/问诊中订单数)、接单大厅、问诊聊天、开方页面、排班设置、收益统计。
pages/ ├── user/ │ ├── index.vue // 患者首页 │ ├── doctor-list.vue // 医生列表 │ ├── doctor-detail.vue // 医生详情 │ ├── consult-chat.vue // 问诊聊天 │ ├── order-list.vue // 订单列表 │ └── private-doctor.vue // 私人医生 ├── doctor/ │ ├── workbench.vue // 医生工作台 │ ├── consult-chat.vue // 接诊聊天 │ ├── prescription.vue // 开方 │ └── schedule.vue // 排班 └── common/ ├── login.vue // 登录 ├── register.vue // 注册 └── webview.vue // 通用WebView5.2 跨端适配的四个关键细节
Uniapp写起来爽,适配起来还是有不少细节的。我总结了几条血泪经验:
- rpx单位适配:Uniapp用的是rpx,750rpx等于屏幕宽度,大部分场景直接用rpx做布局没问题。但是涉及到canvas绘制、地图标注点、富文本编辑器内部样式时还是要用px,我踩过图表在部分安卓机上显示模糊的坑,后来统一封装了px和rpx转换工具函数
- 底部安全区适配:苹果全面屏手机底部有home indicator,页面底部按钮如果不做安全区适配会被挡住。用环境变量判断机型,给底部按钮加上safe-area-inset-bottom的padding
- 图片上传格式统一:不同手机拍照传到后端的图片,有的带exif信息导致方向不对,有的二进制格式不标准。我在uni.uploadFile封装层做了统一处理,前端压缩后转base64,后端再做一次格式校验
- 安卓返回键和物理返回:APP端安卓用户可以按物理返回键,但是页面栈和微信小程序不一样,直接back会退出整个应用。我在onBackPress生命周期里做了统一路由控制,确认是否是首页再允许退出
5.3 聊天界面的实践方案
问诊聊天是这个项目前端的核心,交互上其实类似微信:左侧是对方的消息气泡,右侧是自己的消息气泡,底部是输入框和表情按钮。用Uniapp实现聊天界面有几个注意点:
- 消息列表用scroll-view,高度要撑满剩余空间,使用flex: 1布局,避免出现滚动条计算出错
- 进入聊天页面时,先调用后端接口拉取最近的历史消息,再通过WebSocket长连接接收新消息,消息渲染用v-for加key,保证消息顺序稳定
- 图片消息用uni.previewImage做预览,大图要用云存储的缩略图参数,直接加载原图在弱网环境下会卡
开处方页面比较特殊,我专门设计了一个表单:选择诊断结果(支持搜索常见病名称)、添加药品明细(从药品目录选择,自动带出默认用量)、填写医嘱说明。开完方后提交到后端,进入药师审方流程。这个页面数据层级比较深,用Vuex管理处方数据状态,以免用户在多个步骤之间跳转时数据丢失。
6. 系统部署与运维实录
6.1 从零到能跑的部署流程
整套系统后端是标准的jar包部署,前端是编译后的静态资源加反向代理。部署流程如下:
- 准备一台Linux服务器(2核4G以上够用),安装JDK 1.8+、MySQL 5.7+、Redis 5+、Nginx
- 导入数据库脚本(项目sql目录下),修改application.yml中的数据库配置和Redis配置
- mvn clean package -Dmaven.test.skip=true打包
- 把jar包上传到服务器,用systemd或nohup后台启动
- 前端admin项目npm run build:prod编译,dist目录丢到Nginx的html目录
- Uniapp项目用HBuilderX云打包,生成安卓/ iOS安装包
Nginx关键配置:
server { listen 80; server_name your-domain.com; # 前端页面 location / { root /usr/share/nginx/html/admin; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # WebSocket代理 location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } }6.2 常见问题与排查技巧
把我在集成测试和现场部署阶段遇到的高频问题整理成了一张速查表,基本覆盖了这套系统从开发到上线的常见坑:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| APP请求接口报跨域 | 后端未配置CORS | SpringBoot加CorsFilter,或Nginx统一加Access-Control-Allow-Origin header |
| 问诊单创建后医生看不到 | 消息推送WebSocket断了 | 检查Nginx的ws代理配置,浏览器F12看WebSocket连接状态 |
| 支付成功但订单状态没变 | 支付回调被服务器防火墙拦截 | 检查回调地址是否公网可访问,关闭防火墙对8和443之外端口的限制 |
| 图片上传失败 | 阿里云OSS的bucket权限不对 | 检查Bucket权限为公共读或私有读写+签名URL |
| 数据库连接超时 | 连接池配置太小 | 调整HikariCP的maximum-pool-size,推荐10~20 |
| 安卓手机无法安装APP | 没有签名或目标SDK版本太高 | 用正式证书签名打包,targetSdkVersion保持在33以下 |
| 聊天消息重复发送 | WebSocket重连机制导致消息重发 | 前端保存消息发送状态,重连后根据消息ID做去重 |
6.3 上线后我建议优先优化的几个点
系统刚跑通时只满足基本功能,距离真正生产环境还有距离。我落地之后又做了几轮优化,你可以按优先级参考:
- 问诊高峰期接口性能:热门科室的医生列表和排班接口加Redis缓存,缓存时间为30秒,降低数据库压力
- 敏感信息加密:用户手机号、身份证号、健康档案数据入库前做AES加密,查询时解密。虽然有性能损耗,但医疗场景值得
- 操作日志与审计:后台管理端和医生端的关键操作(处方审核、订单退款、用户信息修改)都要记录操作日志,出问题时才能追溯
- 消息通知渠道扩展:除了站内WebSocket推送,重要的订单状态变化(如医生接诊提醒)接入短信或微信模板消息
我个人在实际项目里体会最深的一点是,医疗平台的开发难点不在技术本身,而在业务流程的完整性和角色权限的边界控制。SpringBoot和Uniapp这套技术组合开发效率很高,但一定要把状态机、角色权限、支付幂等这些基础设计做扎实,否则后面每加一个功能,前面欠的债都会加倍还回来。
最后再分享一个小技巧:如果你需要演示或验收,建议在数据库中预置一批演示数据。我预置了10个科室、30个医生(含排班)、5个私人医生套餐、20条处方模板,演示在线问诊时可以先创建一条问诊单,再把问诊状态直接改到"待接诊",方便现场展示医生接诊和开方的完整流程。这套系统后续还可以扩展的方向不少,比如接入视频问诊(腾讯云TRTC)、电子签名、药品库存管理等,核心业务表结构都保留了扩展余地,往这些方向加功能时不用推倒重来。
本文还有配套的精品资源,点击获取