简介这是一套面向Java全栈开发者与医疗信息化方向学习者的综合医疗云平台源码借鉴京东健康与好医生系统模式覆盖在线问诊、私人医生定制、电子处方审核、模拟医保购药、病患跟踪与智能用药提醒等业务场景适合作为毕业设计、课程实训或企业级项目二次开发的参考。资源包共约2000个文件以1018个Java后端代码、500个Vue页面组件、297个JavaScript脚本为主另含XML配置、JSON数据、CSS样式与SQL建表脚本压缩包约79.73MB。项目采用SpringBoot 2.x、MyBatis-Plus、SpringSecurity与Swagger构建后端API前端由Uniapp、Vue2与ElementUI实现移动端与Web后台数据层使用MySQL 8.0与Redis划分为admin-ui后台、app移动端与elt-server服务端三大模块结构清晰、分层明确。目前已有166人学习下载可帮助读者快速理解医疗问诊类系统的完整实现路径与工程组织方式。1. 仿京东健康与好医生系统一套综合医疗云平台到底由哪些模块拼起来打开招聘软件搜「医疗云平台」你会发现大量岗位描述里同时出现 SpringBoot、Uniapp、在线问诊、后台管理、APP 这几个词。这不是巧合——仿京东健康与好医生系统这类综合医疗云平台本质上就是一套「多端 多角色 强业务流」的组合工程。它要解决的核心问题很具体患者能在手机端挂号、问诊、买药、定制私人医生服务医生能在医生端接诊、开方、管理随访运营能在后台管理端审核资质、配置服务包、看数据看板。三端数据同源业务状态实时同步。这套系统适合谁一是想拿医疗行业项目练手的中高级开发者二是需要快速搭出 MVP 验证商业模式的小团队三是接私活时遇到「做个类似京东健康」这类需求的独立开发者。它不适合只想写个 CRUD 练手的新手——医疗业务的资质审核、问诊状态机、订单与处方联动复杂度远超普通电商。我见过太多人一上来就闷头写代码写到一半发现医生排班和问诊订单对不上又回头改表结构。所以这篇笔记不按「先讲架构再讲代码」的套路走而是按真实落地顺序先把业务域拆清楚再定技术选型然后一个模块一个模块跑通最后把踩过的坑摊开讲。你照着走至少能少返工两轮。2. 业务域拆分与技术选型为什么是 SpringBoot Uniapp 这套组合2.1 医疗云平台的六个核心业务域在动手建表之前先把业务域拆干净。仿京东健康这类平台表面看功能很多归拢起来就六块业务域核心实体关键状态流转用户与资质患者、医生、药师、运营待审核 → 已认证 → 已冻结在线问诊问诊单、会话、消息待接诊 → 问诊中 → 已结束 → 已评价私人医生定制服务包、订阅、履约记录待支付 → 服务中 → 已到期药品与订单药品、购物车、订单待付款 → 待发货 → 已收货 → 已完成处方与审核电子处方、审核记录待审核 → 通过/驳回后台管理菜单、角色、操作日志无状态权限驱动拆完你会发现真正难的不是单个域而是域之间的联动。比如问诊结束后医生开方处方要流转到药师审核审核通过才能生成药品订单。这条链路跨了三个域状态必须一致。我一般会在设计阶段就画一张状态联动图把每个域的「出口状态」和下一个域的「入口状态」对齐后面写代码时直接照着状态机实现能省掉大量扯皮。2.2 后端选 SpringBoot 的四个现实理由SpringBoot 在这个场景里几乎是默认答案但我想说清楚它到底赢在哪而不是跟风用。第一医疗业务的事务边界很清晰。问诊下单、处方审核、订单创建每个操作都需要强事务保证。SpringBoot 的声明式事务配合 MyBatis写起来直接、可控不像某些响应式框架需要额外处理事务传播。第二生态成熟。资质审核要接对象存储常见做法是 MinIO 或云厂商 OSS消息通知要接短信/推送问诊会话可能要用 WebSocket这些 SpringBoot 都有现成 starter。热词里提到的「minio 加入到 springboot」就是这个场景的典型需求后面第 4 章会给配置。第三分页和复杂查询。后台管理端大量列表查询MyBatis 分页插件PageHelper几乎是标配。热词里「mybatis 的分页插件的用法 springboot」搜的人多说明这是高频刚需。第四团队上手成本低。医疗项目往往多人协作SpringBoot 的分层结构Controller-Service-Mapper大家都熟新人进来能快速定位代码。版本选择上我建议 JDK 17 SpringBoot 3.x。热词里有人搜「springboot 版本太高」多半是踩了 Jakarta 包名从javax换成jakarta的坑。如果你用的第三方库还没适配 3.x退回 2.7.x 也完全够用别为了追新把项目卡死。2.3 前端选 Uniapp 的取舍Uniapp 的价值在于一套代码同时出微信小程序、Android、iOS、H5。医疗平台的患者端天然需要多端覆盖——年轻人用小程序中老年人用 APP运营推广用 H5。用 Uniapp 能省掉至少一套客户端的维护成本。但 Uniapp 有它的脾气。热词里「uniapp 开发微信小程序 vs android/ios/鸿蒙」被反复搜说明多端差异是真实痛点。我的经验是问诊会话、视频通话这类强交互功能各端差异大要做好条件编译商品展示、订单列表这类展示型页面多端复用率高可以放心写一套。manifest 配置是另一个高频坑。热词「uniapp manifest 配置」和「uniapp 怎么打包」经常一起出现因为打包失败十有八九是 manifest 里的 appid、证书、权限没配对。这部分我在第 5 章避坑里细讲。提示技术选型没有银弹。如果你的团队完全没碰过 Uniapp而项目又急着上线用原生 小程序双端开发反而更稳。选型要看团队存量不是看热度。3. 从零跑通在线问诊数据库设计、接口实现与状态机3.1 问诊核心表结构与索引设计在线问诊是整个平台的心脏。表设计不好后面状态流转全是坑。先看核心表-- 问诊单主表 CREATE TABLE consult_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL COMMENT 问诊单号, patient_id BIGINT NOT NULL COMMENT 患者ID, doctor_id BIGINT DEFAULT NULL COMMENT 接诊医生ID未接诊为空, dept_id INT NOT NULL COMMENT 科室ID, type TINYINT NOT NULL COMMENT 问诊类型 1图文 2电话 3视频, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待接诊 1问诊中 2已结束 3已取消 4已评价, fee DECIMAL(10,2) NOT NULL COMMENT 问诊费用, symptom_desc VARCHAR(1000) DEFAULT NULL COMMENT 症状描述, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, accepted_at DATETIME DEFAULT NULL COMMENT 接诊时间, finished_at DATETIME DEFAULT NULL COMMENT 结束时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_patient_status (patient_id, status), KEY idx_doctor_status (doctor_id, status), KEY idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT问诊单;几个设计要点。order_no用业务单号而不是自增 ID 对外暴露避免被遍历。idx_patient_status和idx_doctor_status两个联合索引分别服务患者端「我的问诊」和医生端「待接诊列表」这两个查询是最高频的。status用 TINYINT 而不是字符串省空间且比较快但要在代码里用枚举映射别裸写数字。会话消息表单独拆CREATE TABLE consult_message ( id BIGINT NOT NULL AUTO_INCREMENT, consult_id BIGINT NOT NULL COMMENT 问诊单ID, sender_type TINYINT NOT NULL COMMENT 1患者 2医生 3系统, sender_id BIGINT NOT NULL, content_type TINYINT NOT NULL DEFAULT 1 COMMENT 1文本 2图片 3语音 4视频邀请, content TEXT COMMENT 消息内容, is_read TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_consult_created (consult_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT问诊消息;消息表只增不改idx_consult_created支撑按会话拉历史消息。注意content用 TEXT因为图文问诊可能传长文本和图片 URL。3.2 问诊状态机的代码实现状态流转是问诊模块最容易翻车的地方。我一般用枚举 状态转移表来管而不是散落在各个 Service 里的 if-else。public enum ConsultStatus { WAITING(0, 待接诊), IN_PROGRESS(1, 问诊中), FINISHED(2, 已结束), CANCELED(3, 已取消), EVALUATED(4, 已评价); private final int code; private final String desc; // 构造、getter 省略 // 允许的状态转移key 当前状态value 可流转到的状态集合 private static final MapConsultStatus, SetConsultStatus TRANSFER Map.of( WAITING, Set.of(IN_PROGRESS, CANCELED), IN_PROGRESS, Set.of(FINISHED), FINISHED, Set.of(EVALUATED), CANCELED, Set.of(), EVALUATED, Set.of() ); public static boolean canTransfer(ConsultStatus from, ConsultStatus to) { return TRANSFER.getOrDefault(from, Set.of()).contains(to); } }Service 层接诊逻辑Transactional(rollbackFor Exception.class) public void acceptConsult(Long consultId, Long doctorId) { ConsultOrder order consultOrderMapper.selectById(consultId); if (order null) { throw new BizException(问诊单不存在); } // 乐观锁只有待接诊状态才能被接诊防止两个医生同时抢单 int updated consultOrderMapper.acceptWithStatusCheck( consultId, doctorId, ConsultStatus.WAITING.getCode(), ConsultStatus.IN_PROGRESS.getCode()); if (updated 0) { throw new BizException(该问诊单已被接诊或状态已变更); } // 发送系统消息通知患者 messageService.sendSystemMsg(consultId, 医生已接诊请开始描述病情); }关键在acceptWithStatusCheck这条 SQL 用了WHERE status 0做条件更新返回影响行数为 0 就说明被别人抢先了。这是防并发抢单最省事的做法比先查后改加锁轻量得多。update idacceptWithStatusCheck UPDATE consult_order SET doctor_id #{doctorId}, status #{toStatus}, accepted_at NOW() WHERE id #{consultId} AND status #{fromStatus} /update参数说明fromStatus传当前期望状态0toStatus传目标状态1。这条 SQL 的原子性由数据库保证不需要额外分布式锁。3.3 医生排班与接诊的联动医生不是随时都能接诊的。真实业务里医生有排班表只有排班时段内的医生才能被分配问诊单。这块如果一开始不做后期加会牵动整个分配逻辑。CREATE TABLE doctor_schedule ( id BIGINT NOT NULL AUTO_INCREMENT, doctor_id BIGINT NOT NULL, work_date DATE NOT NULL COMMENT 排班日期, time_slot TINYINT NOT NULL COMMENT 时段 1上午 2下午 3晚间, max_consult INT NOT NULL DEFAULT 20 COMMENT 该时段最大接诊量, accepted_count INT NOT NULL DEFAULT 0 COMMENT 已接诊量, PRIMARY KEY (id), UNIQUE KEY uk_doctor_date_slot (doctor_id, work_date, time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班;分配问诊单时先查当前时段有排班且accepted_count max_consult的医生按接诊量升序分配。接诊成功后accepted_count加一同样用条件更新防超卖UPDATE doctor_schedule SET accepted_count accepted_count 1 WHERE doctor_id #{doctorId} AND work_date CURDATE() AND time_slot #{slot} AND accepted_count max_consult这条更新返回 0 就说明该医生这个时段已满换下一个医生。血泪经验别在应用层先查再判断并发下必超卖。4. 私人医生定制服务与后台管理订阅、履约与权限落地4.1 私人医生服务包的数据模型私人医生定制和普通问诊的区别在于「订阅制」。用户买的不是一个单次服务而是一个周期内的一揽子权益比如一个月内不限次图文问诊、两次视频问诊、专属健康档案。数据模型要能表达「服务包定义」和「用户订阅实例」两层。-- 服务包定义运营在后台配置 CREATE TABLE service_package ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 服务包名称, doctor_id BIGINT NOT NULL COMMENT 绑定医生, price DECIMAL(10,2) NOT NULL, duration_days INT NOT NULL COMMENT 有效天数, benefits JSON NOT NULL COMMENT 权益配置如{text_consult:-1,video_consult:2}, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT私人医生服务包; -- 用户订阅实例 CREATE TABLE user_subscription ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, package_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL, start_at DATETIME NOT NULL, end_at DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1服务中 2已到期 3已退款, benefit_used JSON NOT NULL COMMENT 已使用权益如{video_consult:1}, PRIMARY KEY (id), KEY idx_user_status (user_id, status), KEY idx_end_at (end_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户订阅;benefits用 JSON 存权益配置-1表示不限次。benefit_used记录已消耗量。每次用户发起视频问诊前先校验订阅是否有效、对应权益是否还有余量扣减时用 JSON 字段更新。注意MySQL 的 JSON 字段更新要用JSON_SET且并发扣减权益同样需要条件更新或行锁别直接读出来改再写回去。4.2 后台管理端的权限模型后台管理是运营的入口权限必须做细。我用的是经典的 RBAC用户 → 角色 → 菜单/按钮权限。CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password VARCHAR(128) NOT NULL COMMENT BCrypt 加密, real_name VARCHAR(32), status TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ); CREATE TABLE sys_role ( id BIGINT NOT NULL AUTO_INCREMENT, role_code VARCHAR(32) NOT NULL COMMENT 如 ADMIN, DOCTOR_AUDITOR, role_name VARCHAR(32) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_role_code (role_code) ); CREATE TABLE sys_menu ( id BIGINT NOT NULL AUTO_INCREMENT, parent_id BIGINT NOT NULL DEFAULT 0, name VARCHAR(32) NOT NULL, path VARCHAR(128) COMMENT 前端路由, perm VARCHAR(64) COMMENT 权限标识如 doctor:audit, type TINYINT NOT NULL COMMENT 1目录 2菜单 3按钮, PRIMARY KEY (id) ); CREATE TABLE sys_role_menu ( role_id BIGINT NOT NULL, menu_id BIGINT NOT NULL, PRIMARY KEY (role_id, menu_id) ); CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) );权限校验用 Spring Security 自定义PermissionEvaluator或者简单点用拦截器 注解。我一般用注解方式在 Controller 方法上标RequiresPerm(doctor:audit)拦截器解析当前用户的权限集合做比对。RequiresPerm(doctor:audit) PostMapping(/doctor/audit) public ResultVoid auditDoctor(RequestBody DoctorAuditDTO dto) { doctorService.audit(dto.getDoctorId(), dto.getPass(), dto.getReason()); return Result.ok(); }拦截器里从 Redis 取当前用户的权限集合登录时写入比对注解值。这样每次请求不用查库性能好。权限变更时清掉对应用户的缓存即可。4.3 资质审核的完整流程医生入驻必须审核资质这是医疗平台的合规底线。流程是医生提交资料 → 运营初审 → 复审可选→ 通过/驳回。审核记录要留痕方便追溯。Transactional(rollbackFor Exception.class) public void audit(Long doctorId, boolean pass, String reason) { Doctor doctor doctorMapper.selectById(doctorId); if (doctor.getAuditStatus() ! AuditStatus.PENDING.getCode()) { throw new BizException(该医生不在待审核状态); } AuditStatus target pass ? AuditStatus.APPROVED : AuditStatus.REJECTED; int updated doctorMapper.updateAuditStatus(doctorId, AuditStatus.PENDING.getCode(), target.getCode()); if (updated 0) { throw new BizException(审核状态已变更请刷新重试); } // 写审核日志 auditLogMapper.insert(AuditLog.builder() .bizType(DOCTOR) .bizId(doctorId) .result(pass ? 1 : 0) .reason(reason) .operatorId(SecurityUtils.getCurrentUserId()) .build()); // 通知医生 notifyService.sendAuditResult(doctor.getUserId(), pass, reason); }同样用条件更新防并发审核。审核日志表结构简单但字段要全业务类型、业务 ID、结果、原因、操作人、时间。后期出纠纷时这张表就是证据。4.4 后台管理前端的菜单动态渲染后台管理端用 Uniapp 还是单独用 Vue Element Plus我的建议是后台单独做别和患者端混在一个 Uniapp 工程里。原因很简单后台是 PC 端重交互患者端是多端轻交互强行统一会让条件编译爆炸。后台前端登录后调/sys/menu/user拿到当前用户的菜单树动态生成路由和侧边栏。按钮级权限用自定义指令v-perm控制显隐// 权限指令 export default { mounted(el, binding) { const perms store.getters.perms; // 当前用户权限集合 if (binding.value !perms.includes(binding.value)) { el.parentNode el.parentNode.removeChild(el); } } }用法el-button v-permdoctor:audit审核/el-button。没有权限的按钮直接不渲染比禁用更干净。5. 多端打包与联调避坑那些让我返工三次的问题5.1 Uniapp 打包安卓与小程序的高频翻车点现象Uniapp 打包安卓 APK 时报「manifest.json 配置错误」或小程序上传后白屏。原因manifest 里的 appid 和实际申请的不一致安卓证书别名或密码填错小程序没配置合法域名。解决manifest 的 appid 必须和 DCloud 后台、微信公众平台申请的一致。安卓打包用自有证书时别名、密码、证书文件三者要对上建议用keytool -genkey重新生成一套并备份。小程序端所有请求域名必须在微信后台「开发设置-服务器域名」里配好且必须是 HTTPS。现象iOS Safari 上用 Uniapp canvas 导出图片是白图。原因iOS 对 canvas 跨域图片和绘制时机敏感图片没加载完就绘制或者图片跨域没设置。解决绘制前用uni.getImageInfo确保图片加载完成跨域图片让后端配 CORS 或走同域代理。导出用uni.canvasToTempFilePath加延时兜底。5.2 SpringBoot 全局过滤器处理上传文件的 XSS 风险现象后台富文本编辑器上传的 PDF 或 HTML 文件预览时触发脚本。原因全局 XSS 过滤器对multipart/form-data请求体做了转义或者对上传文件内容没做类型校验。解决XSS 过滤器要排除文件上传接口只对 JSON 请求体做转义。上传文件要校验 MIME 类型和文件头PDF 只允许application/pdf且存储时重命名不保留原始文件名。预览时用Content-Disposition: inline配合X-Content-Type-Options: nosniff。// 过滤器排除上传路径 private static final ListString EXCLUDE_URLS List.of(/file/upload, /file/pdf); Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String uri request.getRequestURI(); if (EXCLUDE_URLS.stream().anyMatch(uri::startsWith)) { chain.doFilter(request, response); return; } // 对 JSON body 做 XSS 转义 chain.doFilter(new XssRequestWrapper(request), response); }5.3 多端登录态同步的坑现象用户在 APP 登录后H5 端打开还是未登录。原因各端存储隔离token 没打通。解决如果各端是独立域名登录态无法直接共享。常见做法是 H5 端通过 URL 携带一次性 ticket后端校验后换取 token。或者统一走 OAuth 授权码模式各端拿 code 换 token。别想着用 cookie 跨端域名不同根本带不过去。5.4 数据库时区与时间字段的玄学现象问诊单创建时间比实际时间差 8 小时。原因MySQL 时区、JDBC 连接时区、JVM 时区三者不一致。解决统一用 UTC 存储展示时转本地时区。JDBC URL 加serverTimezoneAsia/ShanghaiMySQL 配default-time-zone08:00JVM 启动参数加-Duser.timezoneAsia/Shanghai。三处对齐别只改一处。5.5 分页插件与多数据源的冲突现象配置了多数据源后PageHelper 分页失效返回全量数据。原因PageHelper 的拦截器只对主数据源生效或者分页参数在切换数据源后丢失。解决确保PageHelper.startPage紧跟在实际查询方法前一行中间不要插入其他数据库操作。多数据源场景下给每个数据源单独配分页拦截器或者改用 MyBatis-Plus 的分页插件它对多数据源支持更好。6. 让这套平台真正能跑起来从能用到好用的三个进阶技巧第一个技巧是问诊消息的可靠投递。图文问诊的消息不能丢但 WebSocket 在弱网下容易断。我的做法是消息先落库再推送客户端收到推送后回 ACK服务端标记已送达。客户端重连时拉取未读消息用consult_id last_msg_id做增量同步。这样即使推送失败消息也不会丢用户重进会话能看到完整记录。第二个技巧是资质审核的自动化预检。人工审核医生资质效率低可以在提交时先做一轮机器校验身份证号格式、执业证书编号规则、上传图片的清晰度用图片质量检测接口。预检不通过的直接打回通过的再进人工队列。这能把运营的审核工作量砍掉一半以上。第三个技巧是订阅到期的定时任务与补偿。私人医生服务包到期要自动改状态并通知用户。用 Spring 的Scheduled每天凌晨扫一遍end_at NOW() AND status 1的订阅批量更新。但定时任务可能因为服务重启漏跑所以再加一个补偿接口运营可以手动触发某段时间的补偿扫描。别只依赖定时任务线上什么都会发生。进阶点核心手段验证方式消息可靠投递落库 ACK 增量拉取断网重连后消息不丢审核自动化格式校验 图片质量检测预检拦截率、人工审核量下降订阅到期补偿定时任务 手动补偿接口模拟漏跑后补偿能补齐最后说个我自己的习惯每做完一个模块我会写一个「状态流转测试用例」把所有合法的和非法状态转移都跑一遍。医疗业务最怕状态错乱测试覆盖到位上线才睡得着。这套平台不算小但按业务域拆开、按状态机实现、按端分别打包一步步来是能落地的。希望帮到你。本文还有配套的精品资源点击获取