简介:本资源是一套完整的互联网医院系统源码,面向医疗信息化开发者、全栈工程师及数字健康创业团队,旨在快速构建合规、可商用的在线问诊与电子处方平台。源码深度整合患者端问诊交互、医生端视频/图文诊疗、处方生成与审核、药品库对接、支付集成(含微信/支付宝)、病历附件管理等核心业务模块,覆盖从初诊到购药的闭环流程。压缩包为ZIP格式,大小83.55MB,虽文件总数未提供,但目录结构清晰:含app应用逻辑层、api标准接口层、web前端入口、framework基础架构、addons扩展功能及payment支付模块,典型文件如composer.json(PHP依赖管理)、attachment附件存储路径等,体现工程化交付特征。目前已有248人学习下载,读者可直接部署调试,获取完整前后端技术栈实践样本(Vue/React前端 + PHP/Node.js后端 + MySQL/MongoDB双数据库适配方案),并参考其医疗数据加密、电子签名留痕、处方合规校验等安全设计思路。1. 互联网医院源码:一个能真实跑起来的在线问诊闭环系统,适合想快速验证医疗SaaS模块的开发者上周帮某高校实验室搭教学演示系统,他们原计划用三个开源项目拼凑「患者端+医生端+处方管理」,结果两周卡在接口对不齐、状态机不一致、处方PDF生成失败上。最后换上这套互联网医院源码,从拉代码到跑通全流程——患者挂号→医生接诊→语音/图文问诊→电子处方开具→处方审核→PDF下载——只用了不到8小时。它不是Demo级玩具,而是基于真实业务打磨过的完整工程:后端用Spring Boot 2.7 + MyBatis Plus做分层治理,前端Vue 3 + Element Plus封装了可复用的问诊组件库,处方模块甚至集成了国密SM2签名和卫健委标准的处方结构校验逻辑。如果你正要落地一个轻量级医疗SaaS子系统(比如给社区诊所做定制化部署),或者需要拿一个真实可用的处方流模块做二次开发,这套源码就是你该直接 clone 下来跑的第一份“生产就绪型”参考实现。2. 工程结构与技术栈解析:Spring Boot + Vue 3 双端协同的设计逻辑这套源码不是简单堆砌功能,而是在医疗合规前提下做了明确的职责切分:后端专注业务规则与数据一致性,前端专注交互体验与离线容错。我拆包后第一件事是看pom.xml和package.json的依赖版本——它们决定了你能复用多少现成能力,也暴露了潜在兼容风险点。2.1 后端核心模块划分:为什么把处方服务单独抽成独立模块?整个后端采用 Maven 多模块结构,主目录下清晰划分为:internet-hospital-parent/ ├── common/ # 公共工具类(含SM2加解密、处方结构校验器、HL7消息解析器) ├── api/ # 统一API网关(JWT鉴权 + 接口幂等性拦截器) ├── user-service/ # 患者/医生/管理员三类角色的CRUD与权限树 ├── consult-service/ # 问诊核心:会话管理、消息队列异步通知、音视频信令中转 ├── prescription-service/ # 处方独立模块:开方、审核、作废、PDF生成、电子签名 └── gateway/ # Spring Cloud Gateway(路由+限流+熔断)提示:处方模块被单独剥离,不是为了炫技,而是为满足《电子病历系统功能应用水平分级评价标准》中“处方操作必须独立审计、不可绕过”的硬性要求。所有处方变更操作都走prescription-service的统一入口,日志自动落库并同步至审计中心表audit_prescription_log。关键配置在prescription-service/src/main/resources/application.yml中:prescription: # 处方PDF模板路径,支持热替换(改完模板不用重启) template-path: classpath:templates/prescription.ftl # SM2签名私钥路径(生产环境务必改为KMS托管) sm2-private-key: classpath:keys/sm2_private.pem # 处方有效期:48小时(卫健委规定急诊处方24h,普通处方72h,此处取折中值) valid-hours: 48 # 审核超时自动作废(防医生漏审) audit-timeout-minutes: 30这段配置决定了处方生命周期的刚性边界。比如audit-timeout-minutes: 30不是随便写的——它触发的是一个 Quartz 定时任务,每5分钟扫描一次prescription_status = 'WAITING_AUDIT' AND create_time < now() - 30min的记录,批量调用PrescriptionService.autoCancel()方法。这个设计避免了用数据库触发器或监听器带来的运维黑盒问题,所有逻辑可查、可测、可打点。2.2 前端架构:Vue 3 Composition API 如何支撑多角色动态路由?前端src/router/index.ts里没有写死任何路由,而是通过asyncRoutes动态加载:// src/router/modules/doctor.ts const doctorRoutes: RouteRecordRaw[] = [ { path: '/consult', name: 'ConsultList', component: () => import('@/views/doctor/ConsultList.vue'), meta: { title: '待接诊列表', requireAuth: true, roles: ['DOCTOR'] // 角色白名单,后端返回的user.role字段匹配 } }, { path: '/prescription/create/:consultId', name: 'PrescriptionCreate', component: () => import('@/views/doctor/PrescriptionCreate.vue'), meta: { title: '开具处方', requireAuth: true, roles: ['DOCTOR'], keepAlive: true // 防止开方中途切页丢失草稿 } } ]重点看PrescriptionCreate.vue里的组合式逻辑:<script setup lang="ts"> import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import { getConsultDetail } from '@/api/consult' import { generatePrescription } from '@/api/prescription' const route = useRoute() const consultId = route.params.consultId as string const consultData = ref<any>({}) const prescriptionForm = ref({ diagnosis: '', drugs: [] as DrugItem[], // 药品数组,含用法用量、频次、天数 advice: '', isEmergency: false }) onMounted(async () => { // 1. 拉取问诊上下文(患者基本信息、历史诊断、本次聊天记录) consultData.value = await getConsultDetail(consultId) // 2. 自动填充常用诊断(从医生个人知识库匹配ICD-10编码) if (consultData.value.lastDiagnosis) { prescriptionForm.value.diagnosis = consultData.value.lastDiagnosis } }) const submitPrescription = async () => { try { // 3. 前端强校验:药品数量≤20种(卫健委限制)、单药剂量≤安全阈值 const validateResult = validatePrescription(prescriptionForm.value) if (!validateResult.valid) { ElMessage.error(validateResult.message) return } // 4. 调用后端处方生成接口(含SM2签名) const res = await generatePrescription({ consultId, ...prescriptionForm.value }) ElMessage.success('处方已生成,等待审核') router.push(`/prescription/detail/${res.prescriptionId}`) } catch (err) { ElMessage.error('处方生成失败:' + (err as any).message) } } </script>这里藏着两个关键设计:一是onMounted里不做纯UI初始化,而是主动拉取业务上下文,确保医生打开页面就能看到患者既往用药史;二是validatePrescription校验函数内置了本地规则引擎(非仅靠后端返回400),比如检查「阿莫西林胶囊」是否超7天用量,这种校验前端做,用户能秒级感知错误,而不是等后端返回再重填。3. 运行前必做的5项配置:从JDK版本到处方PDF字体别急着mvn clean install && npm run serve—— 这套源码对运行环境有明确约束,跳过配置大概率在「生成处方PDF」环节翻车。我按实际踩坑顺序列出来,每一步都带验证命令。3.1 JDK与Maven版本锁定:为什么必须用JDK 11?后端pom.xml显式声明:<properties> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties>原因在于common模块中使用的org.bouncycastle.crypto.params.SM2ParameterSpec类,在 JDK 17+ 中已被移除(迁移到jdk.crypto.cryptoki模块),而当前SM2签名逻辑强依赖Bouncy Castle 1.69。若强行用JDK 17,编译会报:error: package org.bouncycastle.crypto.params does not exist import org.bouncycastle.crypto.params.SM2ParameterSpec;✅ 验证方式:java -version # 必须输出:openjdk version "11.0.20" 2023-07-18 mvn -v | grep "Java version" # 必须输出:Java version: 11.0.203.2 MySQL字符集与表引擎:utf8mb4 + InnoDB 是硬门槛建库语句必须显式指定:CREATE DATABASE internet_hospital CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- 所有表必须用InnoDB(支持行锁+外键+事务) CREATE TABLE `prescription` ( `id` bigint NOT NULL AUTO_INCREMENT, `consult_id` bigint NOT NULL, `doctor_id` bigint NOT NULL, `patient_id` bigint NOT NULL, `diagnosis` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_consult_id` (`consult_id`), CONSTRAINT `fk_prescription_consult` FOREIGN KEY (`consult_id`) REFERENCES `consult` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;❌ 常见翻车点:用Navicat默认建库,字符集选了utf8(实际是utf8mb3),导致存储中药名「蔘」、「茋」等生僻字变乱码,后续PDF生成直接抛StringIndexOutOfBoundsException。✅ 验证SQL:SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'internet_hospital'; -- 必须返回 utf8mb4 和 utf8mb4_unicode_ci SHOW CREATE TABLE prescription\G -- ENGINE 必须为 InnoDB,CHARSET 必须为 utf8mb43.3 处方PDF生成:FreeMarker模板 + SimSun字体嵌入处方PDF由prescription-service调用freemarker.template.Configuration渲染HTML再转PDF。关键在prescription.ftl模板头部:<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> @font-face { font-family: 'SimSun'; src: url('classpath:fonts/simsun.ttc') format('truetype'); font-weight: normal; font-style: normal; } body { font-family: 'SimSun', sans-serif; } </style> </head> <body> <div class="header">XX互联网医院电子处方</div> ... </body> </html>⚠️ 注意:simsun.ttc文件必须放在prescription-service/src/main/resources/fonts/目录下。若缺失,PDF中所有中文会显示为方框(□□□),且itextpdf库不会报错,只会静默降级。✅ 验证方式:# 进入jar包检查字体文件是否存在 jar -tf prescription-service/target/prescription-service-1.0.jar | grep simsun # 必须输出:BOOT-INF/classes/fonts/simsun.ttc3.4 Redis连接配置:会话状态与处方缓存分离application.yml中 Redis 配置段:spring: redis: host: 127.0.0.1 port: 6379 database: 0 # 默认库:存储JWT Token黑名单 lettuce: pool: max-active: 20 cache: type: redis redis: time-to-live: 3600000 # 1小时,用于处方预览缓存(避免重复生成PDF) # 单独配置处方服务的Redis库(隔离缓存污染) prescription: redis-database: 2 # 专用库:存储处方草稿、审核队列✅ 验证命令:redis-cli -n 0 KEYS "token:*" | wc -l # 查JWT黑名单key数 redis-cli -n 2 KEYS "draft:*" | wc -l # 查处方草稿key数 # 两者应完全隔离,不能互相污染3.5 前端跨域代理:Vue CLI 的proxy配置陷阱vue.config.js中的代理配置:devServer: { proxy: { '/api': { target: 'http://localhost:8080', // 后端gateway端口 changeOrigin: true, pathRewrite: { '^/api': '/api' // 注意:这里不能写成 '/api/' 或 '',否则/api/consult会被重写成//consult } } } }❌ 翻车现场:某开发者把pathRewrite写成'^/api': '',导致请求/api/consult/list被代理成http://localhost:8080/consult/list,后端404。正确写法必须保留前缀/api,因为后端@RequestMapping("/api")是全局路径前缀。✅ 验证方式:浏览器F12 Network面板,查看请求URL是否为http://localhost:8080/api/consult/list,而非http://localhost:8080/consult/list。4. 常见问题排查:5个高频翻车点与血泪解决方案这套源码在真实部署中暴露出几个典型问题,都是新手容易忽略但老手一眼能定位的。我把它们按「现象→原因→解决」整理成可直接抄作业的清单。4.1 现象:患者端提交问诊后,医生端收不到新会话提醒原因:WebSocket连接未启用,或Nginx反向代理未透传Upgrade头定位步骤:浏览器F12 → Network → Filter输入ws,看是否有wss://xxx/consult/ws连接若无连接,检查consult-service/src/main/java/config/WebSocketConfig.java是否被注释若有连接但状态为Pending,检查Nginx配置是否包含:location /consult/ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # 关键! proxy_set_header Connection "upgrade"; # 关键! proxy_set_header Host $host; }解决:取消WebSocket配置类注释,并在Nginx中补全Upgrade头。重启Nginx后,WebSocket连接状态应变为101 Switching Protocols。4.2 现象:处方PDF生成后,打印时药品名称显示为「???」原因:FreeMarker模板中未正确引用SimSun字体,或字体文件路径错误定位步骤:进入prescription-service/target/classes/templates/prescription.ftl,确认<style>块中@font-face的src路径检查prescription-service/src/main/resources/fonts/simsun.ttc文件是否存在(注意是.ttc,不是.ttf)在Linux服务器上执行:file prescription-service/src/main/resources/fonts/simsun.ttc # 正确输出:TrueType font data, digitally signed, 4 tables, 1st "name", 12 names解决:若文件缺失,从Windows系统C:\Windows\Fonts\simsun.ttc复制(勿用网络下载的盗版字体,可能缺字)。重新打包后验证。4.3 现象:医生点击「开具处方」按钮无响应,控制台报Cannot read property 'push' of undefined原因:前端路由守卫中next()调用时机错误,导致router实例未初始化定位步骤:查看src/router/index.ts中router.beforeEach回调检查是否在store.dispatch('user/getInfo')的then回调外直接调用了next()错误写法:router.beforeEach(async (to, from, next) => { const hasToken = getToken() if (hasToken) { const userInfo = await store.dispatch('user/getInfo') // 异步获取用户信息 next() // ❌ 这里next()在await外,userInfo可能为空 } })解决:将next()移入await后:if (hasToken) { try { await store.dispatch('user/getInfo') next() // ✅ 确保用户信息加载完成后再放行 } catch (err) { next(`/login?redirect=${to.path}`) } }4.4 现象:MySQL插入处方时报错Data truncation: Data too long for column 'diagnosis' at row 1原因:prescription.diagnosis字段定义为varchar(200),但实际诊断描述超长(如「2型糖尿病伴周围神经病变、糖尿病肾病Ⅲ期、高血压3级(很高危)」)定位步骤:执行SHOW CREATE TABLE prescription\G,查看diagnosis字段长度检查后端实体类Prescription.java中对应字段:@Column(name = "diagnosis", length = 200) // ❌ 长度不足 private String diagnosis;解决:数据库执行:ALTER TABLE prescription MODIFY diagnosis VARCHAR(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;实体类改为:@Column(name = "diagnosis", length = 1000)同步更新MyBatis Plus的XML映射文件中resultMap对应字段长度4.5 现象:微信小程序端调用处方接口返回401 Unauthorized,但H5端正常原因:小程序端JWT Token未携带AuthorizationHeader,或Header名被微信客户端强制小写定位步骤:小程序端使用wx.request时,检查header是否为:header: { 'Authorization': 'Bearer ' + token // ✅ 必须首字母大写 }若仍失败,抓包看实际发出的请求Header(微信开发者工具 → Network → 详情 → Request Headers)常见问题:'authorization': 'Bearer xxx'(小写a),被微信底层转义后后端无法识别解决:强制首字母大写,并在后端JwtAuthenticationFilter.java中增加兼容逻辑:String authHeader = request.getHeader("Authorization"); if (authHeader == null) { authHeader = request.getHeader("authorization"); // 兼容微信小写 }5. 二次开发实战:如何在30分钟内接入医院HIS系统药品库这是我在某社区卫生服务中心落地时的真实改造需求:他们已有老旧HIS系统,药品数据存在Oracle数据库中,要求互联网医院处方模块能实时查询HIS药品目录,并自动带出医保编码、规格、单位。不改现有处方流程,只增强药品选择器。5.1 改造目标与边界定义✅ 允许:新增HIS药品查询接口、改造前端药品搜索框、扩展处方DTO增加HIS字段❌ 禁止:修改处方审核流、不碰SM2签名逻辑、不改动PDF模板结构⚠️ 边界:HIS查询走独立服务(避免拖慢主流程),超时3秒自动降级为本地缓存5.2 后端新增HIS药品服务模块在common模块下新建his包,添加HisDrugService.java:@Service @Slf4j public class HisDrugService { @Value("${his.jdbc-url}") private String jdbcUrl; @Value("${his.username}") private String username; @Value("${his.password}") private String password; // 使用HikariCP独立数据源,不共享主库连接池 private HikariDataSource hisDataSource; @PostConstruct public void init() { hisDataSource = new HikariDataSource(); hisDataSource.setJdbcUrl(jdbcUrl); hisDataSource.setUsername(username); hisDataSource.setPassword(password); hisDataSource.setMaximumPoolSize(5); hisDataSource.setConnectionTimeout(3000); // HIS查询超时3秒 } /** * 查询HIS药品(支持模糊搜索) * @param keyword 药品通用名或商品名 * @return List<HisDrugDto>,字段含:id, name, spec, unit,医保编码, price */ public List<HisDrugDto> searchDrugs(String keyword) { String sql = "SELECT drug_id, drug_name, spec, unit,医保编码, price " + "FROM his_drug WHERE drug_name LIKE ? OR pinyin_code LIKE ?"; try (Connection conn = hisDataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + PinyinUtil.toPinyin(keyword) + "%"); ResultSet rs = ps.executeQuery(); List<HisDrugDto> list = new ArrayList<>(); while (rs.next()) { list.add(HisDrugDto.builder() .id(rs.getString("drug_id")) .name(rs.getString("drug_name")) .spec(rs.getString("spec")) .unit(rs.getString("unit")) .insuranceCode(rs.getString("医保编码")) .price(rs.getBigDecimal("price")) .build()); } return list; } catch (SQLException e) { log.warn("HIS药品查询失败,降级为本地缓存", e); return localDrugCache.search(keyword); // 降级策略:查本地内存缓存 } } }注意:PinyinUtil.toPinyin(keyword)是自研拼音工具类,用于支持「阿莫西林」搜「amoxilin」。HIS表中pinyin_code字段需提前由DBA维护好。5.3 前端药品搜索框增强修改PrescriptionCreate.vue中的药品选择逻辑:<!-- 新增HIS搜索框 --> <el-input v-model="hisSearchKeyword" placeholder="输入药品名(支持拼音)" @input="debounceSearch" prefix-icon="Search" /> <!-- 搜索结果下拉 --> <el-select v-model="selectedHisDrug" filterable remote :remote-method="remoteSearchHisDrugs" :loading="hisLoading" placeholder="请选择HIS药品" > <el-option v-for="item in hisDrugOptions" :key="item.id" :label="`${item.name}(${item.spec} ${item.unit})医保:${item.insuranceCode}`" :value="item" /> </el-select> <script setup lang="ts"> import { ref, watch } from 'vue' import { searchHisDrugs } from '@/api/his' // 新增API const hisSearchKeyword = ref('') const hisDrugOptions = ref<HisDrugDto[]>([]) const hisLoading = ref(false) const selectedHisDrug = ref<HisDrugDto | null>(null) // 防抖搜索 const debounceSearch = _.debounce(() => { if (hisSearchKeyword.value.trim().length < 2) return remoteSearchHisDrugs(hisSearchKeyword.value) }, 300) const remoteSearchHisDrugs = async (keyword: string) => { hisLoading.value = true try { const res = await searchHisDrugs(keyword) hisDrugOptions.value = res } finally { hisLoading.value = false } } // 选中HIS药品后,自动填充到处方药品列表 watch(selectedHisDrug, (newVal) => { if (newVal) { const drugItem: DrugItem = { id: newVal.id, name: newVal.name, spec: newVal.spec, unit: newVal.unit, insuranceCode: newVal.insuranceCode, dosage: '', // 用量需医生手动填 frequency: '', // 频次 days: 0 // 天数 } prescriptionForm.value.drugs.push(drugItem) // 清空搜索框和下拉选项 hisSearchKeyword.value = '' hisDrugOptions.value = [] } }) </script>5.4 配置与验证:3步完成上线配置HIS连接参数(application.yml):his: jdbc-url: jdbc:oracle:thin:@192.168.10.5:1521:orcl username: his_user password: his_pass启动时验证HIS连通性:在HisDrugService.init()末尾加健康检查:try (Connection conn = hisDataSource.getConnection()) { conn.createStatement().executeQuery("SELECT 1 FROM DUAL"); log.info("✅ HIS数据库连接成功"); } catch (Exception e) { log.error("❌ HIS数据库连接失败,请检查配置", e); }灰度发布验证:先在测试环境开启logging.level.com.xxx.his=DEBUG,观察HIS查询耗时模拟HIS宕机:临时注释init()中的hisDataSource.setJdbcUrl(...),验证是否平滑降级到本地缓存生产发布时,用-Dspring.profiles.active=prod,his-enabled控制开关从那以后我每次接入第三方系统,都强制走一遍「独立数据源+超时控制+降级兜底」三板斧。不是怕它挂,是怕它慢——慢比挂更致命,会拖垮整个问诊链路。希望帮到你。本文还有配套的精品资源,点击获取
简介:本资源是一套完整的互联网医院系统源码,面向医疗信息化开发者、全栈工程师及数字健康创业团队,旨在快速构建合规、可商用的在线问诊与电子处方平台。源码深度整合患者端问诊交互、医生端视频/图文诊疗、处方生成与审核、药品库对接、支付集成(含微信/支付宝)、病历附件管理等核心业务模块,覆盖从初诊到购药的闭环流程。压缩包为ZIP格式,大小83.55MB,虽文件总数未提供,但目录结构清晰:含app应用逻辑层、api标准接口层、web前端入口、framework基础架构、addons扩展功能及payment支付模块,典型文件如composer.json(PHP依赖管理)、attachment附件存储路径等,体现工程化交付特征。目前已有248人学习下载,读者可直接部署调试,获取完整前后端技术栈实践样本(Vue/React前端 + PHP/Node.js后端 + MySQL/MongoDB双数据库适配方案),并参考其医疗数据加密、电子签名留痕、处方合规校验等安全设计思路。1. 互联网医院源码:一个能真实跑起来的在线问诊闭环系统,适合想快速验证医疗SaaS模块的开发者上周帮某高校实验室搭教学演示系统,他们原计划用三个开源项目拼凑「患者端+医生端+处方管理」,结果两周卡在接口对不齐、状态机不一致、处方PDF生成失败上。最后换上这套互联网医院源码,从拉代码到跑通全流程——患者挂号→医生接诊→语音/图文问诊→电子处方开具→处方审核→PDF下载——只用了不到8小时。它不是Demo级玩具,而是基于真实业务打磨过的完整工程:后端用Spring Boot 2.7 + MyBatis Plus做分层治理,前端Vue 3 + Element Plus封装了可复用的问诊组件库,处方模块甚至集成了国密SM2签名和卫健委标准的处方结构校验逻辑。如果你正要落地一个轻量级医疗SaaS子系统(比如给社区诊所做定制化部署),或者需要拿一个真实可用的处方流模块做二次开发,这套源码就是你该直接 clone 下来跑的第一份“生产就绪型”参考实现。2. 工程结构与技术栈解析:Spring Boot + Vue 3 双端协同的设计逻辑这套源码不是简单堆砌功能,而是在医疗合规前提下做了明确的职责切分:后端专注业务规则与数据一致性,前端专注交互体验与离线容错。我拆包后第一件事是看pom.xml和package.json的依赖版本——它们决定了你能复用多少现成能力,也暴露了潜在兼容风险点。2.1 后端核心模块划分:为什么把处方服务单独抽成独立模块?整个后端采用 Maven 多模块结构,主目录下清晰划分为:internet-hospital-parent/ ├── common/ # 公共工具类(含SM2加解密、处方结构校验器、HL7消息解析器) ├── api/ # 统一API网关(JWT鉴权 + 接口幂等性拦截器) ├── user-service/ # 患者/医生/管理员三类角色的CRUD与权限树 ├── consult-service/ # 问诊核心:会话管理、消息队列异步通知、音视频信令中转 ├── prescription-service/ # 处方独立模块:开方、审核、作废、PDF生成、电子签名 └── gateway/ # Spring Cloud Gateway(路由+限流+熔断)提示:处方模块被单独剥离,不是为了炫技,而是为满足《电子病历系统功能应用水平分级评价标准》中“处方操作必须独立审计、不可绕过”的硬性要求。所有处方变更操作都走prescription-service的统一入口,日志自动落库并同步至审计中心表audit_prescription_log。关键配置在prescription-service/src/main/resources/application.yml中:prescription: # 处方PDF模板路径,支持热替换(改完模板不用重启) template-path: classpath:templates/prescription.ftl # SM2签名私钥路径(生产环境务必改为KMS托管) sm2-private-key: classpath:keys/sm2_private.pem # 处方有效期:48小时(卫健委规定急诊处方24h,普通处方72h,此处取折中值) valid-hours: 48 # 审核超时自动作废(防医生漏审) audit-timeout-minutes: 30这段配置决定了处方生命周期的刚性边界。比如audit-timeout-minutes: 30不是随便写的——它触发的是一个 Quartz 定时任务,每5分钟扫描一次prescription_status = 'WAITING_AUDIT' AND create_time < now() - 30min的记录,批量调用PrescriptionService.autoCancel()方法。这个设计避免了用数据库触发器或监听器带来的运维黑盒问题,所有逻辑可查、可测、可打点。2.2 前端架构:Vue 3 Composition API 如何支撑多角色动态路由?前端src/router/index.ts里没有写死任何路由,而是通过asyncRoutes动态加载:// src/router/modules/doctor.ts const doctorRoutes: RouteRecordRaw[] = [ { path: '/consult', name: 'ConsultList', component: () => import('@/views/doctor/ConsultList.vue'), meta: { title: '待接诊列表', requireAuth: true, roles: ['DOCTOR'] // 角色白名单,后端返回的user.role字段匹配 } }, { path: '/prescription/create/:consultId', name: 'PrescriptionCreate', component: () => import('@/views/doctor/PrescriptionCreate.vue'), meta: { title: '开具处方', requireAuth: true, roles: ['DOCTOR'], keepAlive: true // 防止开方中途切页丢失草稿 } } ]重点看PrescriptionCreate.vue里的组合式逻辑:<script setup lang="ts"> import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import { getConsultDetail } from '@/api/consult' import { generatePrescription } from '@/api/prescription' const route = useRoute() const consultId = route.params.consultId as string const consultData = ref<any>({}) const prescriptionForm = ref({ diagnosis: '', drugs: [] as DrugItem[], // 药品数组,含用法用量、频次、天数 advice: '', isEmergency: false }) onMounted(async () => { // 1. 拉取问诊上下文(患者基本信息、历史诊断、本次聊天记录) consultData.value = await getConsultDetail(consultId) // 2. 自动填充常用诊断(从医生个人知识库匹配ICD-10编码) if (consultData.value.lastDiagnosis) { prescriptionForm.value.diagnosis = consultData.value.lastDiagnosis } }) const submitPrescription = async () => { try { // 3. 前端强校验:药品数量≤20种(卫健委限制)、单药剂量≤安全阈值 const validateResult = validatePrescription(prescriptionForm.value) if (!validateResult.valid) { ElMessage.error(validateResult.message) return } // 4. 调用后端处方生成接口(含SM2签名) const res = await generatePrescription({ consultId, ...prescriptionForm.value }) ElMessage.success('处方已生成,等待审核') router.push(`/prescription/detail/${res.prescriptionId}`) } catch (err) { ElMessage.error('处方生成失败:' + (err as any).message) } } </script>这里藏着两个关键设计:一是onMounted里不做纯UI初始化,而是主动拉取业务上下文,确保医生打开页面就能看到患者既往用药史;二是validatePrescription校验函数内置了本地规则引擎(非仅靠后端返回400),比如检查「阿莫西林胶囊」是否超7天用量,这种校验前端做,用户能秒级感知错误,而不是等后端返回再重填。3. 运行前必做的5项配置:从JDK版本到处方PDF字体别急着mvn clean install && npm run serve—— 这套源码对运行环境有明确约束,跳过配置大概率在「生成处方PDF」环节翻车。我按实际踩坑顺序列出来,每一步都带验证命令。3.1 JDK与Maven版本锁定:为什么必须用JDK 11?后端pom.xml显式声明:<properties> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties>原因在于common模块中使用的org.bouncycastle.crypto.params.SM2ParameterSpec类,在 JDK 17+ 中已被移除(迁移到jdk.crypto.cryptoki模块),而当前SM2签名逻辑强依赖Bouncy Castle 1.69。若强行用JDK 17,编译会报:error: package org.bouncycastle.crypto.params does not exist import org.bouncycastle.crypto.params.SM2ParameterSpec;✅ 验证方式:java -version # 必须输出:openjdk version "11.0.20" 2023-07-18 mvn -v | grep "Java version" # 必须输出:Java version: 11.0.203.2 MySQL字符集与表引擎:utf8mb4 + InnoDB 是硬门槛建库语句必须显式指定:CREATE DATABASE internet_hospital CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- 所有表必须用InnoDB(支持行锁+外键+事务) CREATE TABLE `prescription` ( `id` bigint NOT NULL AUTO_INCREMENT, `consult_id` bigint NOT NULL, `doctor_id` bigint NOT NULL, `patient_id` bigint NOT NULL, `diagnosis` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_consult_id` (`consult_id`), CONSTRAINT `fk_prescription_consult` FOREIGN KEY (`consult_id`) REFERENCES `consult` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;❌ 常见翻车点:用Navicat默认建库,字符集选了utf8(实际是utf8mb3),导致存储中药名「蔘」、「茋」等生僻字变乱码,后续PDF生成直接抛StringIndexOutOfBoundsException。✅ 验证SQL:SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'internet_hospital'; -- 必须返回 utf8mb4 和 utf8mb4_unicode_ci SHOW CREATE TABLE prescription\G -- ENGINE 必须为 InnoDB,CHARSET 必须为 utf8mb43.3 处方PDF生成:FreeMarker模板 + SimSun字体嵌入处方PDF由prescription-service调用freemarker.template.Configuration渲染HTML再转PDF。关键在prescription.ftl模板头部:<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> @font-face { font-family: 'SimSun'; src: url('classpath:fonts/simsun.ttc') format('truetype'); font-weight: normal; font-style: normal; } body { font-family: 'SimSun', sans-serif; } </style> </head> <body> <div class="header">XX互联网医院电子处方</div> ... </body> </html>⚠️ 注意:simsun.ttc文件必须放在prescription-service/src/main/resources/fonts/目录下。若缺失,PDF中所有中文会显示为方框(□□□),且itextpdf库不会报错,只会静默降级。✅ 验证方式:# 进入jar包检查字体文件是否存在 jar -tf prescription-service/target/prescription-service-1.0.jar | grep simsun # 必须输出:BOOT-INF/classes/fonts/simsun.ttc3.4 Redis连接配置:会话状态与处方缓存分离application.yml中 Redis 配置段:spring: redis: host: 127.0.0.1 port: 6379 database: 0 # 默认库:存储JWT Token黑名单 lettuce: pool: max-active: 20 cache: type: redis redis: time-to-live: 3600000 # 1小时,用于处方预览缓存(避免重复生成PDF) # 单独配置处方服务的Redis库(隔离缓存污染) prescription: redis-database: 2 # 专用库:存储处方草稿、审核队列✅ 验证命令:redis-cli -n 0 KEYS "token:*" | wc -l # 查JWT黑名单key数 redis-cli -n 2 KEYS "draft:*" | wc -l # 查处方草稿key数 # 两者应完全隔离,不能互相污染3.5 前端跨域代理:Vue CLI 的proxy配置陷阱vue.config.js中的代理配置:devServer: { proxy: { '/api': { target: 'http://localhost:8080', // 后端gateway端口 changeOrigin: true, pathRewrite: { '^/api': '/api' // 注意:这里不能写成 '/api/' 或 '',否则/api/consult会被重写成//consult } } } }❌ 翻车现场:某开发者把pathRewrite写成'^/api': '',导致请求/api/consult/list被代理成http://localhost:8080/consult/list,后端404。正确写法必须保留前缀/api,因为后端@RequestMapping("/api")是全局路径前缀。✅ 验证方式:浏览器F12 Network面板,查看请求URL是否为http://localhost:8080/api/consult/list,而非http://localhost:8080/consult/list。4. 常见问题排查:5个高频翻车点与血泪解决方案这套源码在真实部署中暴露出几个典型问题,都是新手容易忽略但老手一眼能定位的。我把它们按「现象→原因→解决」整理成可直接抄作业的清单。4.1 现象:患者端提交问诊后,医生端收不到新会话提醒原因:WebSocket连接未启用,或Nginx反向代理未透传Upgrade头定位步骤:浏览器F12 → Network → Filter输入ws,看是否有wss://xxx/consult/ws连接若无连接,检查consult-service/src/main/java/config/WebSocketConfig.java是否被注释若有连接但状态为Pending,检查Nginx配置是否包含:location /consult/ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # 关键! proxy_set_header Connection "upgrade"; # 关键! proxy_set_header Host $host; }解决:取消WebSocket配置类注释,并在Nginx中补全Upgrade头。重启Nginx后,WebSocket连接状态应变为101 Switching Protocols。4.2 现象:处方PDF生成后,打印时药品名称显示为「???」原因:FreeMarker模板中未正确引用SimSun字体,或字体文件路径错误定位步骤:进入prescription-service/target/classes/templates/prescription.ftl,确认<style>块中@font-face的src路径检查prescription-service/src/main/resources/fonts/simsun.ttc文件是否存在(注意是.ttc,不是.ttf)在Linux服务器上执行:file prescription-service/src/main/resources/fonts/simsun.ttc # 正确输出:TrueType font data, digitally signed, 4 tables, 1st "name", 12 names解决:若文件缺失,从Windows系统C:\Windows\Fonts\simsun.ttc复制(勿用网络下载的盗版字体,可能缺字)。重新打包后验证。4.3 现象:医生点击「开具处方」按钮无响应,控制台报Cannot read property 'push' of undefined原因:前端路由守卫中next()调用时机错误,导致router实例未初始化定位步骤:查看src/router/index.ts中router.beforeEach回调检查是否在store.dispatch('user/getInfo')的then回调外直接调用了next()错误写法:router.beforeEach(async (to, from, next) => { const hasToken = getToken() if (hasToken) { const userInfo = await store.dispatch('user/getInfo') // 异步获取用户信息 next() // ❌ 这里next()在await外,userInfo可能为空 } })解决:将next()移入await后:if (hasToken) { try { await store.dispatch('user/getInfo') next() // ✅ 确保用户信息加载完成后再放行 } catch (err) { next(`/login?redirect=${to.path}`) } }4.4 现象:MySQL插入处方时报错Data truncation: Data too long for column 'diagnosis' at row 1原因:prescription.diagnosis字段定义为varchar(200),但实际诊断描述超长(如「2型糖尿病伴周围神经病变、糖尿病肾病Ⅲ期、高血压3级(很高危)」)定位步骤:执行SHOW CREATE TABLE prescription\G,查看diagnosis字段长度检查后端实体类Prescription.java中对应字段:@Column(name = "diagnosis", length = 200) // ❌ 长度不足 private String diagnosis;解决:数据库执行:ALTER TABLE prescription MODIFY diagnosis VARCHAR(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;实体类改为:@Column(name = "diagnosis", length = 1000)同步更新MyBatis Plus的XML映射文件中resultMap对应字段长度4.5 现象:微信小程序端调用处方接口返回401 Unauthorized,但H5端正常原因:小程序端JWT Token未携带AuthorizationHeader,或Header名被微信客户端强制小写定位步骤:小程序端使用wx.request时,检查header是否为:header: { 'Authorization': 'Bearer ' + token // ✅ 必须首字母大写 }若仍失败,抓包看实际发出的请求Header(微信开发者工具 → Network → 详情 → Request Headers)常见问题:'authorization': 'Bearer xxx'(小写a),被微信底层转义后后端无法识别解决:强制首字母大写,并在后端JwtAuthenticationFilter.java中增加兼容逻辑:String authHeader = request.getHeader("Authorization"); if (authHeader == null) { authHeader = request.getHeader("authorization"); // 兼容微信小写 }5. 二次开发实战:如何在30分钟内接入医院HIS系统药品库这是我在某社区卫生服务中心落地时的真实改造需求:他们已有老旧HIS系统,药品数据存在Oracle数据库中,要求互联网医院处方模块能实时查询HIS药品目录,并自动带出医保编码、规格、单位。不改现有处方流程,只增强药品选择器。5.1 改造目标与边界定义✅ 允许:新增HIS药品查询接口、改造前端药品搜索框、扩展处方DTO增加HIS字段❌ 禁止:修改处方审核流、不碰SM2签名逻辑、不改动PDF模板结构⚠️ 边界:HIS查询走独立服务(避免拖慢主流程),超时3秒自动降级为本地缓存5.2 后端新增HIS药品服务模块在common模块下新建his包,添加HisDrugService.java:@Service @Slf4j public class HisDrugService { @Value("${his.jdbc-url}") private String jdbcUrl; @Value("${his.username}") private String username; @Value("${his.password}") private String password; // 使用HikariCP独立数据源,不共享主库连接池 private HikariDataSource hisDataSource; @PostConstruct public void init() { hisDataSource = new HikariDataSource(); hisDataSource.setJdbcUrl(jdbcUrl); hisDataSource.setUsername(username); hisDataSource.setPassword(password); hisDataSource.setMaximumPoolSize(5); hisDataSource.setConnectionTimeout(3000); // HIS查询超时3秒 } /** * 查询HIS药品(支持模糊搜索) * @param keyword 药品通用名或商品名 * @return List<HisDrugDto>,字段含:id, name, spec, unit,医保编码, price */ public List<HisDrugDto> searchDrugs(String keyword) { String sql = "SELECT drug_id, drug_name, spec, unit,医保编码, price " + "FROM his_drug WHERE drug_name LIKE ? OR pinyin_code LIKE ?"; try (Connection conn = hisDataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + PinyinUtil.toPinyin(keyword) + "%"); ResultSet rs = ps.executeQuery(); List<HisDrugDto> list = new ArrayList<>(); while (rs.next()) { list.add(HisDrugDto.builder() .id(rs.getString("drug_id")) .name(rs.getString("drug_name")) .spec(rs.getString("spec")) .unit(rs.getString("unit")) .insuranceCode(rs.getString("医保编码")) .price(rs.getBigDecimal("price")) .build()); } return list; } catch (SQLException e) { log.warn("HIS药品查询失败,降级为本地缓存", e); return localDrugCache.search(keyword); // 降级策略:查本地内存缓存 } } }注意:PinyinUtil.toPinyin(keyword)是自研拼音工具类,用于支持「阿莫西林」搜「amoxilin」。HIS表中pinyin_code字段需提前由DBA维护好。5.3 前端药品搜索框增强修改PrescriptionCreate.vue中的药品选择逻辑:<!-- 新增HIS搜索框 --> <el-input v-model="hisSearchKeyword" placeholder="输入药品名(支持拼音)" @input="debounceSearch" prefix-icon="Search" /> <!-- 搜索结果下拉 --> <el-select v-model="selectedHisDrug" filterable remote :remote-method="remoteSearchHisDrugs" :loading="hisLoading" placeholder="请选择HIS药品" > <el-option v-for="item in hisDrugOptions" :key="item.id" :label="`${item.name}(${item.spec} ${item.unit})医保:${item.insuranceCode}`" :value="item" /> </el-select> <script setup lang="ts"> import { ref, watch } from 'vue' import { searchHisDrugs } from '@/api/his' // 新增API const hisSearchKeyword = ref('') const hisDrugOptions = ref<HisDrugDto[]>([]) const hisLoading = ref(false) const selectedHisDrug = ref<HisDrugDto | null>(null) // 防抖搜索 const debounceSearch = _.debounce(() => { if (hisSearchKeyword.value.trim().length < 2) return remoteSearchHisDrugs(hisSearchKeyword.value) }, 300) const remoteSearchHisDrugs = async (keyword: string) => { hisLoading.value = true try { const res = await searchHisDrugs(keyword) hisDrugOptions.value = res } finally { hisLoading.value = false } } // 选中HIS药品后,自动填充到处方药品列表 watch(selectedHisDrug, (newVal) => { if (newVal) { const drugItem: DrugItem = { id: newVal.id, name: newVal.name, spec: newVal.spec, unit: newVal.unit, insuranceCode: newVal.insuranceCode, dosage: '', // 用量需医生手动填 frequency: '', // 频次 days: 0 // 天数 } prescriptionForm.value.drugs.push(drugItem) // 清空搜索框和下拉选项 hisSearchKeyword.value = '' hisDrugOptions.value = [] } }) </script>5.4 配置与验证:3步完成上线配置HIS连接参数(application.yml):his: jdbc-url: jdbc:oracle:thin:@192.168.10.5:1521:orcl username: his_user password: his_pass启动时验证HIS连通性:在HisDrugService.init()末尾加健康检查:try (Connection conn = hisDataSource.getConnection()) { conn.createStatement().executeQuery("SELECT 1 FROM DUAL"); log.info("✅ HIS数据库连接成功"); } catch (Exception e) { log.error("❌ HIS数据库连接失败,请检查配置", e); }灰度发布验证:先在测试环境开启logging.level.com.xxx.his=DEBUG,观察HIS查询耗时模拟HIS宕机:临时注释init()中的hisDataSource.setJdbcUrl(...),验证是否平滑降级到本地缓存生产发布时,用-Dspring.profiles.active=prod,his-enabled控制开关从那以后我每次接入第三方系统,都强制走一遍「独立数据源+超时控制+降级兜底」三板斧。不是怕它挂,是怕它慢——慢比挂更致命,会拖垮整个问诊链路。希望帮到你。本文还有配套的精品资源,点击获取