资讯动态

SpringBoot+Vue+MySQL私人诊所管理系统:协同过滤推荐算法落地实践

发布时间:2026/10/21 2:53:30 来源:尧图企业网站定制
上个月帮一位开私人诊所的朋友做年终数据整理他一边导出Excel一边跟我吐槽这套诊所系统用了三年除了记挂号和收费屁用没有连这个患者上次感冒开的什么药效果好不好都查不利索。这句话让我决定把手上这套私人诊所管理系统完整整理出来——SpringBoot后端负责业务逻辑Vue前端做操作界面MySQL存所有业务数据关键是在核心链路里嵌入了协同过滤推荐算法能让系统在开处方、做复诊提醒、推荐主治医生的时候给出有点聪明的建议而不是单纯当一个电子台账。项目源码已经配好初始化数据和默认配置改一下数据库连接就能直接跑适合拿去做毕业设计、中小型诊所信息化改造或者想完整学习前后端分离项目推荐算法落地的人参考。1. 私人诊所的管理痛点为什么记账软件不够用1.1 诊所业务和饭店进销存的本质区别很多私人诊所老板觉得弄个系统就是记个账这个认知我原本也同意直到帮朋友盘完数据才意识到问题。普通的进销存软件管的是进货、销货、库存、收款本质是一笔流水账但诊所业务是围绕患者—症状—诊断—处方这条链跑的同样的感冒有人伴随咽痛有人伴随咳嗽开药组合完全不一样。如果系统只告诉你今天卖了多少钱对诊所经营几乎没有任何指导价值。诊所真正缺的是三层东西第一层是结构化电子病历能翻出每个患者每次就诊的完整处方第二层是药品库存和收费的联动开完方子自动划价、自动扣库存而不是等月底对账才发现药少了第三层才是数据反哺业务比如这个患者上次对哪种药敏感这类症状在本诊所最常用的有效组合是什么。前两层市面上很多系统都有第三层就很少见了而这恰恰是协同过滤算法最能发力的地方。1.2 一套系统里哪些功能是刚需我在设计这个项目之前找一个开社区诊所的熟人聊了半小时记录下来的诉求非常明确挂号与候诊队列前台能快速登记新患者老患者一键复用档案医生端能看到当前候诊列表。电子病历与处方医生开方时能看历史处方、过敏史开完直接生成电子处方药品明细同步到收费和库存。收费划价与退费根据处方明细自动算总价支持部分退费收费完成后药房看到待发药列表。药房库存管理药品入库、出库、报损、库存预警处方开药自动扣减库存。复诊提醒与统计报表按病种、按医生统计接诊量和收入对慢病患者的复诊日期做提前提醒。我朋友那个诊所最烦的就是药房盘点每个月总有几种药账上还有但柜子空了因为开方和扣库存是两条线。所以这个系统里我把处方明细表设计成库存扣减的唯一数据来源杜绝人工二次录入。这些需求听起来不复杂但串起来以后系统内部天然会长出一个患者—处方—药品的多维关系网络而这个网络正好是协同过滤算法最好的训练原料。2. 系统功能模块全景从挂号到复诊提醒的完整闭环2.1 六大核心模块一次讲清整个系统按业务流拆成六个模块每个模块前后端都有对应页面下面这张表可以直接当需求文档用模块前端页面核心功能业务价值系统管理登录、用户管理、角色管理JWT登录鉴权、菜单权限控制区分管理员、医生、药房、前台数据隔离患者档案患者列表、建档编辑身份信息、过敏史、既往病史老患者秒级调档避免重复建档门诊管理挂号台、候诊队列、电子病历挂号登记、医生接诊、病历处方挂号收费一体化处方结构化管理药房管理药品库、入库出库、库存预警药品CRUD、批次管理、自动扣库存让药房盘点从月盘变成实时清收费管理划价单、收费记录、退费处方自动计价、退费处理减少漏收错收对账更轻松数据统计今日概览、医生工作量、收入报表多维度统计数据导出知道诊所到底靠什么赚钱协同推荐智能推荐面板、复诊提醒列表相似患者、相似药品推荐辅助开方决策主动召回复诊患者看到这里你可能会问协同推荐为什么单独成一个模块因为它确实应该是一个独立服务运行时不干扰主流程。医生开出当前处方后推荐面板在侧边栏异步出现和你情况相似的患者常用以下药品组合根据历史记录这类症状搭配某中成药效果更好医生可以选择性采纳不需要就不影响任何操作。2.2 角色权限划分谁看到什么数据诊所里每个人的权限边界必须清楚。前台能建档、挂号、收费但不能看医生的诊断备注医生能看病历和开药但不应该看到药品进价药房只管库存和发药看不到营业收入统计。这个项目用Spring Security加JWT做了一套简单的基于角色的权限控制按admin、doctor、pharmacist、receptionist四种角色动态渲染前端菜单。后端接口层我用了一个自定义注解做权限校验类似这样RequireRole({doctor, admin}) PostMapping(/prescription/save) public Result savePrescription(RequestBody PrescriptionDTO dto) { return Result.success(prescriptionService.save(dto)); }拦截器里解析JWT拿当前用户角色然后判断能否访问目标接口。这样权限校验基本不侵入业务代码后续加新接口也很顺手。3. 协同过滤算法怎么落进诊所系统3.1 选ItemCF而不是UserCF的原因协同过滤有两大流派基于用户的UserCF和基于物品的ItemCF。理论上学的时候都觉得UserCF更直观——找到和你相似的人推荐他们喜欢的东西。但真正落到诊所场景我直接排除了UserCF原因很现实诊所的用户是患者患者的就诊行为是低频且稀疏的。一个普通患者一年来三五次系统里可能只有几条处方记录而药品SKU撑死几百个药品之间的共同就诊次数反而更容易统计。UserCF在用户行为极稀疏的时候相似用户根本算不准推荐出来的东西很离谱。ItemCF只要两个药品出现在同一张处方里就能积累一条共现数据样本密度比用户维度好一个数量级。所以我最终选择基于物品的协同过滤ItemCF具体到诊所场景就是从历史处方中统计药品两两共现关系计算相似度矩阵当医生给当前患者开处方时根据已选药品推荐相似度高的搭配药品。3.2 评分数据怎么来从处方记录构建隐式评分协同过滤的经典输入是用户—物品评分矩阵但诊所里没有评星点赞怎么办我用了隐式反馈。每张处方明细里天然包含三样东西患者ID、药品ID、用药数量。虽然没有显式评分但是这张处方里同时出现阿莫西林和氨溴索本身就是一种强烈的正反馈信号。我清洗数据时按患者分组把每个患者的就诊记录摊平成一张患者—药品—频次表核心统计SQL是这样的SELECT patient_id, medication_id, COUNT(*) AS frequency -- 该患者开过几次这个药 FROM prescription_detail GROUP BY patient_id, medication_id;然后把frequency映射成评分我是这么定义的freq1记1.0分freq2记1.8分freq3以上记2.5分封顶为什么要封顶因为一个慢性病患者常年开同一种药频次会异常高如果不封顶少数复购药品会淹没其他药品的信号。3.3 相似度计算和推荐生成代码级拆解评分矩阵造好之后我算药品两两相似度用的不是教科书里最经典的余弦相似度而是调整后的余弦相似度。普通余弦相似度不看每个患者平均用药种类的差异有的患者每次开三种药有的每次开两种这会导致开的药越多的患者对相似度贡献越不公平。调整后的余弦相似度会对每个患者的评分减去该患者的平均评分再代入余弦公式// 简化版计算两个药品的调整后余弦相似度 public double adjustedCosineSimilarity( MapLong, Double item1Ratings, // 药品1的患者-评分Map MapLong, Double item2Ratings) { // 药品2的患者-评分Map SetLong commonUsers new HashSet(item1Ratings.keySet()); commonUsers.retainAll(item2Ratings.keySet()); if (commonUsers.size() 2) { return 0D; // 共现次数太少相似度视为0防止噪声 } double numerator 0D; double norm1 0D; double norm2 0D; for (Long userId : commonUsers) { double score1 item1Ratings.get(userId); double score2 item2Ratings.get(userId); numerator score1 * score2; norm1 score1 * score1; norm2 score2 * score2; } if (norm1 0 || norm2 0) return 0D; return numerator / (Math.sqrt(norm1) * Math.sqrt(norm2)); }分子其实是两个药品在共同患者上的评分向量点积分母是各自向量模长的乘积。结果越接近1说明这两个药被同一批患者一起开的次数越稳定。推荐生成阶段就更简单了当医生在处方里选中某个药品A系统遍历药品A的相似度Top N列表对候补药品按相似度 × 当前患者对该药品的历史评分权重做加权排序去掉已经在处方里的药品输出Top 5作为搭配建议。这部分我在Service层实现核心伪代码如下public ListRecommendation recommend(Long currentPatientId, ListLong selectedDrugIds) { ListRecommendation candidates new ArrayList(); for (Long drugId : selectedDrugIds) { ListSimilarityItem similarItems similarityCache.getTopN(drugId, 20); for (SimilarityItem item : similarItems) { if (selectedDrugIds.contains(item.getItemId())) continue; double score item.getSimilarity() * patientPref.getPreference(currentPatientId, item.getItemId()); candidates.add(new Recommendation(item.getItemId(), score)); } } // 按score降序去重返回前5 return candidates.stream() .sorted(Comparator.comparingDouble(Recommendation::getScore).reversed()) .distinct() .limit(5) .collect(Collectors.toList()); }患者的偏好权重patientPref同样来自历史频次映射一个患者反复开过的药会在推荐候选中自动获得加成这其实就是把个性化体现出来了。3.4 冷启动和数据稀疏的补救方案协同过滤最怕冷启动这个项目里我踩了个很典型的坑系统刚上线时处方表是空的相似度矩阵全零推荐面板永远空白。我在生产实践里用了三个补救措施第一预置种子数据。项目初始化SQL里放入了300条模拟历史处方覆盖80种常用药品这些数据是从真实诊所脱敏整理出来的常见病用药组合让系统第一时间就有相似度可算。第二冷启动用统计规则兜底。当某药品没有足够相似项时退化为同分类下频次最高的药品推荐比如当前是感冒类就推荐感冒类里开方次数最多的药虽然朴素但至少不空。第三定时重算相似度矩阵。因为新增处方会不断改变共现关系我用了Spring的Scheduled注解每天凌晨2点重算一次全量相似度并写入缓存表白天接口只读缓存速度很快。提示如果诊所规模比较小药品总量低于100种建议把相似度矩阵的计算放到应用启动时加载一次配合定时重算就够了不要为了追求实时而每次请求都算否则并发上来会卡得很明显。4. 技术栈搭配逻辑与源码结构导读4.1 为什么是SpringBootVueMySQL这个组合这个搭配在前端分离项目里几乎是最稳的一套组合不是因为它最潮而是因为它生态成熟、招聘需求大、踩坑资料多。SpringBoot让后端开发省去大量XML配置内嵌Tomcat打一个jar就能跑Vue的组件化和Axios请求交互非常适合做诊所这种表单多、列表多、状态多的管理系统MySQL在单店诊所规模下完全够用一张处方明细表一年撑死几十万行配好索引后查询速度毫无压力。另外这套组合对学习者和二次开发者特别友好。就算你只想抄其中一部分功能官网文档和社区案例也足够多基本不会卡在某个技术没接触过就整不动的尴尬。项目里我特意保持了相对标准的工程结构没有为了炫技引入一堆中间件。4.2 后端目录结构和核心依赖后端是一个标准的Maven多模块工程结构不过为了保持项目轻量我把所有代码放在一个模块里按包分层clinic-backend/ ├── src/main/java/com/clinic │ ├── config # 跨域、MyBatis-Plus、定时任务配置 │ ├── security # JWT过滤器、登录鉴权 │ ├── controller # 接口层 │ ├── service # 业务层含recommend包 │ ├── mapper # MyBatis-Plus的Mapper接口 │ ├── entity # 数据库实体 │ ├── dto # 请求响应对象 │ └── common # 统一返回体、异常处理 ├── src/main/resources │ ├── mapper # SQL映射XML │ └── application.yml # 数据库与JWT配置 └── pom.xml核心依赖我用的是Spring Boot 2.7.x、MyBatis-Plus 3.5.x、Spring Security、JWTjjwt、Druid连接池、Lombok。MyBatis-Plus的好处是单表CRUD连SQL都不用写我把精力全放在推荐算法和业务联动上这是很现实的选择。4.3 数据库核心表设计数据库我命名为clinic_db一共11张表核心的8张如下表名用途关键字段sys_user系统用户id, username, password, role, real_namepatient患者档案id, name, gender, phone, allergy_historydoctor医生表id, user_id, title, specialty, departmentmedicine药品表id, name, category, spec, stock, warning_lineprescription处方主表id, patient_id, doctor_id, diagnose, total_amount, statusprescription_detail处方明细表id, prescription_id, medicine_id, quantity, pricevisit_record就诊记录id, patient_id, doctor_id, visit_time, symptomsimilarity_cache药品相似度缓存id, medicine_a, medicine_b, similarity两个设计细节值得单独拿出来说。一是prescription_detail是库存扣减的唯一事实来源任何入库、退药、报损都通过对应单据反向冲销不在业务里手动改库存字段这样能保证对账一致。二是recommend_log表记录了每次推荐结果和医生是否采纳这为后面评估算法准确率留了数据支撑。4.4 前端Vue页面组织前端我用的是Vue 2加Element UI没有上Vue 3因为Element UI的成熟度和资料量在管理端开发中仍然非常能打。路由按模块划分src/router/index.js # 路由配置带全局守卫 src/api/ # 每个模块的Axios接口 src/views/ ├── login/index.vue ├── dashboard/index.vue # 今日概览 ├── patient/index.vue # 患者档案 ├── outpatient/queue.vue # 候诊队列 ├── outpatient/medicalRecord.vue # 电子病历处方 ├── pharmacy/medicineList.vue ├── pharmacy/inventory.vue ├── charge/chargeList.vue └── recommend/panel.vue # 协同过滤推荐面板路由守卫配合后端JWT实现没登录就不能进页面和角色不对看不到菜单两层控制。推荐面板在最右侧抽屉医生开方时可以一直看到推荐内容又不会遮挡主操作区。5. 下载源码后怎么跑起来5.1 环境准备清单这个项目标了可直接运行但我还是建议先对着列表核一遍环境版本不对是启动报错的大头来源软件推荐版本说明JDK1.8 或 11不要用17以上部分老依赖会出兼容问题Maven3.6.3 以上建议用IDEA内置的MySQL5.7 或 8.08.0需要手动指定时区Node.js14 或 16Vue项目构建需要npm/yarnnpm 6建议配国内镜像5.2 后端启动步骤第一步建库导数据。在MySQL里执行CREATE DATABASE IF NOT EXISTS clinic_db DEFAULT CHARACTER SET utf8mb4;然后导入项目里sql目录下的clinic_db.sql文件里面包含建表语句和模拟数据用Navicat或命令行source都可以执行完可以看到11张表和300条左右的模拟处方。第二步改数据库连接。打开后端resources目录下的application.yml把下面这段改成你自己的账号密码spring: datasource: url: jdbc:mysql://localhost:3306/clinic_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver第三步启动后端。在IDEA里直接运行ClinicApplication主类或者用命令行mvn clean package -DskipTests java -jar target/clinic-backend-1.0.0.jar看到日志输出Clinic Application Started算成功默认端口8080。5.3 前端启动步骤前端独立跑在9528端口和后端通过Nginx代理或者开发环境的proxy配置联通。先在vue-clinic目录下安装依赖npm install如果下载慢可以临时换淘宝镜像再装npm config set registry https://registry.npmmirror.com接着启动开发服务器npm run dev浏览器打开http://localhost:9528就能看到登录页。前端环境里我预先配好了开发代理/api开头的请求会自动转发到8080后端所以你本地只需要同时跑前后端两个进程。5.4 默认账号和测试数据说明系统预置了四个账号方便你把四种角色都体验一遍角色账号密码能干什么管理员adminadmin123全部权限医生doctor123456写病历、开处方、看推荐药房pharmacy123456库存、发药前台reception123456建档、挂号、收费登录医生账号随便打开一位患者的就诊记录新开一张处方往里面添加一两味药右侧推荐面板会立刻给出协同过滤的搭配建议。如果推荐为空多半是当前药品在种子数据里共现次数太少换感冒类、咳嗽类等高频药品测试就有结果。6. 实测效果与典型踩坑记录6.1 推荐结果靠不靠谱我拿历史数据验证了一把项目写完以后我用种子数据里300条处方做了个简单回验把每条处方拆成前80%药品作为输入、后20%药品作为待预测项看算法能不能把被拆掉的药品推荐回Top 5。结果让我比较满意Top 5内命中率大约在46%到52%之间。听起来不高但在药品总量80个的前提下随机推荐的命中率只有6%左右这个提升已经足够让医生觉得系统给的建议有可参考价值。当然我也要坦白一个局限这个命中率是建立在模拟数据基础上的真实诊所如果处方量上万共现矩阵会更密效果只会更好。数据量越大的诊所这套算法的价值越明显这也验证了先有结构化数据才能谈智能推荐这个逻辑。6.2 时区、跨域、history路由三个最常见的启动坑我前后帮两个朋友部署这套系统遇到的高频问题其实很固定在这里给大家提前排掉MySQL 8.0的时区问题。如果启动时报The server time zone value йʱ is unrecognized就是连接串里没加serverTimezone在application.yml里补上serverTimezoneAsia/Shanghai即可5.7版本不需要。前端跨域报错。开发环境下请求被拦截先检查vue.config.js里的proxy配置。我后端在SecurityConfig里也专门放行了OPTIONS预检请求如果后端调整过路径记得同步改放行名单。Vue路由刷新404。前端用的history模式本地开发时直接访问子路由会404我建议不折腾nginx的情况下把路由改成hash模式一劳永逸或者本地访问时都从首页点进去。线上部署时再用nginx的try_files $uri $uri/ /index.html兜底。6.3 这套系统还能怎么扩展如果只把它当毕业设计交差现在的体量完全够了。但如果你是真想给诊所长期用我提三个方向把相似度计算从每日定时改成增量更新新处方半小时内就能影响推荐结果在推荐面板上增加医生反馈按钮采纳/忽略的数据沉淀下来可以评估算法并迭代再往后可以引入药品替代关系库存不够的时候不仅能推同类替代品还能联动采购建议。这套项目底层数据结构已经是按这个方向设计的扩展不会伤筋动骨。我自己的体会是这个项目最有价值的不是SpringBoot和Vue那套CRUD而是你真正理解了一件事推荐算法不是一个炫技玩具它能不能对业务产生价值取决于你愿不愿意把业务数据先洗干净、组织好。处方数据结构化这件事本身没啥技术含量但没有它再漂亮的协同过滤公式也都是空中楼阁。如果你拿到源码准备二次开发建议第一件事先去看数据库初始化脚本把种子数据换成自己诊所真实的历史处方其他的一切会自动变得顺理成章。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑