☰
SpringBoot+Uniapp在线问诊平台搭建:仿京东健康系统实战
2026/10/7 14:39:50 网站建设 项目流程

简介:这是一套基于Spring Boot与Uniapp的综合医疗云平台完整工程,定位为在线问诊与私人医生定制服务的一体化解决方案,适合有Java基础、希望掌握前后端分离医疗项目开发的中高级学习者。项目借鉴京东健康与好医生模式,实现从用户指定名医在线咨询、电子处方提交与药剂师审核,到模拟医保购药、病患跟踪和智能用药提醒的完整就医闭环,覆盖后台管理、PC端与手机端三类入口。压缩包为zip格式,共2000个文件、约79.73MB,其中java文件多达1018个,vue文件500个,js文件297个,配合sql、xml、properties等配置,可对照学习后端服务、页面组件、数据库初始化与项目部署。工程按admin-ui、app、elt-server三大模块划分,分别对应Vue+ElementUI后台、Uniapp移动端和SpringBoot API服务,并集成SpringSecurity、Mybatis-Plus、MySQL与Redis。目前已有166人学习,既可用于毕业设计的功能扩展,也可作为商业医疗平台二次开发的原型参考。

1. 这个标题到底在说什么:一套能跑的在线问诊平台,不是花架子演示

第一次看到这个标题,你八成会把它归类成“又一个管理系统源码”。但实际把它拆开看,这里面的信息量比想象中大得多。它讲的是用 SpringBoot 做后端、Uniapp 做前端,复刻京东健康、好医生这类产品的核心业务闭环:用户在线提问、医生接单回复、平台做订单与内容管理,同时把私人医生这种包月/包年式的定制服务也放进来。换句话说,这套系统不是只做几个静态页面,而是把问诊流程、支付订单、医生排期、消息推送全部串起来的综合医疗云平台。适合谁?适合想快速搭建医疗咨询类 SaaS 产品原型的技术团队,也适合拿来做毕业设计或公司内部研发资源不足时的起步骨架。但我要先泼一盆冷水:这类系统工程量和坑都在“业务规则”上,不在“页面数量”上。问诊单状态怎么流转、医生和患者的对话记录如何保证不丢、私人医生服务到期怎么自动提醒,这些才是真正耗时间的点。

2. 整体架构与模块拆解:为什么选 SpringBoot + Uniapp 而不是两套前端

2.1 功能清单与角色划分:患者端、医生端、管理员端

先对着标题里的“后台管理、PC端及手机端”把功能盘点一遍。三类角色对应三套界面逻辑,但后端接口是共用的。患者端主要做的事:注册登录、浏览医生列表、发起点对点问诊、查看问诊回复、购买私人医生套餐、发起图文或文字咨询。医生端核心操作:接单、回复患者、写处方建议、设置可接诊时间、查看私人医生服务中的固定患者列表。后台管理端则更偏向运营:医生资质审核、科室管理、问诊订单流览、退款处理、整站数据统计。这三块业务放在一个 SpringBoot 单体应用里就能跑,没必要一上来就拆微服务。Uniapp 则负责把患者端和医生端打包成 APP,同时还能编译成 H5 嵌套在 PC 后台里用,一套代码三端复用。我一般会把 Admin 后台单独做成 Vue 项目塞进 SpringBoot 的静态资源目录,避免另起一个服务。

2.2 技术选型理由:SpringBoot 生态和 Uniapp 跨端能力的边界

选择 SpringBoot 的理由很直白:JPA 或者 MyBatis 上手快,Spring Security 或者 Sa-Token 做登录鉴权的资料多,部署只需要一个 Jar 包。对于医疗类项目,最容易被挑战的数据库事务和消息队列,Spring 全家桶都给了现成方案。Uniapp 则解决了多端成本的问题——一套 Vue 语法可以编译到 iOS、Android、微信小程序和 H5。但要注意,Uniapp 不是万能的,如果你需要后台管理那种重表格、复杂表单交互,用 Uniapp 做 PC 端体验会很别扭。我个人的拆解是:患者/医生端用 Uniapp 跑 APP 和 H5,后台管理单独用 Vue + Element,SpringBoot 里同时挂两个前端构建产物。这样既守住了“手机端”的关键场景,也保住了 PC 端后台的操作效率。

2.3 项目结构组织:后端工程与前端工程的目录约定

实际落地时,我建议用 Maven 多模块或包分层都行,但目录职责必须清晰。后端按业务域分包,而不是按技术层分包。我习惯这样组织:

medical-cloud ├── medical-admin 前端后台管理(Vue项目) ├── medical-app 前端Uniapp项目(患者端+医生端) ├── medical-server SpringBoot后端工程 │ ├── src/main/java │ │ ├── controller 接口层 │ │ ├── service 业务层 │ │ ├── mapper MyBatis数据访问层 │ │ ├── entity 数据库对应实体 │ │ ├── dto 入参出参对象 │ │ ├── config 安全、MyBatis、WebMvc配置 │ │ └── component 定时任务、消息推送等组件 │ └── src/main/resources │ ├── mapper/xml SQL映射 │ ├── static 打包时复制后台前端产物 │ └── application.yml

这样的结构避免出现 controller 里写业务 SQL、service 里堆了 2000 行代码的老大难问题。另外,把 Uniapp 项目放在 medical-app 目录,用 HBuilderX 打开后,请求后端接口时注意环境变量区分。开发环境请求局域网 IP,生产环境请求已备案的 HTTPS 域名,这个配置一定要做成可切换的,否则后面打包上线时会因为地址写死而翻车。

3. 数据库设计与核心表结构:把问诊和私人医生服务落到表里

3.1 用户与医生信息表设计要点

这类平台的第一张表是用户表,但不要把所有字段都塞进去。我会拆成user_account和user_profile。user_account只存登录凭证:手机号、密码的 BCrypt 哈希、状态。user_profile存姓名、头像、性别、年龄、既往病史。医生还要有专门的doctor_info表和doctor_verify表,因为医生需要科室、职称、执业证书编号、审核状态。这里有一个容易忽略的细节:user_account里的role字段不要只写 1、2、3 这种魔法数字,要用枚举映射到常量。比如 1 是患者 2 是医生 3 管理员。但更好的做法是单独建user_role关联表,因为一个用户可能既是医生又是患者,这在复刻京东健康时很常见——医生自己生病了也要看病。

3.2 在线问诊流程的表关联:问诊单、消息、处方

在线问诊不是简单地加好友聊天,它必须有一个“问诊单”作为业务主键。我设计了四张核心表:

  • consultation问诊单:包含患者 id、医生 id、状态(待接诊/咨询中/已结束)、类型(图文/电话)、创建时间、结束时间。
  • consultation_message消息表:每一条聊天内容都要落库,字段包括问诊单 id、发送者 id、消息类型(文本/图片/语音)、内容或文件 URL、发送时间、是否已读。
  • consultation_prescription处方建议表:医生结束问诊时可以填写,包含药品名、用法、用量、注意事项。

为什么消息要落库?因为医疗咨询有合规要求,聊天记录必须可追溯。用 MongoDB 或 Redis 存聊天流是常见做法,但如果你只有 MySQL,也可以一把梭。我建议用 MySQL 存全部文本消息,图片走对象存储只存 URL。对于问诊单状态流转,不要用一张状态字段到处 update,而是建一张consultation_status_log状态变更日志表,记录从“待接诊”到“已结束”的每一步操作人和时间,出了问题能复盘。

3.3 私人医生定制服务的续费与排期表

私人医生服务和普通问诊最大的区别是:它卖的是“一段时间内的固定服务权限”。用户买了一个月私人医生套餐,这期间可以和指定的医生不限次数沟通,并且享有优先响应。对应的表结构要有套餐表和服务订阅表:

  • private_doctor_package套餐表:套餐名、时长(天)、价格、包含权益(比如每月可预约几次线下/线上复诊)。
  • private_doctor_subscription订阅表:用户 id、医生 id、套餐 id、开始时间、结束时间、状态(有效/已过期/退款)。

这里有一个必踩的坑:计算到期时间不要用 Java 里的new Date()加减,直接使用数据库时间。不然 app 服务器时钟不同步会导致用户提前或延迟到期。另外要建定期任务,每天凌晨扫描即将到期和已过期的订阅,给用户和医生发提醒。这个任务用 SpringBoot 的@Scheduled注解就能做,但要注意多实例部署时加@ShedLock防止重复执行。

3.4 初始化数据脚本的坑:字符集、时区、演示账号

不要小看init.sql,大部分开发环境翻车都发生在初始化脚本上。第一,字符集必须统一用utf8mb4,才能存 emoji 表情,现在患者问诊动不动发个😷,utf8 会报错。第二,表字段的created_at和updated_at要用datetime,连接字符串里加serverTimezone=Asia/Shanghai。第三,演示账号要准备齐全:管理员账号、医生账号、患者账号各一个,密码统一用 BCrypt 加密的“123456”。这样拿到源码的人不需要手动去数据库改密码。初始化脚本里还要插入默认科室、常见病模板,不然前端科室下拉列表是空的。

4. 后端 SpringBoot 实现:从登录鉴权到问诊接口的完整链路

4.1 基于 JWT 的登录与角色鉴权

我用 Spring Security + JWT 做无状态登录,但拦截规则必须精确。常见做法是把登录接口、Swagger 文档、静态资源设为白名单。其他请求都要解析 token。注意一个关键点:不同角色的权限控制不能只依赖前端菜单隐藏,后端接口要校验角色。比如患者调“创建问诊单”接口,医生调“结束问诊”接口,必须校验当前 token 对应的用户是否具有该角色。我写一个极简的 JWT 工具类:

public String generateToken(Integer userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }

逻辑说明:生成 token 时把 userId 和 role 放到 claim 里,过期时间设为 7 天。医疗平台不建议把 token 有效期拉太长,因为涉及隐私,用户长时间不操作就该重新登录。解析时用同一个secretKey验签,然后把解析出的 claim 放进ThreadLocal,方便后续 service 层通过UserContext.getUserId()拿到当前操作者。

参数说明:secretKey要存在配置文件里,长度至少 32 字节;HS256 算法对称加密,服务端重启不影响已发 token 验证,但如果你改了密钥,所有用户都要重新登录。

4.2 在线问诊核心接口:提交问诊、医生接单、消息推送

在线问诊的第一步是患者提交问诊单。接口设计为POST /api/consultation/create,入参包含doctorId和description。患者提交后,问诊单状态为“待接诊”。这里的业务规则是:医生端收到新单提醒,医生接单后状态变成“咨询中”。接单接口POST /api/consultation/accept要防止两个医生同抢一单,用乐观锁或数据库条件更新解决:

UPDATE consultation SET status = 'IN_PROGRESS', doctor_id = #{doctorId}, accept_time = NOW() WHERE id = #{consultationId} AND status = 'PENDING'

逻辑说明:这个 update 只有成功影响一行记录才算抢单成功,因为status = 'PENDING'是条件,一旦被另一个医生更新,当前更新的行数就是 0,业务上就能判断“已经被抢”。这比先 select 再 update 安全得多。参数说明:consultationId就是患者创建后返回的 id;doctorId从当前登录医生上下文取,而不是前端传,避免伪造。

消息推送部分,如果只是做演示,用轮询也能跑通。但要让体验像样,我建议集成 WebSocket。患者和医生各建一个连接。服务端在收到消息后保存数据库,再通过 session 推给目标用户。SpringBoot 里用SimpMessagingTemplate最省事:

@MessageMapping("/chat.sendMessage") @SendToUser("/queue/reply") public ConsultationMessage sendMessage(@Payload ChatRequest req) { consultationMessageService.save(req); return new ConsultationMessage(req); }

逻辑说明:@MessageMapping接收前端发来的消息,保存落库之后,通过@SendToUser只推送给当前登录用户对应的客户端。这里要注意:前端订阅的地址必须是/user/queue/reply,否则收不到。医疗场景的敏感字段多,不要直接把整个实体类返回,而是用单独的 DTO 做响应。

参数说明:WebSocket 端点路径需要在前端和WebSocketConfig中保持一致,建议用/ws-consultation。心跳检测用 Spring 自带setHeartbeatTime,否则连接闲置几分钟会被网关断掉。

4.3 私人医生定制服务的订单与排期逻辑

私人医生服务本质上是一个商品订单。先创建订单POST /api/private/order/create,入参是套餐 id。这时要校验用户是否已经拥有未过期的同科室订阅,通常不允许重复购买同一科室的私人医生服务,但可以买不同科室。然后调用支付模块,支付成功后才真正生成订阅记录。复刻阶段可以做一个假支付:调用支付接口直接返回成功,但要在代码里留出outTradeNo字段,以后接微信支付、支付宝支付时只需要替换支付那一步。

排期逻辑是另一个大头。私人医生承诺用户可以预约每周一次线上复诊,需要一个排期表:

public List<LocalDate> getAvailableDates(Integer doctorId, LocalDate start, LocalDate end) { // 剔除医生休息日和已被预约满的日期 }

这里的坑是时区问题:医生设置可接诊时间是 10:00-12:00,患者和医生在不同时区,或者服务器是 UTC 时区,会显示成 18:00-20:00。解决办法统一用数据库存datetime,前端展示时使用用户浏览器时区转换。

4.4 后台管理接口:数据统计与内容管理

后台管理的接口没什么花哨的,就是标准的增删改查。但有一个容易忽略的点:统计查询不要用 MyBatis 的count(1)和select *混合使用。建议单独写一个StatisticMapper,用 SQL 聚合函数。比如统计每日问诊量:

SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM consultation WHERE create_time >= #{startTime} AND create_time < #{endTime} GROUP BY DATE(create_time) ORDER BY day;

逻辑说明:DATE(create_time)会把日期截断到天,再分组计数。注意endTime要取第二天的 00:00,否则会漏掉当天的数据。参数说明:startTime和endTime用于做时间范围查询,后端 controller 里定义参数时建议用字符串接收,再用LocalDateTime.parse转换,避免前端传的格式不一致导致 500。

5. Uniapp 端实现:一套代码覆盖 APP 与 PC 浏览器

5.1 页面路由与状态管理:登录态同步

Uniapp 的页面管理入口在pages.json,配置tabBar和页面路由。登录态我习惯用uni.setStorageSync存 token。但跨页面状态不能只用存储,还要有一个全局实例。Uniapp 没有 Vuex 的强制要求,但涉及用户信息、未读消息数量这种跨页面参数,我建议引入 Pinia 或直接用 Vuex。这里的关键是:每次 APP 启动时,先检查本地存储的 token 是否存在,然后调GET /api/user/info刷新用户信息。如果接口返回 401,就跳转登录页。这个流程在App.vue的onLaunch里做。

5.2 问诊聊天页的实现:长连接与消息队列

聊天页是 Uniapp 端最容易写砸的地方。文本消息、图片消息、消息时间分组、输入框和消息列表的滚动,每一步都有讲究。我先说连接逻辑:在患者进入问诊单详情页时建立 WebSocket 连接,页面卸载时关闭。Uniapp 原生支持uni.connectSocket,但要注意不同平台的差异。写成这样:

connectWebSocket(consultationId) { const token = uni.getStorageSync('token'); this.socketTask = uni.connectSocket({ url: `wss://api.yourdomain.com/ws-consultation?token=${token}&consultationId=${consultationId}`, complete: () => {} }); this.socketTask.onMessage((res) => { const data = JSON.parse(res.data); this.messages.push(data); this.markRead(); }); }

逻辑说明:通过 URL 参数把 token 和问诊单 id 传过去,服务端鉴权后建立专属通道。注意uni.connectSocket是异步的,socketTask需要先赋值,再在onMessage监听。如果直接uni.connectSocket不走回调,消息事件会丢失。参数说明:ws在审核不通过的 iOS 上必须换成wss,也就是加一层 SSL。开发阶段可以用ws://的局域网地址,但打包生产包前一定要换成wss。

5.3 私人医生服务页:套餐展示与下单

套餐展示页直接请求GET /api/private/package/list,渲染卡片列表。下单按钮触发uni.request调创建订单接口,再跳到模拟支付弹窗。这里有一个交互细节:支付成功后不能只把前端页面跳转,必须等后端确认订阅生成后才跳。我见过很多演示项目为了省事,前端弹窗直接显示“支付成功”,然后状态就没同步。正确做法:前端调支付确认接口POST /api/private/order/pay,返回订阅 id 后再跳转到医生列表。不然用户买了服务,医生那边看不到订阅记录。

5.4 打包与调试:HBuilderX 发行、安卓权限配置

Uniapp 的打包流程属于固定操作,但坑全在权限上。安卓打包时需要选择权限,不要无脑全选。医疗类 APP 需要的网络权限、相机权限、通知栏权限是必须的,但短信读取权限千万别勾。勾了隐私合规直接过不去。HBuilderX 云打包的时候,图标和应用名称都要提前准备,不然默认图标太丑,审核也会因为这被驳回。另外,如果你要把 Uniapp 编译成微信小程序,记得在 manifest 里配好小程序 appid。开发时可以关掉域名校验,但发布版必须配置合法的request、socket合法域名。

6. 避坑/常见问题:从开发到上线的 5 条血泪经验

6.1 现象:用户头像上传后 PC 端显示正常,APP 端裂开

原因:上传接口返回的 URL 是后端本地磁盘路径,比如/upload/avatar.jpg,PC 浏览器可以拼上域名访问,但 Uniapp APP 里直接用相对路径解析会失败。解决:上传接口返回完整可访问的 URL,比如https://api.yourdomain.com/upload/avatar.jpg。如果文件存储在对象存储,就返回对象存储的 CDN 地址。同时前端使用图片时不要手动拼接域名。

6.2 现象:问诊消息偶尔丢失,而且只在弱网时出现

原因:前端发送消息后,WebSocket 连接在弱网环境下断线重连,消息没有做本地重发。解决:消息发送时先给一个前端生成的临时 id,存入待发送队列。onSocketOpen时检查队列,把所有未确认的消息重新发送。服务端在处理消息时要根据临时 id 做幂等,避免重复插入。这属于经典的消息可靠性问题,演示系统最容易忽略。

6.3 现象:后台管理页面在手机上排版错乱

原因:后台管理用的 Vue + Element 组件库本身是为 PC 设计,没有做响应式。但标题要求“PC端及手机端”,后台管理也要能看。解决:不要用 Element 的栅格布局硬撑,直接给后台管理单独写一个移动端入口。常见做法是在管理端页面头部判断设备宽度,小于 768px 时跳转到一个精简版数据看板,只展示核心运营数据。或者用@media给表格换成卡片式展示。

6.4 现象:Uniapp 打包后请求地址还是本地 localhost

原因:开发时把baseURL写死在代码里,忘了切换环境变量。解决:在config.js里根据process.env.NODE_ENV区分开发和生产。或者更稳妥的方案:在manifest.json的 H5 节点配置开发代理,APP 端则根据运行平台uni.getSystemInfoSync()判断,如果是安卓真机调试而接口是在本机,需要填局域网 IP。我见过太多人打包后死在 localhost 这个坑上。强烈建议做环境配置时把接口域名抽到uni.request的封装里,不要散落在每个页面。

6.5 现象:定时任务重复推送私人医生到期提醒

原因:项目部署了多个实例,每个实例的@Scheduled都会执行一遍,导致用户被短信轰炸。解决:引入spring-integration-jdbc或ShedLock做分布式锁。ShedLock 的原理是利用数据库表记录任务执行时间,竞争到锁的实例才执行。配置很简单,加一个依赖,加一个@EnableScheduling注解,再在任务方法上标注。另外提醒本身要加去重逻辑,比如在提醒记录表里判断task_type + user_id + date是否已存在,存在就跳过。

7. 上线前的验证和性能优化:把演示项目做成能跑的产品

在正式投入之前,建议花两天时间做三件事。第一,接口层压测。用 JMeter 模拟 100 个并发用户同时访问医生列表接口,数据库加了索引的情况下,响应时间应该控制在 200 毫秒以内。如果碰到慢 SQL,把 MyBatis 的日志级别调成 debug,看打印出来的 SQL 是否走了索引。问诊消息表一定要在consultation_id和create_time上建联合索引,否则随着运营时间拉长,查聊天记录会越来越卡。

第二,数据备份与恢复演练。医疗平台的数据是最容易被当成靶子的,数据库不仅要每天全量备份,还要做增量备份。常见做法是备份主库的 binlog,并用mysqlbinlog工具做时间点恢复。我一般还会在application.yml里配好多数据源,让报表查询走从库,减轻主库压力。如果条件允许,用数据库同步软件把生产库只读同步到测试环境,方便复现线上问题。

第三,APP 的权限合规与热更新。安卓和 iOS 商店对医疗类 APP 审核非常严格,隐私政策里必须说明收集了哪些个人信息、用来干什么、是否共享给第三方。在线问诊涉及的健康数据要单独列明。建议在 APP 启动时弹窗获得用户同意,不同意就直接退出。热更新用 Uniapp 的uni-upgrade-center可以实现整包更新,但注意 iOS 的 AppStore 审核不允许静默更新核心代码,所以热更新只建议用来修复小的不需要改原生层的功能。

最后说一个我自己的教训:项目接近收尾时,不要为了赶进度跳过联调阶段。前端的页面和后端的接口各自看着没问题,但一旦把 Uniapp 打包成 Android 实际访问,就会遇到证书校验失败、WebSocket 被系统切断、网络权限没开等一系列怪问题。把这些时间和环境因素留够,比多写一个接口更重要。搭建这套仿京东健康的平台,最好的方式是按“问诊主线”先跑通,等核心链路稳定后再去兼容那些边角功能。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询