资讯详情

资讯详情

互联网医院系统源码实战:Java+SpringBoot+MySQL+原生APP全栈落地指南

简介这是一套基于Java技术栈的互联网医院系统源码采用Spring Boot框架与MySQL数据库构建面向希望深入在线医疗应用开发的中高级开发者与学习者。项目覆盖在线挂号、电子病历、在线问诊、处方开具、药品配送等核心业务并涉及JPA与Hibernate数据访问、前后端分离架构、Spring Security安全控制、日志记录、单元与集成测试以及Docker容器化部署等关键知识点可作为完整项目实战与技能提升的参考方案。资源以zip压缩包形式提供整体约267.92MB文件类型明细暂未提供但内容围绕后端服务、数据库脚本与原生APP接口展开便于按模块查阅与二次开发。目前已有207人学习关注适合需要研究Spring Boot微服务、RESTful API设计及互联网医疗业务落地的开发者参考借鉴。1. 从一套互联网医院系统源码说起Java 全栈 原生 APP 到底能跑通什么互联网医院系统源码这个方向最近两年问的人明显变多了。原因不复杂线下问诊的线上化需求一直在而一套能自己部署、自己改业务逻辑的源码比 SaaS 订阅更适合有研发团队的机构。标题里这套技术栈——Java SpringBoot MySQL 原生 APP——其实是一套非常典型的「后端稳、端侧轻」组合。后端用 SpringBoot 扛住挂号、问诊、处方、订单这些高频事务MySQL 存结构化医疗数据原生 APP 负责患者端和医生端的交互体验。它解决的核心问题是让你在自有服务器上把一条从「患者注册→选医生→图文/视频问诊→开方→支付→复诊」的链路完整跑起来而不是被第三方平台的接口和抽成卡住。适合谁有 Java 基础、想切入医疗信息化赛道的后端工程师或者手里有诊所资源、想自建平台的技术负责人。下面我按实际落地顺序把选型、建库、接口、联调和踩坑一条条拆开讲。2. 后端骨架怎么搭SpringBoot 分层与医疗业务的映射关系2.1 为什么医疗系统偏爱 SpringBoot 而不是别的框架医疗业务有个绕不开的特点事务边界特别多。一次挂号要同时写挂号记录、扣减号源、生成订单一次开方要校验库存、写处方主表和明细表、触发支付单。这些操作要么全成功要么全回滚SpringBoot 的声明式事务Transactional在这里几乎是刚需。另外医疗系统往往要对接医保、短信、对象存储、视频服务SpringBoot 的 starter 机制让这些第三方依赖的引入成本很低加个依赖、写几行配置就能用。再说版本选择。热搜里常出现「springboot版本太高」这个抱怨这不是空穴来风。SpringBoot 3.x 默认要求 JDK 17而且把javax.*换成了jakarta.*很多老版本的 MyBatis、Swagger、Redis 客户端直接编译不过。如果你拿到的源码是基于 2.7.x 写的硬升到 3.x 会踩一堆包名不兼容的坑。我的建议是新项目直接上 SpringBoot 3.2 JDK 17老项目维护就锁在 2.7.18别折腾。2.2 一套能落地的包结构医疗系统的模块划分要跟着业务走不要按技术分层硬切。下面这个结构是我在几个项目里验证过比较顺手的com.hospital ├── common // 统一返回、异常、常量、工具 ├── config // 拦截器、跨域、线程池、Swagger ├── module │ ├── user // 患者/医生/管理员账号 │ ├── doctor // 医生排班、科室、职称 │ ├── appointment // 挂号、号源 │ ├── consult // 图文/视频问诊会话 │ ├── prescription// 处方、药品 │ ├── order // 订单、支付 │ └── record // 病历、随访 └── HospitalApplication.java每个 module 内部再分controller / service / mapper / entity / dto。这样切的好处是问诊模块要改视频逻辑不会牵动挂号模块的代码。原生 APP 那边调接口时URL 前缀也能按模块区分比如/api/consult/start、/api/order/pay前端路由和权限拦截都好写。2.3 最小可运行配置先看pom.xml里必须锁死的几个依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version /parent properties java.version17/java.version mybatis-plus.version3.5.6/mybatis-plus.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies这里有个细节SpringBoot 3.x 对应的 MyBatis-Plus starter 名字变了是mybatis-plus-spring-boot3-starter用老的mybatis-plus-boot-starter会启动报错。MySQL 驱动也从mysql-connector-java换成了mysql-connector-jgroupId 变成com.mysql。这两个改动是升级时最容易翻车的地方。application.yml里数据库和连接池配置spring: datasource: url: jdbc:mysql://127.0.0.1:3306/hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: trueserverTimezone必须显式指定否则 MySQL 8.0 下时间字段会差 8 小时挂号时间对不上是医疗系统里最容易被投诉的问题之一。连接池最大连接数别拍脑袋写 100医疗系统并发没那么高20 到 30 足够写太大反而在数据库侧堆积慢查询。3. MySQL 建表号源、问诊、处方三张核心表怎么设计3.1 号源表别用「剩余数量」字段硬扣很多人设计号源表时喜欢加一个remain_count字段每次挂号减一。这个做法在并发下会出问题两个请求同时读到剩余 1都判断可以挂结果超卖。正确做法是把号源拆成「排班记录 号源明细」或者用乐观锁版本号。CREATE TABLE doctor_schedule ( id BIGINT NOT NULL AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT 医生ID, dept_id BIGINT NOT NULL COMMENT 科室ID, work_date DATE NOT NULL COMMENT 出诊日期, time_slot TINYINT NOT NULL COMMENT 时段 1上午 2下午, total_count INT NOT NULL DEFAULT 0 COMMENT 总号源, locked_count INT NOT NULL DEFAULT 0 COMMENT 已锁定, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停诊, PRIMARY KEY (id), UNIQUE KEY uk_doctor_date_slot (doctor_id,work_date,time_slot), KEY idx_dept_date (dept_id,work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班号源;扣号源时用一条带条件的 UPDATE把判断和扣减合并成原子操作UPDATE doctor_schedule SET locked_count locked_count 1, version version 1 WHERE id #{scheduleId} AND status 1 AND locked_count total_count;返回影响行数为 0 就说明号源已满直接抛业务异常。这个写法比「先查再改」可靠得多也不需要分布式锁。uk_doctor_date_slot这个唯一索引还能防止同一医生同一天同一时段被重复插入排班。3.2 问诊会话表状态机是灵魂图文问诊和视频问诊的会话生命周期不一样但状态流转可以统一抽象。核心状态有待接诊、进行中、已结束、已取消、超时关闭。CREATE TABLE consult_session ( id BIGINT NOT NULL AUTO_INCREMENT, session_no VARCHAR(32) NOT NULL COMMENT 会话编号, patient_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL, type TINYINT NOT NULL COMMENT 1图文 2视频, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待接诊 1进行中 2已结束 3已取消 4超时, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, start_time DATETIME DEFAULT NULL, end_time DATETIME DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_session_no (session_no), KEY idx_patient_status (patient_id,status), KEY idx_doctor_status (doctor_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT问诊会话;状态字段用 TINYINT 而不是 VARCHAR索引体积小、比较快。idx_patient_status和idx_doctor_status这两个联合索引是给「我的问诊列表」和「医生待接诊列表」用的没有它们列表页在数据量上来后会全表扫描。3.3 处方表主从结构 药品快照处方必须拆主表和明细表而且明细里要冗余药品名称和单价。为什么因为药品价格会变如果只存药品 ID半年后回头看处方金额对不上这是医疗纠纷的高发点。CREATE TABLE prescription ( id BIGINT NOT NULL AUTO_INCREMENT, presc_no VARCHAR(32) NOT NULL, session_id BIGINT NOT NULL, patient_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL, diagnosis VARCHAR(500) DEFAULT NULL COMMENT 诊断, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已通过 2已驳回, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_presc_no (presc_no), KEY idx_session (session_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT处方主表; CREATE TABLE prescription_item ( id BIGINT NOT NULL AUTO_INCREMENT, presc_id BIGINT NOT NULL, drug_id BIGINT NOT NULL, drug_name VARCHAR(100) NOT NULL COMMENT 药品名称快照, spec VARCHAR(50) DEFAULT NULL COMMENT 规格快照, unit_price DECIMAL(10,2) NOT NULL COMMENT 单价快照, quantity INT NOT NULL, usage_desc VARCHAR(200) DEFAULT NULL COMMENT 用法用量, PRIMARY KEY (id), KEY idx_presc (presc_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT处方明细;drug_name、spec、unit_price这三个快照字段是血泪经验别为了「数据库范式」省掉它们。处方一旦生成就应该是一份不可变的历史记录。4. 原生 APP 与后端联调接口约定和三个必调参数4.1 统一返回体是联调的地基原生 APP 和后端联调最容易吵起来的就是返回格式。后端一会儿返回数组一会儿返回对象前端解析逻辑写一堆 if。解决办法是定一个统一返回体所有接口无例外。Data public class RT { private int code; // 0成功非0失败 private String msg; private T data; public static T RT ok(T data) { RT r new R(); r.code 0; r.msg success; r.data data; return r; } public static T RT fail(int code, String msg) { RT r new R(); r.code code; r.msg msg; return r; } }code用 0 表示成功而不是 200。原因是 HTTP 状态码和业务状态码要分开HTTP 200 只代表请求到达了服务器业务是否成功看code。这样 APP 端拦截器只需要判断code ! 0就统一弹提示不用每个接口单独处理。4.2 三个必调参数第一个是分页参数。列表接口统一用pageNum和pageSize别一个接口叫page另一个叫current。MyBatis-Plus 的分页插件配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor page new PaginationInnerInterceptor(DbType.MYSQL); page.setMaxLimit(100L); // 单页最大100条防止APP传pageSize10000拖垮数据库 interceptor.addInnerInterceptor(page); return interceptor; } }setMaxLimit这个参数一定要设。我见过 APP 端传pageSize99999把数据库连接打满的情况加了这个限制超过就按 100 处理。第二个是时间格式。全局配置 Jackson 序列化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai不配这个APP 收到的可能是时间戳也可能是 ISO 格式前端解析要写兼容逻辑。第三个是 token 传递。原生 APP 一般把 token 放在请求头Authorization里后端拦截器统一校验。别用 CookieAPP 端管理 Cookie 很麻烦。4.3 视频问诊的信令通道视频问诊不是纯 HTTP 能搞定的需要信令通道。常见做法是 APP 端集成第三方音视频 SDK后端只负责生成房间号和签名。后端提供一个接口PostMapping(/consult/video/room) public RVideoRoomVO createRoom(RequestParam Long sessionId) { ConsultSession session consultService.getById(sessionId); if (session null || session.getStatus() ! 1) { return R.fail(4001, 会话不存在或未开始); } VideoRoomVO vo new VideoRoomVO(); vo.setRoomId(room_ session.getSessionNo()); vo.setUserId(session.getPatientId()); vo.setDoctorId(session.getDoctorId()); vo.setExpireAt(System.currentTimeMillis() 3600_000); // 房间1小时有效 return R.ok(vo); }房间号用会话编号拼保证唯一且可追溯。expireAt给 1 小时防止房间被长期占用。签名部分各家 SDK 不一样按官方文档生成即可但签名有效期别设太长医疗场景对隐私要求高。5. 避坑排查这套源码落地时最容易翻车的五个地方5.1 现象挂号接口偶发超卖日志里两个请求都显示扣减成功原因用了「先 SELECT 查剩余再 UPDATE 减一」的写法两个请求在 SELECT 阶段都读到相同余量。解决改成第 3 章里那条带locked_count total_count条件的原子 UPDATE用影响行数判断成败。如果业务复杂到一条 SQL 搞不定就在 service 方法上加Transactional并配合SELECT ... FOR UPDATE行锁但要注意锁的粒度别把整张排班表锁住。5.2 现象APP 端显示的问诊时间比实际早 8 小时原因MySQL 连接串没配serverTimezone或者配成了 UTC而 JVM 时区是东八区两边对不上。解决连接串加serverTimezoneAsia/Shanghai同时application.yml里配spring.jackson.time-zone: Asia/Shanghai。两个地方都要改只改一个还会出问题。改完重启用SELECT NOW()和 Java 的new Date()对比一下。5.3 现象SpringBoot 启动报Invalid bound statement (not found)原因MyBatis-Plus 的 mapper XML 没被打包进去或者mapper-locations路径写错。Maven 默认只打包src/main/resources下的文件如果你的 XML 放在src/main/java旁边需要在pom.xml的build里加资源目录配置。解决build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build5.4 现象升级 SpringBoot 3.x 后所有javax.servlet相关的类报红原因SpringBoot 3.x 迁移到了 Jakarta EE 9包名从javax.*变成jakarta.*。解决全局替换javax.servlet为jakarta.servletjavax.validation为jakarta.validation。但要注意有些第三方库还没跟进比如老版本的 Swaggerspringfox它内部还引用javax会直接启动失败。解决办法是换成 springdoc-openapi它原生支持 SpringBoot 3。5.5 现象处方审核通过后患者端订单金额和处方金额对不上原因处方明细里的药品单价是实时从药品表查的而药品表价格被运营改过。解决处方生成时就把unit_price快照写进prescription_item订单金额直接累加明细里的快照单价不再回查药品表。这个坑我在两个项目里都遇到过本质是把「可变数据」和「历史记录」混在一起了。6. 进阶技巧用 MyBatis-Plus 代码生成器把建表效率提上去前面建表都是手写的实际项目里表一多手写实体类和 mapper 很费时间。MyBatis-Plus 的代码生成器可以根据数据库表反向生成 entity、mapper、service、controller省掉大量重复劳动。热搜里那个「mybatisplus根据java实体类生成创建表的sql语句」是反过来的需求但更常见的做法是先建表再生成代码因为表结构设计需要反复推敲代码可以随时重生成。下面是一个能直接跑的生成器配置public class CodeGenerator { public static void main(String[] args) { String url jdbc:mysql://127.0.0.1:3306/hospital?serverTimezoneAsia/Shanghai; String username root; String password your_password; FastAutoGenerator.create(url, username, password) .globalConfig(builder - builder .author(dev) .outputDir(System.getProperty(user.dir) /src/main/java) .disableOpenDir() ) .packageConfig(builder - builder .parent(com.hospital) .moduleName(module.prescription) // 按模块生成别全堆一个包 .entity(entity) .mapper(mapper) .service(service) .controller(controller) ) .strategyConfig(builder - builder .addInclude(prescription, prescription_item) // 只生成指定表 .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .controllerBuilder() .enableRestStyle() // 生成 RestController .mapperBuilder() .enableBaseResultMap() ) .execute(); } }几个关键参数说明。addInclude一定要写不写会把整个库的表全生成一遍包括你不想要的日志表。moduleName按业务模块填生成的文件会自动落到对应包下避免所有实体类挤在一个包里。enableTableFieldAnnotation会给每个字段加上TableField注解字段名和列名不一致时不用手动改。enableRestStyle生成的是RestController而不是Controller省得自己改。生成完之后别急着用有两件事必须手动做。第一检查主键策略。MyBatis-Plus 默认用雪花算法生成 Long 型 ID如果你的表是AUTO_INCREMENT要在实体类主键上加TableId(type IdType.AUTO)否则插入时会用雪花 ID 覆盖自增。第二检查逻辑删除字段。医疗数据不建议物理删除如果表里有deleted字段在application.yml里配mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配好之后所有deleteById都会变成 UPDATE查询自动过滤已删除数据。这个习惯在医疗系统里很重要病历和处方不能真删。最后说一个验证方法生成代码后先别写业务逻辑直接启动项目用 Swagger 或 Postman 调一下生成的 list 接口。如果能正常返回分页数据说明 mapper 扫描、分页插件、数据库连接这三条链路都通了。这一步过了再往上叠业务逻辑出问题就只可能是业务代码本身排查范围小很多。我自己做这类系统的习惯是每加一个模块先把「建表 → 生成代码 → 跑通 CRUD → 再写业务」这四步走完不跳步。跳步省下的十分钟后面往往要用两小时来还。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →