简介这份互联网医院源码面向医疗信息化开发者与创业团队用于快速搭建支持在线问诊与在线开处方的远程医疗服务平台帮助打破地域限制、提升问诊效率。源码围绕患者与医生的即时沟通展开涵盖文字聊天、语音视频诊疗、病情描述与病历图片上传并支持医生问诊后开具电子处方涉及药品数据库接口、处方审核修改与打印等环节。包内文件以PHP项目结构为主composer.json负责依赖管理app目录承载控制器、模型与视图api提供对外接口web存放静态资源framework为核心框架addons可扩展预约挂号等插件payment对接支付宝、微信支付attachment存储病历与检查报告等附件data用于测试数据。压缩包约83.55MB已有248人学习关注。整体目录清晰适合研究在线医疗业务流程、前后端技术选型与医疗数据合规策略的开发者参考借鉴。1. 互联网医院源码拆包在线问诊与在线开处方到底怎么落地去年帮一个做区域医疗信息化的团队做技术选型对方甩过来一份「互联网医院源码」说是在线问诊和在线开处方都齐了让我评估能不能直接改。我打开一看目录结构确实完整但真正跑起来才发现问诊流程和处方流转之间隔着一整套业务状态机不是把两个模块拼一起就能用的。这份源码解决的核心问题是把「患者发起问诊 → 医生接诊 → 开具处方 → 药师审方 → 处方流转」这条链路用代码固化下来适合有一定后端基础、想快速搭建互联网医院原型或二次开发的从业者。它不是一个能直接上线的成品而是一套需要你理解业务逻辑后重新组装的骨架。下面我从拆包到跑通把这条链路里的关键节点和踩过的坑逐一讲清楚。2. 源码结构拆解问诊模块与处方模块的依赖关系拿到一份互联网医院源码第一件事不是急着跑而是先看清楚模块之间怎么咬合的。很多源码包看起来功能齐全但问诊和处方之间的数据传递是断的医生开完处方处方数据没进审方队列或者审方通过后没回写问诊记录这种「半截子工程」在二次开发时最要命。2.1 目录层级与核心包定位常见的互联网医院源码后端一般分这么几层controller层负责对外接口service层写业务逻辑dao或mapper层对接数据库entity层放数据模型。问诊相关的代码通常集中在consultation包下处方相关的在prescription包下但两者之间的交互往往散落在service层的交叉调用里。我一般会先找三个文件问诊状态枚举、处方状态枚举、以及连接两者的业务服务类。问诊状态一般有「待接诊、问诊中、已结束、已取消」处方状态有「待审核、审核通过、审核驳回、已流转」。这两个状态机的流转规则决定了整个系统的行为边界。# 快速定位核心业务文件以 Java Spring Boot 项目为例 find . -type f -name *.java | grep -iE consultation|prescription | head -30 # 查看问诊状态枚举定义 grep -r enum.*ConsultationStatus --include*.java -l # 查看处方状态枚举定义 grep -r enum.*PrescriptionStatus --include*.java -l上面这几条命令的作用是快速缩小范围。第一条找出所有跟问诊、处方相关的 Java 文件第二条和第三条分别定位两个状态枚举的定义位置。参数上没什么需要调的关键是grep -iE里的正则要覆盖你实际项目里的命名习惯有些源码用inquiry代替consultation用recipe代替prescription得根据实际情况改。2.2 数据库表结构与关键字段问诊和处方的数据模型是整份源码的地基。我见过不少源码包表结构设计得很随意比如问诊记录表里直接存了处方 JSON 字符串而不是通过外键关联处方表。这种设计在单次问诊里能跑通但一旦要做处方统计、药品追溯或者对接医保接口就会非常痛苦。常见的做法是至少拆成这几张表consultation_record问诊记录、prescription处方主表、prescription_item处方明细、drug_dict药品字典。问诊记录表通过consultation_id关联处方主表处方主表通过prescription_id关联明细表。-- 问诊记录表核心字段 CREATE TABLE consultation_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL COMMENT 患者ID, doctor_id BIGINT NOT NULL COMMENT 医生ID, status TINYINT DEFAULT 0 COMMENT 0待接诊 1问诊中 2已结束 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 处方主表核心字段 CREATE TABLE prescription ( id BIGINT PRIMARY KEY AUTO_INCREMENT, consultation_id BIGINT NOT NULL COMMENT 关联问诊记录, doctor_id BIGINT NOT NULL, patient_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待审核 1审核通过 2审核驳回 3已流转, audit_pharmacist_id BIGINT COMMENT 审方药师ID, audit_time DATETIME COMMENT 审核时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 处方明细表 CREATE TABLE prescription_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, prescription_id BIGINT NOT NULL, drug_id BIGINT NOT NULL, drug_name VARCHAR(200) NOT NULL, dosage VARCHAR(100) COMMENT 用法用量, quantity INT NOT NULL COMMENT 数量, unit VARCHAR(20) COMMENT 单位 );建表语句里几个关键点值得注意。consultation_record的status字段用 TINYINT 存枚举值比 VARCHAR 省空间但可读性差建议在代码里维护一份映射。prescription表里的audit_pharmacist_id和audit_time是审方环节的痕迹很多简易源码会漏掉这两个字段导致审方流程无法追溯。prescription_item把药品明细拆出来而不是在主表里存 JSON是为了后续做药品统计和库存扣减时能直接 SQL 聚合。注意如果你拿到的源码里prescription表没有audit_pharmacist_id说明审方环节可能是假的或者审方状态直接写死在代码里这种要特别小心。2.3 问诊到开方的服务调用链从代码层面看问诊到开方的调用链一般是这样的医生在问诊界面点击「开具处方」前端调PrescriptionController.create()Controller 调PrescriptionService.createPrescription()Service 里先校验问诊状态是否为「问诊中」再校验医生是否有处方权然后写入处方主表和明细表最后把问诊记录的状态更新为「已结束」或者保持「问诊中」但标记「已开方」。// PrescriptionService 核心方法示意 Transactional public PrescriptionVO createPrescription(Long consultationId, ListPrescriptionItemDTO items) { // 1. 校验问诊记录状态 ConsultationRecord record consultationMapper.selectById(consultationId); if (record null || record.getStatus() ! ConsultationStatus.IN_PROGRESS) { throw new BusinessException(当前问诊状态不允许开方); } // 2. 校验医生处方权常见做法是查医生权限表 if (!doctorPermissionService.hasPrescriptionRight(record.getDoctorId())) { throw new BusinessException(该医生无处方权); } // 3. 写入处方主表 Prescription prescription new Prescription(); prescription.setConsultationId(consultationId); prescription.setDoctorId(record.getDoctorId()); prescription.setPatientId(record.getPatientId()); prescription.setStatus(PrescriptionStatus.PENDING_AUDIT); prescriptionMapper.insert(prescription); // 4. 写入处方明细 for (PrescriptionItemDTO item : items) { PrescriptionItem entity new PrescriptionItem(); entity.setPrescriptionId(prescription.getId()); entity.setDrugId(item.getDrugId()); entity.setDrugName(item.getDrugName()); entity.setDosage(item.getDosage()); entity.setQuantity(item.getQuantity()); entity.setUnit(item.getUnit()); prescriptionItemMapper.insert(entity); } // 5. 更新问诊记录可选标记已开方 record.setStatus(ConsultationStatus.FINISHED); consultationMapper.updateById(record); return convertToVO(prescription); }这段代码的逻辑说明Transactional保证处方主表和明细表要么都写成功要么都回滚避免出现主表有记录但明细为空的脏数据。第一步校验问诊状态防止医生对已结束的问诊重复开方。第二步校验处方权这是合规要求不是可选项。第三步和第四步是核心写入逻辑。第五步更新问诊状态这里有个设计选择是把问诊直接结束还是保持问诊中但允许继续开方常见做法是首次开方后问诊状态变为「已结束」但如果业务允许一次问诊开多张处方就不能这么写。参数方面consultationId和items是必传的items里每个元素的drugId必须能在药品字典里查到否则后续审方环节会出问题。我一般会在写入前加一道药品字典校验很多源码漏了这一步导致处方里出现不存在的药品 ID。3. 在线问诊模块跑通从接诊到结束的状态流转问诊模块看起来简单不就是患者发消息、医生回消息吗但真正跑起来状态流转才是最容易翻车的地方。我见过一个源码包患者发起问诊后医生端一直显示「待接诊」但数据库里状态已经是「问诊中」了原因是前端轮询接口和状态更新接口用了不同的状态判断逻辑。这种玄学问题不把状态机理清楚根本查不出来。3.1 问诊状态机设计与接口定义问诊状态一般设计成四个PENDING待接诊、IN_PROGRESS问诊中、FINISHED已结束、CANCELLED已取消。流转规则是患者发起问诊 →PENDING医生接诊 →IN_PROGRESS医生结束问诊或开方后 →FINISHED患者或医生取消 →CANCELLED。接口层面至少需要这几个创建问诊、医生接诊、发送消息、结束问诊、查询问诊详情。每个接口都要校验当前状态是否允许该操作。// 问诊状态流转校验工具类 public class ConsultationStateMachine { private static final MapConsultationStatus, SetConsultationStatus TRANSITIONS new HashMap(); static { // 待接诊 - 问诊中医生接诊 TRANSITIONS.put(PENDING, Set.of(IN_PROGRESS, CANCELLED)); // 问诊中 - 已结束医生结束或开方后结束 TRANSITIONS.put(IN_PROGRESS, Set.of(FINISHED, CANCELLED)); // 已结束和已取消是终态不能再流转 TRANSITIONS.put(FINISHED, Set.of()); TRANSITIONS.put(CANCELLED, Set.of()); } public static boolean canTransition(ConsultationStatus from, ConsultationStatus to) { return TRANSITIONS.getOrDefault(from, Set.of()).contains(to); } public static void assertTransition(ConsultationStatus from, ConsultationStatus to) { if (!canTransition(from, to)) { throw new BusinessException(状态不允许从 from 流转到 to); } } }这个状态机工具类的核心是把流转规则集中管理而不是散落在各个 Service 方法里。TRANSITIONS这个 Map 定义了每个状态能去往哪些状态。canTransition返回布尔值assertTransition直接抛异常。我一般会在所有涉及状态变更的地方调用assertTransition这样一旦有非法流转日志里能立刻定位到是哪个接口触发的。参数上没什么需要调的关键是TRANSITIONS的初始化要跟业务规则严格对齐。比如有些业务允许医生在问诊中直接取消那IN_PROGRESS的集合里就要加上CANCELLED。有些业务不允许患者取消已接诊的问诊那IN_PROGRESS里就不能有CANCELLED。3.2 消息推送与轮询的取舍在线问诊的实时性要求不高但也不能让患者发完消息等半天才看到医生回复。常见的做法有两种WebSocket 长连接和 HTTP 轮询。WebSocket 实时性好但服务端要维护连接状态对部署环境有要求。HTTP 轮询实现简单但延迟取决于轮询间隔间隔太短浪费资源太长体验差。我一般会建议如果源码里已经集成了 WebSocket就用 WebSocket如果没有先用轮询把流程跑通后续再替换。轮询间隔设 3 到 5 秒比较平衡太短了数据库压力大太长了患者觉得卡。// 前端轮询获取新消息的简化实现 let lastMessageId 0; let pollingTimer null; function startPolling(consultationId) { pollingTimer setInterval(async () { try { const res await fetch(/api/consultation/${consultationId}/messages?afterId${lastMessageId}); const data await res.json(); if (data.code 0 data.data.length 0) { data.data.forEach(msg { appendMessageToChat(msg); lastMessageId Math.max(lastMessageId, msg.id); }); } } catch (e) { console.error(轮询失败, e); } }, 3000); // 3秒轮询一次 } function stopPolling() { if (pollingTimer) { clearInterval(pollingTimer); pollingTimer null; } }这段前端代码的逻辑是维护一个lastMessageId每次轮询只请求比这个 ID 更新的消息避免重复拉取全量数据。setInterval的 3000 毫秒是轮询间隔可以根据实际并发量调整。stopPolling在离开问诊页面时调用防止定时器泄漏。注意轮询接口一定要加afterId参数做增量查询我见过不少源码直接select * from message where consultation_id ?消息一多就卡死。3.3 医生接诊的并发问题医生接诊这个操作看起来简单但并发场景下容易出问题。两个医生同时点「接诊」如果不加锁可能两个人都接诊成功问诊记录被更新两次患者端看到两个医生。常见做法是用数据库乐观锁或者 Redis 分布式锁。-- 乐观锁方式更新时检查状态是否还是待接诊 UPDATE consultation_record SET doctor_id ?, status 1, update_time NOW() WHERE id ? AND status 0;这条 SQL 的关键在WHERE id ? AND status 0只有当记录还是「待接诊」状态时才会更新成功。如果返回的影响行数是 0说明已经被其他医生接诊了直接提示「该问诊已被接诊」。// Service 层调用 int affected consultationMapper.acceptConsultation(consultationId, doctorId); if (affected 0) { throw new BusinessException(该问诊已被其他医生接诊); }这种乐观锁方式比synchronized或者分布式锁更轻量适合并发不高的场景。如果并发很高比如抢单模式那就得用 Redis 锁或者消息队列串行化。4. 在线开处方模块药品字典、审方与流转开处方模块比问诊复杂得多因为它涉及药品数据、审方规则、处方流转等多个环节。很多源码包把开方做成了「填个表单存库」但真正的互联网医院处方系统审方环节才是核心。没有审方的处方就是一张废纸。4.1 药品字典的初始化与匹配药品字典是处方模块的基础数据。源码包里一般会带一份初始药品数据可能是 SQL 文件或者 JSON 文件。我拿到手第一件事是看药品字典的字段设计至少要有药品通用名、商品名、规格、剂型、单位、是否处方药、是否医保。-- 药品字典表 CREATE TABLE drug_dict ( id BIGINT PRIMARY KEY AUTO_INCREMENT, generic_name VARCHAR(200) NOT NULL COMMENT 通用名, trade_name VARCHAR(200) COMMENT 商品名, specification VARCHAR(100) COMMENT 规格, dosage_form VARCHAR(50) COMMENT 剂型, unit VARCHAR(20) COMMENT 单位, is_prescription TINYINT DEFAULT 1 COMMENT 是否处方药, is_insurance TINYINT DEFAULT 0 COMMENT 是否医保, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 );初始化数据一般放在src/main/resources/db/或者sql/目录下。导入时注意字符集药品名里常有生僻字用utf8mb4比较稳妥。# 导入药品字典数据 mysql -u root -p hospital_db sql/drug_dict_init.sql # 验证导入结果 mysql -u root -p hospital_db -e SELECT COUNT(*) FROM drug_dict;导入后一定要验证条数我遇到过 SQL 文件里用了INSERT IGNORE结果一半数据因为主键冲突被跳过了但没有任何报错。4.2 处方开具的校验逻辑医生开处方时前端传过来的是药品 ID 列表和用法用量后端要做多层校验药品是否存在且启用、是否处方药、剂量是否在合理范围、是否有配伍禁忌这个一般需要额外的规则引擎。public void validatePrescriptionItems(ListPrescriptionItemDTO items) { for (PrescriptionItemDTO item : items) { // 1. 药品存在性校验 DrugDict drug drugDictMapper.selectById(item.getDrugId()); if (drug null || drug.getStatus() ! 1) { throw new BusinessException(药品不存在或已停用: item.getDrugId()); } // 2. 处方药标识校验互联网医院一般只能开处方药 if (drug.getIsPrescription() ! 1) { throw new BusinessException(非处方药不允许在此开具: drug.getGenericName()); } // 3. 数量校验 if (item.getQuantity() null || item.getQuantity() 0 || item.getQuantity() 99) { throw new BusinessException(药品数量不合法: drug.getGenericName()); } // 4. 用法用量非空校验 if (StringUtils.isBlank(item.getDosage())) { throw new BusinessException(用法用量不能为空: drug.getGenericName()); } } }这段校验逻辑里第一条和第二条是硬性要求第三条的数量上限 99 是我一般会加的防止误操作开出天量处方。第四条用法用量非空很多源码不校验这个结果处方上药品没有用法药师审方时直接驳回。4.3 审方流程与状态回写审方是处方模块的核心环节。流程一般是医生提交处方 → 处方进入待审核队列 → 药师审核 → 审核通过或驳回 → 状态回写到处方主表 → 通知医生和患者。// 药师审方接口 Transactional public void auditPrescription(Long prescriptionId, Long pharmacistId, boolean approved, String reason) { Prescription prescription prescriptionMapper.selectById(prescriptionId); if (prescription null) { throw new BusinessException(处方不存在); } if (prescription.getStatus() ! PrescriptionStatus.PENDING_AUDIT) { throw new BusinessException(该处方不在待审核状态); } prescription.setAuditPharmacistId(pharmacistId); prescription.setAuditTime(new Date()); if (approved) { prescription.setStatus(PrescriptionStatus.AUDIT_PASSED); } else { prescription.setStatus(PrescriptionStatus.AUDIT_REJECTED); prescription.setRejectReason(reason); } prescriptionMapper.updateById(prescription); // 通知医生常见做法是发站内信或消息队列 notificationService.notifyDoctor(prescription.getDoctorId(), approved ? 处方审核通过 : 处方被驳回: reason); }审方接口的关键是状态校验和回写。PENDING_AUDIT状态才能审核审核后根据结果更新为AUDIT_PASSED或AUDIT_REJECTED。驳回时一定要记录原因否则医生不知道为什么被驳回只能干瞪眼。注意审方接口一定要加权限校验只有药师角色才能调用。我见过源码里审方接口没做角色校验任何登录用户都能调这是严重的安全漏洞。5. 避坑与排查源码二次开发中的五个血泪教训5.1 问诊状态和处方状态不同步现象医生开完处方后问诊记录还是「问诊中」患者端一直显示可以继续发消息但医生端已经看不到这个问诊了。原因开方逻辑里更新了处方表但忘了更新问诊记录的状态或者更新问诊状态的代码在事务提交前被异常中断了。解决把开方和更新问诊状态放在同一个Transactional方法里确保要么都成功要么都回滚。同时在问诊详情接口里加一层判断如果关联处方已存在且状态不是驳回问诊状态强制显示为「已结束」。5.2 药品字典 ID 对不上现象前端选完药品提交后端报「药品不存在」但数据库里明明有这条记录。原因前端用的药品 ID 是自增主键但后端校验时查的是另一个字段或者前后端用的字典数据版本不一致。解决统一用药品字典的主键 ID 作为前后端交互字段不要用药品编码之类的业务字段。初始化数据后前后端都从同一个接口拉取药品列表不要各自维护一份。5.3 审方队列堆积但没人处理现象处方提交后一直显示「待审核」药师端却看不到待审核列表。原因审方列表查询条件写错了比如WHERE status 0但实际待审核状态值是 1或者查询时加了doctor_id过滤导致药师看不到其他医生的处方。解决检查审方列表的查询 SQL确保状态条件跟枚举定义一致。药师端查询不应该按医生过滤应该按状态和创建时间排序。5.4 处方明细数量与主表不一致现象处方主表显示 3 种药但明细表只有 2 条记录。原因写入明细时循环里某个药品校验失败抛了异常但主表已经插入成功了事务没有回滚。解决确保主表和明细的写入在同一个事务里并且明细写入前先做完整校验不要边写边校验。我一般会先遍历一遍所有明细做校验全部通过后再统一写入。5.5 轮询接口把数据库打满现象问诊高峰期数据库 CPU 飙升慢查询日志里全是消息轮询的 SQL。原因轮询接口没有加增量条件每次都是全量查询而且没有加索引。解决轮询接口必须带afterId参数并且consultation_id和id上要建联合索引。如果并发量很大考虑把轮询间隔从 3 秒调到 5 秒或者改用 WebSocket。6. 二次开发进阶把审方规则做成可配置的源码包里的审方逻辑一般是写死的比如只校验处方状态和药师权限。但实际业务里审方规则可能经常变某些药品需要双药师审核、某些科室的处方需要主任医师确认、某些药品有剂量上限。如果每次改规则都改代码维护成本太高。我一般会建议把审方规则抽成配置表用简单的规则引擎来驱动。下面是一个最小化的规则配置方案。-- 审方规则配置表 CREATE TABLE audit_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(100) NOT NULL, rule_type VARCHAR(50) NOT NULL COMMENT 规则类型drug_limit/dept_limit/double_audit, rule_condition VARCHAR(500) COMMENT 规则条件JSON格式, rule_action VARCHAR(200) COMMENT 触发动作, priority INT DEFAULT 0 COMMENT 优先级数字越大越先执行, status TINYINT DEFAULT 1 ); -- 示例规则某药品单次剂量超过 10 需要双药师审核 INSERT INTO audit_rule (rule_name, rule_type, rule_condition, rule_action, priority) VALUES (某药品剂量限制, drug_limit, {drugId: 1001, maxQuantity: 10}, double_audit, 10);规则表的核心字段是rule_type和rule_condition。rule_type决定用哪个规则处理器rule_condition存 JSON 格式的条件参数。priority控制规则执行顺序数字大的先执行。// 规则引擎简化实现 public AuditResult executeRules(Prescription prescription) { ListAuditRule rules auditRuleMapper.selectByStatus(1); rules.sort(Comparator.comparingInt(AuditRule::getPriority).reversed()); for (AuditRule rule : rules) { JSONObject condition JSON.parseObject(rule.getRuleCondition()); if (drug_limit.equals(rule.getRuleType())) { Long drugId condition.getLong(drugId); Integer maxQuantity condition.getInteger(maxQuantity); for (PrescriptionItem item : prescription.getItems()) { if (item.getDrugId().equals(drugId) item.getQuantity() maxQuantity) { return AuditResult.needDoubleAudit(rule.getRuleName()); } } } if (dept_limit.equals(rule.getRuleType())) { String deptCode condition.getString(deptCode); if (deptCode.equals(prescription.getDeptCode())) { return AuditResult.needDoubleAudit(rule.getRuleName()); } } } return AuditResult.pass(); }这段规则引擎的逻辑是按优先级倒序加载所有启用的规则逐条匹配。drug_limit类型检查药品剂量是否超限dept_limit检查科室是否命中。命中后返回需要双审的结果否则返回通过。参数方面rule_condition的 JSON 结构要根据rule_type来定没有统一标准。我一般会在代码里维护一份规则类型和对应 JSON schema 的文档避免配置时写错字段名。验证规则是否生效最简单的办法是造一条测试处方看审方接口返回的结果是否符合预期。我习惯在开发环境把规则执行日志打到控制台方便排查哪条规则命中了、哪条没命中。从那以后我每次拿到新的互联网医院源码都强制先跑一遍「问诊 → 开方 → 审方 → 流转」的完整链路把每个状态变更点都打上日志确认没有断点才开始改业务代码。希望帮到你。本文还有配套的精品资源点击获取