☰
SpringBoot+Vue+MySQL私人诊所管理系统:协同过滤推荐算法落地实践
2026/10/21 2:53:17 网站建设 项目流程

上个月帮一位开私人诊所的朋友做年终数据整理,他一边导出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映射成评分,我是这么定义的:freq=1记1.0分,freq=2记1.8分,freq=3以上记2.5分封顶,为什么要封顶?因为一个慢性病患者常年开同一种药,频次会异常高,如果不封顶,少数复购药品会淹没其他药品的信号。

3.3 相似度计算和推荐生成:代码级拆解

评分矩阵造好之后,我算药品两两相似度,用的不是教科书里最经典的余弦相似度,而是调整后的余弦相似度。普通余弦相似度不看每个患者平均用药种类的差异,有的患者每次开三种药,有的每次开两种,这会导致"开的药越多"的患者对相似度贡献越不公平。调整后的余弦相似度会对每个患者的评分减去该患者的平均评分,再代入余弦公式:

// 简化版:计算两个药品的调整后余弦相似度 public double adjustedCosineSimilarity( Map<Long, Double> item1Ratings, // 药品1的患者-评分Map Map<Long, Double> item2Ratings) { // 药品2的患者-评分Map Set<Long> 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 List<Recommendation> recommend(Long currentPatientId, List<Long> selectedDrugIds) { List<Recommendation> candidates = new ArrayList<>(); for (Long drugId : selectedDrugIds) { List<SimilarityItem> 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 为什么是SpringBoot+Vue+MySQL这个组合

这个搭配在前端分离项目里几乎是"最稳"的一套组合,不是因为它最潮,而是因为它生态成熟、招聘需求大、踩坑资料多。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、JWT(jjwt)、Druid连接池、Lombok。MyBatis-Plus的好处是单表CRUD连SQL都不用写,我把精力全放在推荐算法和业务联动上,这是很现实的选择。

4.3 数据库核心表设计

数据库我命名为clinic_db,一共11张表,核心的8张如下:

表名用途关键字段
sys_user系统用户id, username, password, role, real_name
patient患者档案id, name, gender, phone, allergy_history
doctor医生表id, user_id, title, specialty, department
medicine药品表id, name, category, spec, stock, warning_line
prescription处方主表id, patient_id, doctor_id, diagnose, total_amount, status
prescription_detail处方明细表id, prescription_id, medicine_id, quantity, price
visit_record就诊记录id, patient_id, doctor_id, visit_time, symptom
similarity_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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 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里补上serverTimezone=Asia/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,而是你真正理解了一件事:推荐算法不是一个炫技玩具,它能不能对业务产生价值,取决于你愿不愿意把业务数据先洗干净、组织好。处方数据结构化这件事本身没啥技术含量,但没有它,再漂亮的协同过滤公式也都是空中楼阁。如果你拿到源码准备二次开发,建议第一件事先去看数据库初始化脚本,把种子数据换成自己诊所真实的历史处方,其他的一切会自动变得顺理成章。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询